diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 451e62bf..6e7b3f9e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -39,7 +39,7 @@ jobs: fetch-depth: 0 fetch-tags: true - - name: Install Qt native version (the one provided by aqt doesn't seem to work) + - name: Install Qt native version (the one provided by aqt does not seem to work) uses: jurplel/install-qt-action@v4 with: aqtversion: '==3.1.*' diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 10a298f8..975b723d 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -65,16 +65,6 @@ jobs: modules: 'qtcharts qtpositioning' cache: true - - name: Debug output - shell: bash - run: | - echo "${{github.workspace}}/qt/Qt/6.8.1": - ls ${{github.workspace}}/qt/Qt/6.8.1 - echo "===" - echo "${QT_ROOT_DIR}/lib/cmake/Qt6Linguist:" - ls ${QT_ROOT_DIR}/lib/cmake/Qt6Linguist - echo "===" - - name: Configure env: CC: ${{ matrix.CC }} diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 3334306b..41883fd0 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -58,6 +58,8 @@ GlParams gl_tex_params(gl_engine::Texture::Format format) return { GL_RG8, GL_RG, GL_UNSIGNED_BYTE, 2, 1, true }; case F::RG32UI: return { GL_RG32UI, GL_RG_INTEGER, GL_UNSIGNED_INT, 2, 4 }; + case F::RGB32UI: + return { GL_RGB32UI, GL_RGB_INTEGER, GL_UNSIGNED_INT, 3, 4 }; case F::R8UI: return { GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, 1, 1 }; case F::R16UI: @@ -230,8 +232,9 @@ template void gl_engine::Texture::upload(const nucleus::Raster& template void gl_engine::Texture::upload(const nucleus::Raster&, unsigned); template void gl_engine::Texture::upload(const nucleus::Raster&, unsigned); template void gl_engine::Texture::upload(const nucleus::Raster&, unsigned); -template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); +template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); +template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned); @@ -260,6 +263,7 @@ template void gl_engine::Texture::upload(const nucleus::Raster template void gl_engine::Texture::upload(const nucleus::Raster&); template void gl_engine::Texture::upload(const nucleus::Raster&); template void gl_engine::Texture::upload>(const nucleus::Raster>&); +template void gl_engine::Texture::upload>(const nucleus::Raster>&); template void gl_engine::Texture::upload>(const nucleus::Raster>&); template void gl_engine::Texture::upload>(const nucleus::Raster>&); template void gl_engine::Texture::upload>(const nucleus::Raster>&); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 0632f4ac..808bb053 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -39,6 +39,7 @@ class Texture { RGBA32F, RG8, // normalised on gpu RG32UI, + RGB32UI, R8UI, R16UI, R32UI, @@ -84,7 +85,12 @@ class Texture { extern template void gl_engine::Texture::upload(const nucleus::Raster&); extern template void gl_engine::Texture::upload(const nucleus::Raster&); extern template void gl_engine::Texture::upload>(const nucleus::Raster>&); +extern template void gl_engine::Texture::upload>(const nucleus::Raster>&); extern template void gl_engine::Texture::upload>(const nucleus::Raster>&); extern template void gl_engine::Texture::upload>(const nucleus::Raster>&); +extern template void gl_engine::Texture::upload(const nucleus::Raster&, unsigned int); +extern template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned int); +extern template void gl_engine::Texture::upload>(const nucleus::Raster>&, unsigned int); + } // namespace gl_engine diff --git a/nucleus/CMakeLists.txt b/nucleus/CMakeLists.txt index a50455b7..059a8fb2 100644 --- a/nucleus/CMakeLists.txt +++ b/nucleus/CMakeLists.txt @@ -29,6 +29,7 @@ if(ALP_ENABLE_LABELS) alp_add_git_repository(vector_tiles URL https://github.com/AlpineMapsOrg/vector-tile.git COMMITISH faba88257716c4bc01ebd44d8b8b98f711ecb78c) endif() alp_add_git_repository(goofy_tc URL https://github.com/AlpineMapsOrgDependencies/Goofy_slim.git COMMITISH 13b228784960a6227bb6ca704ff34161bbac1b91 DO_NOT_ADD_SUBPROJECT) +alp_add_git_repository(cdt URL https://github.com/artem-ogre/CDT.git COMMITISH 46f1ce1f495a97617d90e8c833d0d29406335fdf DO_NOT_ADD_SUBPROJECT) add_library(zppbits INTERFACE) target_include_directories(zppbits SYSTEM INTERFACE ${zppbits_SOURCE_DIR}) @@ -40,6 +41,9 @@ set_target_properties(goofy_tc PROPERTIES SYSTEM true) add_library(tl_expected INTERFACE) target_include_directories(tl_expected INTERFACE ${tl_expected_SOURCE_DIR}/include) +add_library(cdt INTERFACE) +target_include_directories(cdt INTERFACE ${cdt_SOURCE_DIR}/CDT/include) + set(alp_version_out ${CMAKE_BINARY_DIR}/alp_version/nucleus/version.cpp) # cmake tests for existance of ${alp_version_out}.do_always_run. since it's always missing, cmake tries to generate it using this command. @@ -113,6 +117,7 @@ qt_add_library(nucleus STATIC tile/GeometryScheduler.h tile/GeometryScheduler.cpp utils/error.h utils/lang.h + utils/rasterizer.h utils/rasterizer.cpp tile/SchedulerDirector.h tile/SchedulerDirector.cpp tile/drawing.h tile/drawing.cpp camera/gesture.h @@ -147,7 +152,7 @@ endif() target_include_directories(nucleus PUBLIC ${CMAKE_SOURCE_DIR}) # Please keep Qt::Gui outside the nucleus. If you need it optional via a cmake based switch -target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits tl_expected nucleus_version stb_slim goofy_tc) +target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits tl_expected nucleus_version stb_slim goofy_tc cdt) qt_add_resources(nucleus "icons" PREFIX "/map_icons" diff --git a/nucleus/tile/conversion.h b/nucleus/tile/conversion.h index 91a9cd1f..403a9a72 100644 --- a/nucleus/tile/conversion.h +++ b/nucleus/tile/conversion.h @@ -115,4 +115,38 @@ inline glm::u8vec4 uint162alpineRGBA(uint16_t v) { return { v >> 8, v & 255, 0, 255 }; } + +inline QImage u8raster_to_qimage(const nucleus::Raster& raster) +{ + size_t width = raster.width(); + size_t height = raster.height(); + + QImage image(width, height, QImage::Format_Grayscale8); + + for (size_t y = 0; y < height; ++y) { + uchar* line = image.scanLine(y); + for (size_t x = 0; x < width; ++x) { + line[x] = raster.pixel({ x, y }); + } + } + + return image; +} + +inline QImage u8raster_2_to_qimage(const nucleus::Raster& raster1, const nucleus::Raster& raster2) +{ + size_t width = raster1.width(); + size_t height = raster1.height(); + + QImage image(width, height, QImage::Format_RGB888); + + for (size_t y = 0; y < height; ++y) { + for (size_t x = 0; x < width; ++x) { + image.setPixel(QPoint(x, y), (raster1.pixel({ x, y }) << 16) + (raster2.pixel({ x, y }) << 8)); + } + } + + return image; +} + } // namespace nucleus::tile::conversion diff --git a/nucleus/utils/rasterizer.cpp b/nucleus/utils/rasterizer.cpp new file mode 100644 index 00000000..cc621b93 --- /dev/null +++ b/nucleus/utils/rasterizer.cpp @@ -0,0 +1,103 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Lucas Dworschak + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "rasterizer.h" + +#include + +namespace nucleus::utils::rasterizer { + +std::vector generate_neighbour_edges(std::vector polygon_points, const size_t start_offset) +{ + std::vector edges; + { // create the edges + edges.reserve(polygon_points.size()); + for (size_t i = 0; i < polygon_points.size() - 1; i++) { + edges.push_back(glm::ivec2(start_offset + int(i), start_offset + int(i + 1))); + } + + // last edge between start and end vertex + edges.push_back(glm::ivec2(start_offset + polygon_points.size() - 1, start_offset)); + } + + return edges; +} + +std::vector triangulize(std::vector polygon_points, std::vector edges, bool remove_duplicate_vertices) +{ + std::vector processed_triangles; + + // triangulation + CDT::Triangulation cdt; + + if (remove_duplicate_vertices) { + CDT::RemoveDuplicatesAndRemapEdges( + polygon_points, + [](const glm::vec2& p) { return p.x; }, + [](const glm::vec2& p) { return p.y; }, + edges.begin(), + edges.end(), + [](const glm::ivec2& p) { return p.x; }, + [](const glm::ivec2& p) { return p.y; }, + [](CDT::VertInd start, CDT::VertInd end) { return glm::ivec2 { start, end }; }); + } + + cdt.insertVertices(polygon_points.begin(), polygon_points.end(), [](const glm::vec2& p) { return p.x; }, [](const glm::vec2& p) { return p.y; }); + cdt.insertEdges(edges.begin(), edges.end(), [](const glm::ivec2& p) { return p.x; }, [](const glm::ivec2& p) { return p.y; }); + cdt.eraseOuterTrianglesAndHoles(); + + // fill our own data structures + for (size_t i = 0; i < cdt.triangles.size(); ++i) { + auto tri = cdt.triangles[i]; + + std::vector tri_indices = { tri.vertices[0], tri.vertices[1], tri.vertices[2] }; + + int top_index = (cdt.vertices[tri.vertices[0]].y < cdt.vertices[tri.vertices[1]].y) ? ((cdt.vertices[tri.vertices[0]].y < cdt.vertices[tri.vertices[2]].y) ? 0 : 2) + : ((cdt.vertices[tri.vertices[1]].y < cdt.vertices[tri.vertices[2]].y) ? 1 : 2); + // for middle and bottom index we first initialize them randomly with the values that still need to be tested + int middle_index; + int bottom_index; + if (top_index == 0) { + middle_index = 1; + bottom_index = 2; + } else if (top_index == 1) { + middle_index = 2; + bottom_index = 0; + } else { + middle_index = 0; + bottom_index = 1; + } + + // and now we test if we assigned them correctly + if (cdt.vertices[tri.vertices[middle_index]].y > cdt.vertices[tri.vertices[bottom_index]].y) { + // if not we have to interchange them + int tmp = middle_index; + middle_index = bottom_index; + bottom_index = tmp; + } + + // lastly add the vertices to the vector in the correct order + processed_triangles.push_back({ cdt.vertices[tri.vertices[top_index]].x, cdt.vertices[tri.vertices[top_index]].y }); + processed_triangles.push_back({ cdt.vertices[tri.vertices[middle_index]].x, cdt.vertices[tri.vertices[middle_index]].y }); + processed_triangles.push_back({ cdt.vertices[tri.vertices[bottom_index]].x, cdt.vertices[tri.vertices[bottom_index]].y }); + } + + return processed_triangles; +} + +} // namespace nucleus::utils::rasterizer diff --git a/nucleus/utils/rasterizer.h b/nucleus/utils/rasterizer.h new file mode 100644 index 00000000..7168d795 --- /dev/null +++ b/nucleus/utils/rasterizer.h @@ -0,0 +1,558 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Lucas Dworschak + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include +#include + +#include + +#include + +namespace nucleus::utils::rasterizer { +/* + * Possible future improvements: + * - check how often each pixel is written to -> maybe we can improve performance here + */ + +/* PixelWriterFunctionConcept + * The functions use a PixelWriterFunction as a parameter. + * The pixelwriter function is a lambda with one glm::ivec2 position parameter and one optional unsigned int data argument + * glm::ivec2 position + * the position where a pixel should be rendered + * unsigned int data + * the triangle/line-segment index + */ +template +concept PixelWriterFunctionConcept = requires(Func f) { + { f(glm::ivec2(11, 22)) }; +} || requires(Func f) { + { f(glm::ivec2(11, 22), 123456u) }; +}; + +namespace details { + + /* + * This function calls either the 1 parameter or the 2 parameter pixelwriter (determined at runtime) + */ + template inline void invokePixelWriter(const PixelWriterFunction& pixel_writer, glm::ivec2 position, unsigned int data_index) + { + if constexpr (std::is_invocable_v) { + pixel_writer(position, data_index); + } else if constexpr (std::is_invocable_v) { + pixel_writer(position); + } + } + + /* + * Calculates the x value for a given y. + * The resulting x value will be on the line. + */ + inline float get_x_for_y_on_line(const glm::vec2& point_on_line, const glm::vec2& line, float y) + { + // pos_on_line = point_on_line + t * line + float t = (y - point_on_line.y) / line.y; + + return point_on_line.x + t * line.x; + } + + /* + * calculate the x value of a line + * uses fallback lines if the y value is outside of the line segment + */ + inline int get_x_with_fallback(const glm::vec2& point, + const glm::vec2& line, + float y, + bool is_enlarged, + const glm::vec2& fallback_point_bottom, + const glm::vec2& fallback_line_bottom, + const glm::vec2& fallback_point_top, + const glm::vec2& fallback_line_top) + { + // decide whether or not to fill only to the fallback line (if outside of other line segment) + if (is_enlarged && y < point.y) + return get_x_for_y_on_line(fallback_point_top, fallback_line_top, y); + else if (is_enlarged && y > point.y + line.y) + return get_x_for_y_on_line(fallback_point_bottom, fallback_line_bottom, y); + else + return get_x_for_y_on_line(point, line, y); + } + + /* + * fills a scanline in the intveral [x1,x2] (inclusive) + */ + template inline void fill_between(const PixelWriterFunction& pixel_writer, int y, int x1, int x2, unsigned int data_index) + { + int fill_direction = (x1 < x2) ? 1 : -1; + + for (int j = x1; j != x2 + fill_direction; j += fill_direction) { + invokePixelWriter(pixel_writer, glm::ivec2(j, y), data_index); + } + } + + /* + * renders the given line + * if other line is given fills everything in between the current and the other line + * if the other line is not located on the current scan line -> we fill to the given fall back lines + * the given line should always go from top to bottom (only exception horizontal lines) + */ + template + void render_line(const PixelWriterFunction& pixel_writer, + unsigned int data_index, + const glm::vec2& line_point, + const glm::vec2& line, + const int fill_direction = 0, + const glm::vec2& other_point = { 0, 0 }, + const glm::vec2& other_line = { 0, 0 }, + const glm::vec2& fallback_point_bottom = { 0, 0 }, + const glm::vec2& fallback_line_bottom = { 0, 0 }, + const glm::vec2& fallback_point_top = { 0, 0 }, + const glm::vec2& fallback_line_top = { 0, 0 }) + { + // if a line would go directly through integer points, we would falsely draw a whole pixel -> we want to prevent this + constexpr float epsilon = 0.00001; + + // find out how many y steps lie between origin and line end + const int y_steps = floor(line_point.y + line.y) - floor(line_point.y); + + // const bool is_enlarged_triangle = normal_line != glm::vec2 { 0, 0 }; + const bool fill = other_line != glm::vec2 { 0, 0 }; // no other line given? -> no fill needed + const bool is_enlarged = fallback_line_bottom != glm::vec2 { 0, 0 }; // no fallback lines given -> not enlarged + + // create variables to remember where to check for x values for the fill_between fill + // -> in the current scan line do we check the top of the pixel or the bottom of the pixel to fill a line to completion + // this assumes that line is left of other line (if it is the other way around this will be fixed further down) + bool fill_current_from_top_x = (line.x > 0) ? true : false; + bool fill_other_from_top_x = (other_line.x > 0) ? false : true; + + if (fill && fill_direction < 0) { + // we have to switch where to look for the fill line to stop + fill_current_from_top_x = !fill_current_from_top_x; + fill_other_from_top_x = !fill_other_from_top_x; + } + + // special cases for line start/end + if line is smaller than 1px + if (y_steps == 0) { + // line is smaller than one scan line + // only draw from line start to end + if (!fill) + fill_between(pixel_writer, line_point.y, line_point.x, line.x + line_point.x, data_index); + else { + // go from top or bottom of line (depending on where the other line is located) + const auto x1 = (fill_current_from_top_x) ? line_point.x : line_point.x + line.x; + + const float test_y_other = (fill_other_from_top_x) ? line_point.y : line_point.y + line.y; + const auto x2 = get_x_with_fallback(other_point, other_line, test_y_other, is_enlarged, fallback_point_bottom, fallback_line_bottom, fallback_point_top, fallback_line_top); + + fill_between(pixel_writer, line_point.y, x1, x2, data_index); + } + } else { + // draw first and last stretches of the line + if (!fill) { + // start of the line (start to bottom of pixel) + int x_start_bottom = get_x_for_y_on_line(line_point, line, ceil(line_point.y) - epsilon); + fill_between(pixel_writer, line_point.y, line_point.x, x_start_bottom, data_index); + + // end of the line (top of pixel to end of line) + int x_end_top = get_x_for_y_on_line(line_point, line, floor(line_point.y + line.y) + epsilon); + fill_between(pixel_writer, line_point.y + line.y, line_point.x + line.x, x_end_top, data_index); + } else { + { // start of the line + // const float line_top_y = line_point.y; + // in case that the current point is exactly on an integer value, we first have to increase the value by a small amount then ceil it. + const float bottom_y = ceil(line_point.y + epsilon) - epsilon; // bottom of the y step + + // either use line start or calculate the x at the bottom of the pixel + const auto x1 = (fill_current_from_top_x) ? line_point.x : get_x_for_y_on_line(line_point, line, bottom_y); + + // decide whether or not to fill only to the fallback line (if outside of other line segment) + // although this is the end of the line -> we have to test both upper and lower of the other line for large enlarged triangles and small other lines + const float test_y_other = (fill_other_from_top_x) ? line_point.y : bottom_y; + const auto x2 = get_x_with_fallback(other_point, other_line, test_y_other, is_enlarged, fallback_point_bottom, fallback_line_bottom, fallback_point_top, fallback_line_top); + + fill_between(pixel_writer, line_point.y, x1, x2, data_index); + } + + { // end of the line + const float line_bottom_y = line_point.y + line.y; + const float top_y = floor(line_bottom_y) + epsilon; // top of the y step + + // either calculate the y value at top of pixel or use the end point of line + const auto x1 = (fill_current_from_top_x) ? get_x_for_y_on_line(line_point, line, top_y) : line_point.x + line.x; + + // decide whether or not to fill only to the fallback line (if outside of other line segment) + // although this is the end of the line -> we still have to test both upper and lower of the other line for large enlarged triangles and small other lines + const float test_y_other = (fill_other_from_top_x) ? top_y : line_bottom_y; + const auto x2 = get_x_with_fallback(other_point, other_line, test_y_other, is_enlarged, fallback_point_bottom, fallback_line_bottom, fallback_point_top, fallback_line_top); + + fill_between(pixel_writer, line_bottom_y, x1, x2, data_index); + } + } + } + + // draw all the steps in between + for (int y = line_point.y + 1; y < ceil(line_point.y + line.y + epsilon) - 1; y++) { + // at a distinct y step draw the current line from floor to ceil + + if (!fill) { + // draw between line start and line end of current y level + const auto x1 = get_x_for_y_on_line(line_point, line, y + epsilon); + const auto x2 = get_x_for_y_on_line(line_point, line, y + 1 - epsilon); + + fill_between(pixel_writer, y, x1, x2, data_index); + } else { + // fill to other line + const float test_y_current = (fill_current_from_top_x) ? y + epsilon : y + 1 - epsilon; + const auto x1 = get_x_for_y_on_line(line_point, line, test_y_current); + + const float test_y_other = (fill_other_from_top_x) ? y + epsilon : y + 1 - epsilon; + const auto x2 = get_x_with_fallback(other_point, other_line, test_y_other, is_enlarged, fallback_point_bottom, fallback_line_bottom, fallback_point_top, fallback_line_top); + + fill_between(pixel_writer, y, x1, x2, data_index); + } + } + } + + /* + * renders a circle at the given position + */ + template void add_circle_end_cap(const PixelWriterFunction& pixel_writer, const glm::vec2& position, unsigned int data_index, float distance) + { + distance += sqrt(0.5); + float distance_test = ceil(distance); + + for (int i = -distance_test; i < distance_test; i++) { + for (int j = -distance_test; j < distance_test; j++) { + const auto offset = glm::vec2(i + 0.0, j + 0.0); + if (glm::length(offset) < distance) + invokePixelWriter(pixel_writer, position + offset, data_index); + } + } + } + + /* + * In order to process enlarged triangles, the triangle is separated into 3 parts: top, middle and bottom + * we first generate the normals for each edge and offset the triangle origins by the normal multiplied by the supplied distance to get 6 "enlarged" points that define the bigger triangle + * + * top / bottom part: + * the top and bottom part of the triangle are rendered by going through each y step of the line with the shorter y value and filling the inner triangle until it reaches the other side of the + * triangle for the very top and very bottom of the triangle special considerations have to be done, otherwise we would render too much if the line on the other side of the triangle is not hit + * during a fill operation (= we want to fill above or below the line segment), we use the normal of the current line as the bound for the fill operation + * + * for the middle part: + * if we enlarge the triangle by moving the edge along an edge normal we generate a gap between the top-middle and middle-bottom edge (since those lines are only translated) + * we have to somehow rasterize this gap between the bottom vertice of the translated top-middle edge and the top vertice of the middle-bottom edge. + * in order to do this we draw a new edge between those vertices and rasterize everything between this new edge and the top-bottom edge. + * + * lastly we have to add endcaps on each original vertice position with the given distance + */ + template + void render_triangle(const PixelWriterFunction& pixel_writer, const std::array triangle, unsigned int triangle_index, float distance) + { + assert(triangle[0].y <= triangle[1].y); + assert(triangle[1].y <= triangle[2].y); + + auto edge_top_bottom = triangle[2] - triangle[0]; + auto edge_top_middle = triangle[1] - triangle[0]; + auto edge_middle_bottom = triangle[2] - triangle[1]; + + // by comparing the middle vertex with the middle position of the longest line, we can determine the direction of our fill algorithm + float x_middle_of_top_bottom_line = get_x_for_y_on_line(triangle[0], edge_top_bottom, triangle[1].y); + int fill_direction = (triangle[1].x < x_middle_of_top_bottom_line) ? 1 : -1; + + if (distance == 0.0) { + // top middle + render_line(pixel_writer, triangle_index, triangle[0], edge_top_middle, fill_direction, triangle[0], edge_top_bottom); + // middle bottom + render_line(pixel_writer, triangle_index, triangle[1], edge_middle_bottom, fill_direction, triangle[0], edge_top_bottom); + + return; // finished + } + + // we need to render an enlarged triangle + auto normal_top_bottom = glm::normalize(glm::vec2(-edge_top_bottom.y, edge_top_bottom.x)); + auto normal_top_middle = glm::normalize(glm::vec2(-edge_top_middle.y, edge_top_middle.x)); + auto normal_middle_bottom = glm::normalize(glm::vec2(-edge_middle_bottom.y, edge_middle_bottom.x)); + + { // swap normal direction if they are incorrect (pointing to center) + glm::vec2 centroid = (triangle[0] + triangle[1] + triangle[2]) / 3.0f; + if (glm::dot(normal_top_bottom, centroid - triangle[0]) > 0) { + normal_top_bottom *= -1; + } + if (glm::dot(normal_top_middle, centroid - triangle[0]) > 0) { + normal_top_middle *= -1; + } + if (glm::dot(normal_middle_bottom, centroid - triangle[1]) > 0) { + normal_middle_bottom *= -1; + } + } + + auto enlarged_top_bottom_origin = triangle[0] + normal_top_bottom * distance; + // auto enlarged_top_bottom_end = triangle[2] + normal_top_bottom * distance; // not needed + + auto enlarged_top_middle_origin = triangle[0] + normal_top_middle * distance; + auto enlarged_top_middle_end = triangle[1] + normal_top_middle * distance; + + auto enlarged_middle_bottom_origin = triangle[1] + normal_middle_bottom * distance; + auto enlarged_middle_bottom_end = triangle[2] + normal_middle_bottom * distance; + + // top middle + render_line(pixel_writer, + triangle_index, + enlarged_top_middle_origin, + edge_top_middle, + fill_direction, + enlarged_top_bottom_origin, + edge_top_bottom, + enlarged_middle_bottom_end, + normal_middle_bottom, + enlarged_top_middle_origin, + normal_top_middle); + + // // middle_top_part to middle_bottom_part + auto half_middle_line = (enlarged_middle_bottom_origin - enlarged_top_middle_end) / glm::vec2(2.0); + + render_line(pixel_writer, + triangle_index, + enlarged_top_middle_end, + half_middle_line, + fill_direction, + enlarged_top_bottom_origin, + edge_top_bottom, + enlarged_middle_bottom_end, + normal_middle_bottom, + enlarged_top_middle_origin, + normal_top_middle); + render_line(pixel_writer, + triangle_index, + enlarged_top_middle_end + half_middle_line, + half_middle_line, + fill_direction, + enlarged_top_bottom_origin, + edge_top_bottom, + enlarged_middle_bottom_end, + normal_middle_bottom, + enlarged_top_middle_origin, + normal_top_middle); + + // middle bottom + render_line(pixel_writer, + triangle_index, + enlarged_middle_bottom_origin, + edge_middle_bottom, + fill_direction, + enlarged_top_bottom_origin, + edge_top_bottom, + enlarged_middle_bottom_end, + normal_middle_bottom, + enlarged_top_middle_origin, + normal_top_middle); + + // endcaps + add_circle_end_cap(pixel_writer, triangle[0], triangle_index, distance); + add_circle_end_cap(pixel_writer, triangle[1], triangle_index, distance); + add_circle_end_cap(pixel_writer, triangle[2], triangle_index, distance); + + // { // DEBUG visualize enlarged points + // auto enlarged_top_bottom_end = triangle[2] + normal_top_bottom * distance; + // invokePixelWriter(pixel_writer, triangle[0], 50); + // invokePixelWriter(pixel_writer, enlarged_top_bottom_origin, 50); + // invokePixelWriter(pixel_writer, enlarged_top_middle_origin, 50); + + // invokePixelWriter(pixel_writer, triangle[1], 50); + // invokePixelWriter(pixel_writer, enlarged_middle_bottom_origin, 50); + // invokePixelWriter(pixel_writer, enlarged_top_middle_end, 50); + + // invokePixelWriter(pixel_writer, triangle[2], 50); + // invokePixelWriter(pixel_writer, enlarged_middle_bottom_end, 50); + // invokePixelWriter(pixel_writer, enlarged_top_bottom_end, 50); + // } + } + + template + void render_line_preprocess(const PixelWriterFunction& pixel_writer, const std::array line_points, unsigned int line_index, float distance) + { + // edge from top to bottom + glm::vec2 origin; + glm::vec2 edge; + if (line_points[0].y > line_points[1].y) { + edge = line_points[0] - line_points[1]; + origin = line_points[1]; + } else { + edge = line_points[1] - line_points[0]; + origin = line_points[0]; + } + + // only draw lines with 1 pixel width + if (distance == 0.0) { + render_line(pixel_writer, line_index, origin, edge); + + // debug output for points + // invokePixelWriter(pixel_writer, line_points[0], 50); + // invokePixelWriter(pixel_writer, line_points[1], 50); + + return; // finished + } + + // we want to draw lines with a thickness + + // end caps + add_circle_end_cap(pixel_writer, line_points[0], line_index, distance); + add_circle_end_cap(pixel_writer, line_points[1], line_index, distance); + + // distance += sqrt(0.5); + + // create normal + // make sure that the normal points downwards + glm::vec2 normal; + if (edge.x < 0) + normal = glm::normalize(glm::vec2(edge.y, -edge.x)) * distance; + else + normal = glm::normalize(glm::vec2(-edge.y, edge.x)) * distance; + + auto enlarged_top_origin = origin + normal * -1.0f; + auto enlarged_middle_origin = origin + normal; + auto enlarged_middle_end = origin + edge + normal * -1.0f; + // auto enlarged_bottom_origin = origin + edge + normal; + + // double the normal so that we construct a rectangle with the edge + normal *= 2.0f; + + // determine if the doubled normal or the edge has a larger y difference + // and switch if normal is larger in y direction + if (edge.y < normal.y) { + auto tmp = normal; + normal = edge; + edge = tmp; + + // we also need to change two enlarged points + tmp = enlarged_middle_origin; + enlarged_middle_origin = enlarged_middle_end; + enlarged_middle_end = tmp; + } + + // special case: one side is horizontal + // -> only one pass from top to bottom necessary + if (normal.y == 0) { + render_line(pixel_writer, line_index, enlarged_top_origin, edge, (enlarged_top_origin.x > enlarged_middle_origin.x) ? -1 : 1, enlarged_middle_origin, edge); + + return; + } + + // we have to fill once the edge side and once the normal side. + // one side should not need fallbacks to render everything + + int fill_direction = (edge.x > 0) ? -1 : 1; + + // go from top of the line to bottom of the line + // first fill everything between edge and 2xnormal, after this fill everything betweent edge and other edge + render_line(pixel_writer, line_index, enlarged_top_origin, edge, fill_direction, enlarged_middle_origin, edge, enlarged_top_origin, normal, enlarged_top_origin, normal); + + // render the last bit of the line (from line end top to line end bottom) -> no need for a fallback line since both lines meet at the bottom + render_line(pixel_writer, line_index, enlarged_middle_end, normal, fill_direction, enlarged_middle_origin, edge * 2.0f); + + // { // DEBUG visualize enlarged points + // // const auto enlarged_bottom_origin = origin + edge + normal * distance; + // invokePixelWriter(pixel_writer, enlarged_top_origin, 50); + // invokePixelWriter(pixel_writer, enlarged_middle_origin, 50); + + // invokePixelWriter(pixel_writer, enlarged_middle_end, 50); + // invokePixelWriter(pixel_writer, enlarged_bottom_origin, 50); + + // invokePixelWriter(pixel_writer, line_points[0], 50); + // invokePixelWriter(pixel_writer, line_points[1], 50); + // } + } + +} // namespace details + +/* + * generates edges of a polygon + * this assumes that polygons neighbouring in the vector should form an edge and the first and last edge are also connected + * start offset if you want to only create an edge ring list of a part of the bigger polygon you can only provide the part and an start_offset, + * that indicates by how much the vertice index has to be offset + */ +std::vector generate_neighbour_edges(std::vector polygon_points, const size_t start_offset = 0); + +/* + * triangulizes polygons and orders the vertices by y position per triangle + * output: top, middle, bottom, top, middle,... + */ +std::vector triangulize(std::vector polygon_points, std::vector edges, bool remove_duplicate_vertices = false); + +/* + * Rasterize a triangle + * in this method every triangle is traversed only once, and it only accesses pixels it needs for itself. + * this function determines the position where pixels should be set and calls the given pixel_writer lambda to actually draw the pixels + * + * NOTE: the triangle points have to be ordered correctly by y position + * -> input triangles: ordered by y position within each triangle (top_ypos_triangle_1, middle_ypos_triangle_1, bottom_ypos_triangle_1, top_ypos_triangle_2, middle_ypos_triangle_2, ...) + * + * example usage: + * const std::vector triangle_points = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5) }; + * nucleus::Raster output({ 64, 64 }, 0u); + * const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + * nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangle_points); + */ +template void rasterize_triangle(const PixelWriterFunction& pixel_writer, const std::vector& triangles, float distance = 0.0) +{ + for (size_t i = 0; i < triangles.size() / 3; ++i) { + details::render_triangle(pixel_writer, { triangles[i * 3 + 0], triangles[i * 3 + 1], triangles[i * 3 + 2] }, i, distance); + } +} + +/* + * Rasterize a line + * + * this function determines the position where pixels should be set and calls the given pixel_writer lambda to actually draw the pixels + * + * input line_points: a list of points that should form a line (line_start, line_point1, line_point2, ..., line_end) + * -> generates line segments between line_start-line_point1, line_point1 to line_point2, ... + * + * example usage: + * const std::vector line = { glm::vec2(30.5, 10.5), glm::vec2(50.5, 30.5), glm::vec2(30.5, 50.5), glm::vec2(10.5, 30.5), glm::vec2(30.5, 10.5) }; + * nucleus::Raster output({ 64, 64 }, 0u); + * const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + * nucleus::utils::rasterizer::rasterize_line(pixel_writer, line); + */ +template void rasterize_line(const PixelWriterFunction& pixel_writer, const std::vector& line_points, float distance = 0.0) +{ + for (size_t i = 0; i < line_points.size() - 1; ++i) { + details::render_line_preprocess(pixel_writer, { line_points[i + 0], line_points[i + 1] }, i, distance); + } +} + +/* + * Rasterize a polygon + * convenience function that really just calls rasterize_triangle after triangulizing the polygon + * this function determines the position where pixels should be set and calls the given pixel_writer lambda to actually draw the pixels + * + * example usage: + * const std::vector polygon_points = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5) }; + * nucleus::Raster output({ 64, 64 }, 0u); + * const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + * nucleus::utils::rasterizer::rasterize_polygon(pixel_writer, polygon_points); + */ +template void rasterize_polygon(const PixelWriterFunction& pixel_writer, const std::vector& polygon_points, float distance = 0.0) +{ + const auto edges = generate_neighbour_edges(polygon_points); + const auto triangles = triangulize(polygon_points, edges); + + rasterize_triangle(pixel_writer, triangles, distance); +} + +} // namespace nucleus::utils::rasterizer diff --git a/unittests/gl_engine/texture.cpp b/unittests/gl_engine/texture.cpp index 4180e3cf..359327c9 100644 --- a/unittests/gl_engine/texture.cpp +++ b/unittests/gl_engine/texture.cpp @@ -174,6 +174,85 @@ void test_float_texture_with(const TexelType& texel_value, gl_engine::Texture::F CHECK(qAlpha(render_result.pixel(0, 0)) == 126); } +template > +void test_unsigned_texture_array_with(const std::array& texel_value, gl_engine::Texture::Format format) +{ + Framebuffer b(Framebuffer::DepthFormat::None, { Framebuffer::ColourFormat::RGBA8, Framebuffer::ColourFormat::RGBA8 }, { 1, 1 }); + + b.bind(); + + gl_engine::Texture opengl_texture(gl_engine::Texture::Target::_2dArray, format); + opengl_texture.setParams(gl_engine::Texture::Filter::Nearest, gl_engine::Texture::Filter::Nearest); + opengl_texture.allocate_array(1, 1, 2); + + const auto tex0 = nucleus::Raster({ 1, 1 }, texel_value[0]); + const auto tex1 = nucleus::Raster({ 1, 1 }, texel_value[1]); + opengl_texture.upload(tex0, 0); + opengl_texture.upload(tex1, 1); + + const auto precision = []() -> QString { + if (sizeof(Type) == 1) + return "lowp"; + if (sizeof(Type) == 2) + return "mediump"; + if (sizeof(Type) == 4) + return "highp"; + assert(false); + return "Type has unexpected size"; + }; + + ShaderProgram shader = create_debug_shader(QString(R"( + uniform %1 usampler2DArray texture_sampler; + layout (location = 0) out lowp vec4 out_color0; + layout (location = 1) out lowp vec4 out_color1; + void main() { + %1 uvec4 v0 = texelFetch(texture_sampler, ivec3(0, 0, 0), 0); + out_color0 = vec4((v0.r == %2) ? 123.0 / 255.0 : 9.0 / 255.0, + (%10 < 2 || v0.g == %3) ? 124.0 / 255.0 : 9.0 / 255.0, + (%10 < 3 || v0.b == %4) ? 125.0 / 255.0 : 9.0 / 255.0, + (%10 < 4 || v0.a == %5) ? 126.0 / 255.0 : 9.0 / 255.0); + + %1 uvec4 v1 = texelFetch(texture_sampler, ivec3(0, 0, 1), 0); + out_color1 = vec4((v1.r == %6) ? 127.0 / 255.0 : 9.0 / 255.0, + (%10 < 2 || v1.g == %7) ? 128.0 / 255.0 : 9.0 / 255.0, + (%10 < 3 || v1.b == %8) ? 129.0 / 255.0 : 9.0 / 255.0, + (%10 < 4 || v1.a == %9) ? 130.0 / 255.0 : 9.0 / 255.0); + } + )") + .arg(precision()) + .arg(texel_component(texel_value[0], 0)) + .arg(texel_component(texel_value[0], 1)) + .arg(texel_component(texel_value[0], 2)) + .arg(texel_component(texel_value[0], 3)) + .arg(texel_component(texel_value[1], 0)) + .arg(texel_component(texel_value[1], 1)) + .arg(texel_component(texel_value[1], 2)) + .arg(texel_component(texel_value[1], 3)) + .arg(length)); + shader.bind(); + opengl_texture.bind(0); + shader.set_uniform("texture_sampler", 0); + gl_engine::helpers::create_screen_quad_geometry().draw(); + + // render_result.save("render_result.png"); + { + const QImage render_result = b.read_colour_attachment(0); + CHECK(qRed(render_result.pixel(0, 0)) == 123); + CHECK(qGreen(render_result.pixel(0, 0)) == 124); + CHECK(qBlue(render_result.pixel(0, 0)) == 125); + CHECK(qAlpha(render_result.pixel(0, 0)) == 126); + } + { + const QImage render_result = b.read_colour_attachment(1); + CHECK(qRed(render_result.pixel(0, 0)) == 127); + CHECK(qGreen(render_result.pixel(0, 0)) == 128); + CHECK(qBlue(render_result.pixel(0, 0)) == 129); + CHECK(qAlpha(render_result.pixel(0, 0)) == 130); + } + + Framebuffer::unbind(); +} + QImage create_test_rgba_qimage(unsigned width, unsigned height) { QImage test_texture(width, height, QImage::Format_RGBA8888); @@ -360,8 +439,15 @@ TEST_CASE("gl texture") SECTION("rgba8ui") { test_unsigned_texture_with<4, unsigned char>({ 1, 2, 255, 140 }, gl_engine::Texture::Format::RGBA8UI); } SECTION("rg32ui") { test_unsigned_texture_with<2, uint32_t>({ 3000111222, 4000111222 }, gl_engine::Texture::Format::RG32UI); } SECTION("red8ui") { test_unsigned_texture_with<1, uint8_t, uint8_t>(uint8_t(178), gl_engine::Texture::Format::R8UI); } + SECTION("rgb32ui") { test_unsigned_texture_with<3, uint32_t>({ 3000111222, 4000111222, 2500111222 }, gl_engine::Texture::Format::RGB32UI); } SECTION("red16ui") { test_unsigned_texture_with<1, uint16_t, uint16_t>(uint16_t(60123), gl_engine::Texture::Format::R16UI); } SECTION("red32ui") { test_unsigned_texture_with<1, uint32_t, uint32_t>(uint32_t(4000111222), gl_engine::Texture::Format::R32UI); } + SECTION("r32ui_array") { test_unsigned_texture_array_with<1, uint32_t, uint32_t>({ uint32_t { 3000111222 }, uint32_t { 3000114422 } }, gl_engine::Texture::Format::R32UI); } + SECTION("rg32ui_array") { test_unsigned_texture_array_with<2, uint32_t>({ glm::uvec2 { 3000111222, 4000111222 }, glm::uvec2 { 3000114422, 4000114422 } }, gl_engine::Texture::Format::RG32UI); } + SECTION("rgb32ui_array") + { + test_unsigned_texture_array_with<3, uint32_t>({ glm::uvec3 { 3000111222, 4000111222, 2500111222 }, glm::uvec3 { 3000114422, 4000114422, 2500114422 } }, gl_engine::Texture::Format::RGB32UI); + } SECTION("rgba32f") { test_float_texture_with<4, float, glm::vec4>(glm::vec4(2.0, 0.0, 234012.0, -239093.0), gl_engine::Texture::Format::RGBA32F); } diff --git a/unittests/nucleus/CMakeLists.txt b/unittests/nucleus/CMakeLists.txt index 717145af..0ee5ece3 100644 --- a/unittests/nucleus/CMakeLists.txt +++ b/unittests/nucleus/CMakeLists.txt @@ -25,6 +25,7 @@ alp_add_unittest(unittests_nucleus DrawListGenerator.cpp test_helpers.h test_helpers.cpp raster.cpp + rasterizer.cpp terrain_mesh_index_generator.cpp srs.cpp track.cpp @@ -69,6 +70,8 @@ qt_add_resources(unittests_nucleus "test_data" data/eaws_0-0-0.mvt data/eaws_2-2-0.mvt data/eaws_10-236-299.mvt + data/rasterizer_simple_triangle.png + data/rasterizer_output_random_triangle.png data/quad/7_68_82.jpg data/quad/7_68_83.jpg data/quad/7_69_82.jpg diff --git a/unittests/nucleus/data/rasterizer_output_random_triangle.png b/unittests/nucleus/data/rasterizer_output_random_triangle.png new file mode 100644 index 00000000..b39875a1 Binary files /dev/null and b/unittests/nucleus/data/rasterizer_output_random_triangle.png differ diff --git a/unittests/nucleus/data/rasterizer_simple_triangle.png b/unittests/nucleus/data/rasterizer_simple_triangle.png new file mode 100644 index 00000000..b9f3ce34 Binary files /dev/null and b/unittests/nucleus/data/rasterizer_simple_triangle.png differ diff --git a/unittests/nucleus/rasterizer.cpp b/unittests/nucleus/rasterizer.cpp new file mode 100644 index 00000000..46323a52 --- /dev/null +++ b/unittests/nucleus/rasterizer.cpp @@ -0,0 +1,1050 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2024 Lucas Dworschak + * Copyright (C) 2024 Adam Celarek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +// #define WRITE_RASTERIZER_DEBUG_IMAGE + +#include +#include +#include + +#include + +#include "nucleus/Raster.h" +#include "nucleus/tile/conversion.h" +#include "nucleus/utils/rasterizer.h" + +#include + +/* + * calculates how far away the given position is from a triangle + * uses distance to shift the current triangle distance + * if it is within the triangle, the pixel writer function is called + * * uses triangle distance calculation from: https://iquilezles.org/articles/distfunctions2d/ + */ +template +void triangle_sdf(const PixelWriterFunction& pixel_writer, + const glm::vec2 current_position, + const std::array points, + const std::array edges, + float s, + unsigned int data_index, + float distance) +{ + glm::vec2 v0 = current_position - points[0], v1 = current_position - points[1], v2 = current_position - points[2]; + + // pq# = distance from edge # + glm::vec2 pq0 = v0 - edges[0] * glm::clamp(glm::dot(v0, edges[0]) / glm::dot(edges[0], edges[0]), 0.0f, 1.0f); + glm::vec2 pq1 = v1 - edges[1] * glm::clamp(glm::dot(v1, edges[1]) / glm::dot(edges[1], edges[1]), 0.0f, 1.0f); + glm::vec2 pq2 = v2 - edges[2] * glm::clamp(glm::dot(v2, edges[2]) / glm::dot(edges[2], edges[2]), 0.0f, 1.0f); + // d.x == squared distance to triangle edge/vertice + // d.y == are we inside or outside triangle + glm::vec2 d = min(min(glm::vec2(dot(pq0, pq0), s * (v0.x * edges[0].y - v0.y * edges[0].x)), glm::vec2(dot(pq1, pq1), s * (v1.x * edges[1].y - v1.y * edges[1].x))), + glm::vec2(dot(pq2, pq2), s * (v2.x * edges[2].y - v2.y * edges[2].x))); + float dist_from_tri = -sqrt(d.x) * glm::sign(d.y); + + if (dist_from_tri < distance) { + nucleus::utils::rasterizer::details::invokePixelWriter(pixel_writer, current_position, data_index); + } +} + +/* + * calculates how far away the given position is from a line segment + * uses distance to shift the current triangle distance + * if it is within the triangle, the pixel writer function is called + * * uses line segment distance calculation from: https://iquilezles.org/articles/distfunctions2d/ + */ +template +void line_sdf(const PixelWriterFunction& pixel_writer, const glm::vec2 current_position, const glm::vec2 origin, glm::vec2 edge, unsigned int data_index, float distance) +{ + glm::vec2 v0 = current_position - origin; + + float dist_from_line = length(v0 - edge * glm::clamp(glm::dot(v0, edge) / glm::dot(edge, edge), 0.0f, 1.0f)); + + if (dist_from_line < distance) { + nucleus::utils::rasterizer::details::invokePixelWriter(pixel_writer, current_position, data_index); + } +} + +/* + * ideal if you want to rasterize only a few triangles, where every triangle covers a large part of the raster size + * in this method we traverse through every triangle and generate the bounding box and traverse the bounding box + */ +template void rasterize_triangle_sdf(const PixelWriterFunction& pixel_writer, const std::vector& triangles, float distance) +{ + // we sample from the center + // and have a radius of a half pixel diagonal + // NOTICE: this still leads to false positives if the edge of a triangle is slighly in another pixel without every comming in this pixel!! + distance += sqrt(0.5); + + for (size_t i = 0; i < triangles.size() / 3; ++i) { + const std::array points = { triangles[i * 3 + 0], triangles[i * 3 + 1], triangles[i * 3 + 2] }; + const std::array edges = { points[1] - points[0], points[2] - points[1], points[0] - points[2] }; + + auto min_bound = min(min(points[0], points[1]), points[2]) - glm::vec2(distance); + auto max_bound = max(max(points[0], points[1]), points[2]) + glm::vec2(distance); + + float s = glm::sign(edges[0].x * edges[2].y - edges[0].y * edges[2].x); + + for (size_t x = min_bound.x; x < max_bound.x; x++) { + for (size_t y = min_bound.y; y < max_bound.y; y++) { + auto current_position = glm::vec2 { x + 0.5, y + 0.5 }; + + triangle_sdf(pixel_writer, current_position, points, edges, s, i, distance); + } + } + } +} + +template void rasterize_line_sdf(const PixelWriterFunction& pixel_writer, const std::vector& line_points, float distance) +{ + // we sample from the center + // and have a radius of a half pixel diagonal + // NOTICE: this still leads to false positives if the edge of a triangle is slighly in another pixel without every comming in this pixel!! + distance += sqrt(0.5); + + for (size_t i = 0; i < line_points.size() - 1; ++i) { + const std::array points = { line_points[i + 0], line_points[i + 1] }; + auto edge = points[1] - points[0]; + + auto min_bound = min(points[0], points[1]) - glm::vec2(distance); + auto max_bound = max(points[0], points[1]) + glm::vec2(distance); + + for (size_t x = min_bound.x; x < max_bound.x; x++) { + for (size_t y = min_bound.y; y < max_bound.y; y++) { + auto current_position = glm::vec2 { x + 0.5, y + 0.5 }; + + line_sdf(pixel_writer, current_position, points[0], edge, i, distance); + } + } + } +} +std::pair>> triangulate(std::vector points, std::vector edges, bool remove_duplicate_vertices) +{ + + CDT::Triangulation cdt; + + if (remove_duplicate_vertices) { + CDT::RemoveDuplicatesAndRemapEdges( + points, + [](const glm::vec2& p) { return p.x; }, + [](const glm::vec2& p) { return p.y; }, + edges.begin(), + edges.end(), + [](const glm::ivec2& p) { return p.x; }, + [](const glm::ivec2& p) { return p.y; }, + [](CDT::VertInd start, CDT::VertInd end) { return glm::ivec2 { start, end }; }); + } + + cdt.insertVertices(points.begin(), points.end(), [](const glm::vec2& p) { return p.x; }, [](const glm::vec2& p) { return p.y; }); + cdt.insertEdges(edges.begin(), edges.end(), [](const glm::ivec2& p) { return p.x; }, [](const glm::ivec2& p) { return p.y; }); + cdt.eraseOuterTrianglesAndHoles(); + + return std::make_pair(cdt.triangles, cdt.vertices); +} + +QImage example_rasterizer_image(QString filename) +{ + auto image_file = QFile(QString("%1%2").arg(ALP_TEST_DATA_DIR, filename)); + image_file.open(QFile::ReadOnly); + const auto image_bytes = image_file.readAll(); + REQUIRE(!QImage::fromData(image_bytes).isNull()); + return QImage::fromData(image_bytes); +} + +TEST_CASE("nucleus/rasterizer") +{ + + // NOTE: most tests compare the dda renderer with the sdf renderer + // unfortunatly the sdf renderer can produce false positives (writing to cell that should be empty if near edge) + // those errors have all been subverted by choosing parameters that work with both renderers + // but future changes might cause problems, when there shouldn't be problems + // so if a test fails check the debug images to see what exactly went wrong. + + SECTION("Triangulation") + { + // 5 point polygon + // basically a square where one side contains an inward facing triangle + // a triangulation algorithm should be able to discern that 3 triangles are needed to construct this shape + const std::vector points = { glm::vec2(0, 0), glm::vec2(1, 1), glm::vec2(0, 2), glm::vec2(2, 2), glm::vec2(2, 0) }; + const std::vector edges = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 4), glm::ivec2(4, 0) }; + + auto pair = triangulate(points, edges, false); + + auto tri = pair.first; + auto vert = pair.second; + + // check if only 3 triangles have been found + CHECK(tri.size() == 3); + + // 1st triangle + CHECK(vert[tri[0].vertices[0]].x == 0.0); + CHECK(vert[tri[0].vertices[0]].y == 2.0); + CHECK(vert[tri[0].vertices[1]].x == 1.0); + CHECK(vert[tri[0].vertices[1]].y == 1.0); + CHECK(vert[tri[0].vertices[2]].x == 2.0); + CHECK(vert[tri[0].vertices[2]].y == 2.0); + + // 2nd triangle + CHECK(vert[tri[1].vertices[0]].x == 1.0); + CHECK(vert[tri[1].vertices[0]].y == 1.0); + CHECK(vert[tri[1].vertices[1]].x == 2.0); + CHECK(vert[tri[1].vertices[1]].y == 0.0); + CHECK(vert[tri[1].vertices[2]].x == 2.0); + CHECK(vert[tri[1].vertices[2]].y == 2.0); + + // 3rd triangle + CHECK(vert[tri[2].vertices[0]].x == 1.0); + CHECK(vert[tri[2].vertices[0]].y == 1.0); + CHECK(vert[tri[2].vertices[1]].x == 0.0); + CHECK(vert[tri[2].vertices[1]].y == 0.0); + CHECK(vert[tri[2].vertices[2]].x == 2.0); + CHECK(vert[tri[2].vertices[2]].y == 0.0); + + // DEBUG print out all the points of the triangles (to check what might have went wrong) + // for (std::size_t i = 0; i < tri.size(); i++) { + // printf("Triangle points: [[%f, %f], [%f, %f], [%f, %f]]\n", + // vert[tri[i].vertices[0]].x, // x0 + // vert[tri[i].vertices[0]].y, // y0 + // vert[tri[i].vertices[1]].x, // x1 + // vert[tri[i].vertices[1]].y, // y1 + // vert[tri[i].vertices[2]].x, // x2 + // vert[tri[i].vertices[2]].y // y2 + // ); + // } + } + + SECTION("Triangulation - duplicate vertices") + { + // 6 point polygon + // basically a square where two sides contains an inward facing triangle that meets at the same middle vertice (so esentially two triangles that mirror at top of bottom triangle) + // a triangulation algorithm should be able to discern that 3 triangles are needed to construct this shape + std::vector points = { glm::vec2(0, 0), glm::vec2(1, 1), glm::vec2(0, 2), glm::vec2(2, 2), glm::vec2(1, 1), glm::vec2(2, 0) }; + std::vector edges = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 4), glm::ivec2(4, 5), glm::ivec2(5, 0) }; + + auto pair = triangulate(points, edges, true); + + auto tri = pair.first; + auto vert = pair.second; + + // check if only 3 triangles have been found + CHECK(tri.size() == 2); + + // 1st triangle + CHECK(vert[tri[0].vertices[0]].x == 0.0); + CHECK(vert[tri[0].vertices[0]].y == 2.0); + CHECK(vert[tri[0].vertices[1]].x == 1.0); + CHECK(vert[tri[0].vertices[1]].y == 1.0); + CHECK(vert[tri[0].vertices[2]].x == 2.0); + CHECK(vert[tri[0].vertices[2]].y == 2.0); + + // 2nd triangle + CHECK(vert[tri[1].vertices[0]].x == 1.0); + CHECK(vert[tri[1].vertices[0]].y == 1.0); + CHECK(vert[tri[1].vertices[1]].x == 0.0); + CHECK(vert[tri[1].vertices[1]].y == 0.0); + CHECK(vert[tri[1].vertices[2]].x == 2.0); + CHECK(vert[tri[1].vertices[2]].y == 0.0); + + // // DEBUG print out all the points of the triangles(to check what might have went wrong) for (std::size_t i = 0; i < tri.size(); i++) + // { + // for (std::size_t i = 0; i < tri.size(); i++) { + // printf("Triangle points: [[%f, %f], [%f, %f], [%f, %f]]\n", + // vert[tri[i].vertices[0]].x, // x0 + // vert[tri[i].vertices[0]].y, // y0 + // vert[tri[i].vertices[1]].x, // x1 + // vert[tri[i].vertices[1]].y, // y1 + // vert[tri[i].vertices[2]].x, // x2 + // vert[tri[i].vertices[2]].y // y2 + // ); + // } + // } + } + + SECTION("Triangle y ordering") + { + // make sure that the triangle_order function correctly orders the triangle points from lowest y to highest y value + const std::vector triangle_points_012 = { { glm::vec2(30, 10), glm::vec2(10, 30), glm::vec2(50, 50) } }; + const std::vector triangle_points_021 = { glm::vec2(30, 10), glm::vec2(50, 50), glm::vec2(10, 30) }; + const std::vector triangle_points_102 = { glm::vec2(10, 30), glm::vec2(30, 10), glm::vec2(50, 50) }; + const std::vector triangle_points_201 = { glm::vec2(10, 30), glm::vec2(50, 50), glm::vec2(30, 10) }; + const std::vector triangle_points_120 = { glm::vec2(50, 50), glm::vec2(30, 10), glm::vec2(10, 30) }; + const std::vector triangle_points_210 = { glm::vec2(50, 50), glm::vec2(10, 30), glm::vec2(30, 10) }; + + const std::vector correct = { { glm::vec2(30, 10), glm::vec2(10, 30), glm::vec2(50, 50) } }; + + const auto edges_012 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_012); + const auto edges_021 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_021); + const auto edges_102 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_102); + const auto edges_201 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_201); + const auto edges_120 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_120); + const auto edges_210 = nucleus::utils::rasterizer::generate_neighbour_edges(triangle_points_210); + + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_012, edges_012)); + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_021, edges_021)); + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_102, edges_102)); + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_201, edges_201)); + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_120, edges_120)); + CHECK(correct == nucleus::utils::rasterizer::triangulize(triangle_points_210, edges_210)); + } + + SECTION("rasterize simple triangle - integer values") + { + // simple triangle with integer vertice values + // this tests if the visualization works with integer values + // test was added, since there was a case where the second to last row was never rendered + const std::vector polygon_points = { glm::vec2(5, 1), glm::vec2(5, 5), glm::vec2(1, 5) }; + + nucleus::Raster output({ 7, 7 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_polygon(pixel_writer, polygon_points); + + auto image = nucleus::tile::conversion::u8raster_to_qimage(output); + CHECK(image == example_rasterizer_image("rasterizer_simple_triangle.png")); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + image.save(QString("rasterizer_output_simple_triangle.png")); +#endif + } + + SECTION("rasterize Polygon") + { + const std::vector polygon_points = { + glm::vec2(10.5, 10.5), + glm::vec2(30.5, 10.5), + glm::vec2(50.5, 50.5), + glm::vec2(10.5, 30.5), + }; + + nucleus::Raster output({ 64, 64 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_polygon(pixel_writer, polygon_points); + + const auto edges = nucleus::utils::rasterizer::generate_neighbour_edges(polygon_points); + const auto triangles = nucleus::utils::rasterizer::triangulize(polygon_points, edges); + nucleus::Raster output2({ 64, 64 }, 0u); + auto pixel_writer2 = [&output2](glm::ivec2 pos) { output2.pixel(pos) = 255; }; + rasterize_triangle_sdf(pixel_writer2, triangles, 0); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_polygon.png")); +#endif + } + + SECTION("rasterize triangle") + { + // two triangles + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5), glm::vec2(5.5, 5.5), glm::vec2(15.5, 10.5), glm::vec2(5.5, 15.5) }; + + nucleus::Raster output({ 64, 64 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles); + + nucleus::Raster output2({ 64, 64 }, 0u); + auto pixel_writer2 = [&output2](glm::ivec2 pos) { output2.pixel(pos) = 255; }; + rasterize_triangle_sdf(pixel_writer2, triangles, 0); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output.png")); +#endif + } + + SECTION("rasterize triangle small") + { + const std::vector triangles_small = { glm::vec2(2, 2), glm::vec2(1, 3), glm::vec2(3.8, 3.8) }; + nucleus::Raster output({ 6, 6 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles_small); + + nucleus::Raster output2({ 6, 6 }, 0u); + const auto pixel_writer2 = [&output2](glm::ivec2 pos) { output2.pixel(pos) = 255; }; + rasterize_triangle_sdf(pixel_writer2, triangles_small, 0); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_small.png")); +#endif + } + + SECTION("rasterize triangle smallest") + { + // less than one pixel + const std::vector triangles_smallest = { glm::vec2(30.4, 30.4), glm::vec2(30.8, 30.6), glm::vec2(30.8, 30.8) }; + nucleus::Raster output({ 64, 64 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles_smallest); + + nucleus::Raster output2({ 64, 64 }, 0u); + const auto pixel_writer2 = [&output2](glm::ivec2 pos) { output2.pixel(pos) = 255; }; + rasterize_triangle_sdf(pixel_writer2, triangles_smallest, 0); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_smallest.png")); +#endif + } + + SECTION("rasterize triangle enlarged endcaps only") + { + // less than one pixel + const std::vector triangles = { glm::vec2(5.35, 5.35), glm::vec2(5.40, 5.40), glm::vec2(5.45, 5.35) }; + auto size = glm::vec2(10, 10); + nucleus::Raster output(size, 0u); + float distance = 4.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::details::add_circle_end_cap(pixel_writer, triangles[0], 1, distance); + nucleus::utils::rasterizer::details::add_circle_end_cap(pixel_writer, triangles[1], 1, distance); + nucleus::utils::rasterizer::details::add_circle_end_cap(pixel_writer, triangles[2], 1, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_triangle_sdf(pixel_writer2, triangles, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_enlarged_endcap.png")); +#endif + } + + SECTION("rasterize triangle enlarged") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), + glm::vec2(10.5, 30.5), + glm::vec2(50.5, 50.5), + + glm::vec2(5.5, 5.5), + glm::vec2(15.5, 10.5), + glm::vec2(5.5, 15.5) }; + // const std::vector triangles = { glm::vec2(5.35, 5.35), glm::vec2(5.40, 5.40), glm::vec2(5.45, 5.35) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 5.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_triangle_sdf(pixel_writer2, triangles, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_enlarged.png")); +#endif + } + + SECTION("rasterize triangle horizontal") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(45.5, 45.5), glm::vec2(10.5, 45.5) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 4.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_triangle_sdf(pixel_writer2, triangles, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_enlarged_horizontal.png")); +#endif + } + + // // can't be verified exactly, since SDF is currently slightly inaccurate + // SECTION("rasterize triangle narrow") + // { + // const std::vector triangles = { glm::vec2(6.5, 16.5), glm::vec2(55.5, 18.5), glm::vec2(6.5, 20.5), glm::vec2(55.5, 46.5), glm::vec2(6.5, 48.5), glm::vec2(55.5, 50.5) + // } + // ; + // auto size = glm::vec2(64, 64); + // nucleus::Raster output(size, 0u); + // float distance = 5.0; + // radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + // const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + // if (bounds.contains(pos)) + // output.pixel(pos) = 255; + // }; + // nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles, distance); + + // nucleus::Raster output2(size, 0u); + // const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + // if (bounds.contains(pos)) + // output2.pixel(pos) = 255; + // }; + // rasterize_triangle_sdf(pixel_writer2, triangles, distance); + + // CHECK(output.buffer() == output2.buffer()); + + // #ifdef WRITE_DEBUG_IMAGE + // // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + // auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + // image.save(QString("rasterizer_output_enlarged_narrow.png")); + // #endif + // } + + SECTION("validate small raster with large raster") + { + // Note: this test is necessary to validate the rasterizer for small rasterizations + // while larger rasterizations also encounter this problem it is more noticeable if you rasterize on a small raster and try to rasterize the actual triangle afterwards + // specific bug: we rasterized a triangle on a 16x16 raster and saw that on some pixels that should be filled, no triangle was rendered + // the problem was that we compared an integer with a float value, and for certain circumstances this caused the rasterizer to switch fill direction and render less than it should have + + // first render on the original 16x16 raster + // note orig_scale makes sure that we can easily test other scales without having to change the triangle coords + constexpr int orig_size = 16; + constexpr double orig_scale = orig_size / 16.0; + + const std::vector triangles_grid + = { glm::vec2(12.4023 * orig_scale, 0.2754 * orig_scale), glm::vec2(7.0312 * orig_scale, 10.8027 * orig_scale), glm::vec2(7.3633 * orig_scale, 11.0684 * orig_scale) }; + + nucleus::Raster output({ orig_size, orig_size }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles_grid); + + // next write the same triangle to larger raster + constexpr int enlarged_size = 2048; + nucleus::Raster output_enlarged({ enlarged_size, enlarged_size }, 0u); + + { + constexpr double enlarged_scale = enlarged_size / 16.0; + const std::vector triangles = { + glm::vec2(12.4023 * enlarged_scale, 0.2754 * enlarged_scale), glm::vec2(7.0312 * enlarged_scale, 10.8027 * enlarged_scale), glm::vec2(7.3633 * enlarged_scale, 11.0684 * enlarged_scale) + }; + + const auto pixel_writer_enlarged = [&output_enlarged](glm::ivec2 pos) { output_enlarged.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer_enlarged, triangles); + } + + // finally compare smaller with larger output -> everytime the enlarged output has a value, the smaller output should also have a value + constexpr double enlarged_to_orig = double(orig_size) / double(enlarged_size); + for (size_t i = 0; i < enlarged_size; i++) { + for (size_t j = 0; j < enlarged_size; j++) { + + const auto orig_value = output.pixel({ i * enlarged_to_orig, j * enlarged_to_orig }); + const auto enlarged_value = output_enlarged.pixel({ i, j }); + + if (enlarged_value != 0) { + CHECK(orig_value != 0); + } + + // debug output to visualize both values in one graphic at the end +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + if (orig_value != 0 && enlarged_value == 0) + output_enlarged.pixel({ i, j }) = 125; +#endif + } + } +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + auto image = nucleus::tile::conversion::u8raster_to_qimage(output_enlarged); + image.save(QString("rasterizer_problem.png")); +#endif + } + + SECTION("rasterize donut") + { + const std::vector> polygon_points = { { glm::vec2(10.5, 10.5), glm::vec2(50.5, 10.5), glm::vec2(50.5, 50.5), glm::vec2(10.5, 50.5) }, + { glm::vec2(20.5, 20.5), glm::vec2(40.5, 20.5), glm::vec2(40.5, 40.5), glm::vec2(20.5, 40.5) } }; + + const std::vector correct_edges + = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 0), glm::ivec2(4, 5), glm::ivec2(5, 6), glm::ivec2(6, 7), glm::ivec2(7, 4) }; + + std::vector vertices; + std::vector edges; + + for (size_t i = 0; i < polygon_points.size(); i++) { + auto current_edges = nucleus::utils::rasterizer::generate_neighbour_edges(polygon_points[i], vertices.size()); + edges.insert(edges.end(), current_edges.begin(), current_edges.end()); + vertices.insert(vertices.end(), polygon_points[i].begin(), polygon_points[i].end()); + } + + CHECK(edges.size() == correct_edges.size()); + + CHECK(edges == correct_edges); + + auto triangles = nucleus::utils::rasterizer::triangulize(vertices, edges); + + nucleus::Raster output({ 64, 64 }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles); + + nucleus::Raster output2({ 64, 64 }, 0u); + auto pixel_writer2 = [&output2](glm::ivec2 pos) { output2.pixel(pos) = 255; }; + rasterize_triangle_sdf(pixel_writer2, triangles, 0); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_donut.png")); +#endif + } + + SECTION("rasterize random triangles") + { + constexpr auto cells = 8u; + constexpr auto cell_size = 32u; + + nucleus::Raster output({ cells * cell_size, cells * cell_size }, 0u); + const auto pixel_writer = [&output](glm::ivec2 pos) { output.pixel(pos) = 255; }; + + const std::vector> polygon_points = { { { 11.5, 16 }, { 12.9, 3.9 }, { 4.8, 5.5 } }, + { { 59.3, 11.9 }, { 58.9, 6.9 }, { 38.5, 24.7 } }, + { { 79.1, 14.3 }, { 82, 2.4 }, { 72.7, 31.9 } }, + { { 125.2, 24.7 }, { 105.2, 30.8 }, { 125.5, 12.1 } }, + { { 145.9, 17.1 }, { 128, 15.5 }, { 141.6, 29.5 } }, + { { 160.7, 23.1 }, { 174.3, 12.4 }, { 165.1, 0.8 } }, + { { 223.9, 19.1 }, { 205.2, 0.4 }, { 217.1, 26.8 } }, + { { 252.4, 6.9 }, { 233.3, 27.4 }, { 251.3, 14.4 } }, + { { 7.1, 37.2 }, { 4, 43.7 }, { 11.1, 35.5 } }, + { { 39.9, 52.7 }, { 55.5, 48.2 }, { 45.7, 39.9 } }, + { { 82.3, 37.1 }, { 68.7, 46.5 }, { 66.5, 32.6 } }, + { { 102.9, 41.8 }, { 125.5, 34.5 }, { 112.5, 52.2 } }, + { { 155.1, 41.8 }, { 133.3, 44.9 }, { 155.3, 55.7 } }, + { { 188.9, 51.8 }, { 178.8, 53.6 }, { 172.4, 52.2 } }, + { { 220.1, 61.9 }, { 206.1, 39.5 }, { 194.6, 51.6 } }, + { { 236, 59.8 }, { 253.5, 52.9 }, { 232.7, 35.9 } }, + { { 13.8, 83.3 }, { 21.8, 79.7 }, { 23.1, 94.5 } }, + { { 40.3, 70.3 }, { 51.3, 69.4 }, { 61.1, 77.6 } }, + { { 84.7, 65.8 }, { 87.5, 77.3 }, { 84.7, 71.5 } }, + { { 101.4, 87.1 }, { 126.6, 80.8 }, { 119.7, 70.7 } }, + { { 149.7, 65.2 }, { 137.4, 67.7 }, { 135.6, 70.5 } }, + { { 185.8, 82.1 }, { 181, 72.6 }, { 167.1, 79.6 } }, + { { 213.9, 76.1 }, { 209.6, 79.5 }, { 208.3, 92.4 } }, + { { 254.3, 65.9 }, { 226.4, 88.2 }, { 255.7, 73 } }, + { { 19.3, 119.1 }, { 27.2, 120.3 }, { 18.2, 113.9 } }, + { { 38.3, 114.9 }, { 54.6, 104 }, { 46.5, 98.9 } }, + { { 85.1, 113.4 }, { 90.1, 123.5 }, { 75.1, 125.3 } }, + { { 105.5, 97.2 }, { 99.9, 116.2 }, { 99.9, 110.3 } }, + { { 131.4, 116.3 }, { 159.7, 98.2 }, { 158.4, 101.9 } }, + { { 176.2, 127.5 }, { 171, 113.7 }, { 182.8, 107.4 } }, + { { 222.3, 125.3 }, { 216.5, 125.1 }, { 192.1, 104.1 } }, + { { 249.6, 122.2 }, { 245.7, 104.4 }, { 229.7, 115.7 } }, + { { 8.1, 148 }, { 27.4, 130.4 }, { 3.9, 140.1 } }, + { { 51.6, 159.3 }, { 52.7, 135.4 }, { 45.3, 147.3 } }, + { { 70, 147.1 }, { 68.8, 144.7 }, { 92.2, 132.3 } }, + { { 116.8, 142.8 }, { 127.2, 141.7 }, { 117.9, 141.3 } }, + { { 154.7, 146.5 }, { 154.9, 130.7 }, { 137.6, 144.4 } }, + { { 183.6, 147.8 }, { 169.5, 132.9 }, { 177, 147 } }, + { { 210.4, 141.4 }, { 200, 151.8 }, { 222.4, 135.1 } }, + { { 237.5, 136.5 }, { 249.3, 151.6 }, { 251.9, 133.5 } }, + { { 20.4, 168.7 }, { 22.4, 163.9 }, { 4.4, 179.6 } }, + { { 38.1, 191.5 }, { 53.4, 191.1 }, { 34.7, 180.2 } }, + { { 72, 191 }, { 68, 186.3 }, { 77.4, 164.7 } }, + { { 101.4, 168.2 }, { 115.3, 179 }, { 122.1, 173.4 } }, + { { 137.2, 177.7 }, { 137.3, 186.8 }, { 128.4, 181.7 } }, + { { 190.4, 184.5 }, { 188.4, 180.8 }, { 168.4, 180.8 } }, + { { 199.6, 160.9 }, { 211.2, 161.7 }, { 213.9, 189 } }, + { { 252.1, 182 }, { 240.3, 189.9 }, { 245.9, 179.3 } }, + { { 14.7, 208.9 }, { 23.1, 219.3 }, { 27.9, 194 } }, + { { 51.7, 196.5 }, { 63.3, 197.1 }, { 58.8, 208.3 } }, + { { 92, 223.7 }, { 84.5, 204.4 }, { 65.3, 216.4 } }, + { { 108.5, 208.1 }, { 113.8, 200.9 }, { 121.1, 223.8 } }, + { { 137, 199.7 }, { 152.9, 213.2 }, { 155.7, 204.5 } }, + { { 189.5, 206.8 }, { 189.3, 202.5 }, { 172.5, 212.6 } }, + { { 217.1, 217.2 }, { 209.6, 211.5 }, { 215, 203.7 } }, + { { 235.4, 223.6 }, { 236.1, 211 }, { 254.6, 192 } }, + { { 3.3, 224.6 }, { 28.7, 254.4 }, { 17.4, 245.2 } }, + { { 48.1, 245.8 }, { 62.3, 250.4 }, { 38.9, 233 } }, + { { 75, 250 }, { 87.7, 247.6 }, { 95.5, 232.3 } }, + { { 116.8, 247.5 }, { 107, 235.8 }, { 119.5, 230.4 } }, + { { 153.2, 245.2 }, { 136.2, 246.2 }, { 137.4, 229.3 } }, + { { 185.1, 250 }, { 184.4, 236.7 }, { 161.9, 245.8 } }, + { { 222.9, 233.8 }, { 215.4, 242.1 }, { 206.3, 253.2 } }, + { { 234.4, 241.6 }, { 233.2, 249.4 }, { 225.7, 245.4 } } }; + + for (const auto& tri : polygon_points) { + nucleus::utils::rasterizer::rasterize_polygon(pixel_writer, tri); + + // DEBUG view the vertices in the image(uses different pixel intensity) + // const auto pixel_writer_points = [&output](glm::ivec2 pos) { output.pixel(pos) = 125; }; + // pixel_writer_points(tri[0]); + // pixel_writer_points(tri[1]); + // pixel_writer_points(tri[2]); + } + + // DEBUG how the random points were generated + // const auto rand_pos = []() { return std::rand() % (cell_size * 10u) / 10.0f; }; + // std::srand(123458); // initialize rand -> we need to create consistent triangles + + // for (size_t i = 0; i < cells; i++) { + // for (size_t j = 0; j < cells; j++) { + // const auto cell_offset = glm::vec2(j * cell_size, i * cell_size); + + // const std::vector polygon_points + // = { glm::vec2(rand_pos(), rand_pos()) + cell_offset, glm::vec2(rand_pos(), rand_pos()) + cell_offset, glm::vec2(rand_pos(), rand_pos()) + cell_offset }; + + // nucleus::utils::rasterizer::rasterize_polygon(pixel_writer, polygon_points); + + // qDebug() << "{{" << polygon_points[0].x << "," << polygon_points[0].y << "}, " << "{" << polygon_points[1].x << "," << polygon_points[1].y << "}, " << "{" << polygon_points[2].x << + // "," + // << polygon_points[2].y << "}}, "; + // } + // } + + auto image = nucleus::tile::conversion::u8raster_to_qimage(output); + CHECK(image == example_rasterizer_image("rasterizer_output_random_triangle.png")); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + image.save(QString("rasterizer_output_random_triangle.png")); +#endif + } + + SECTION("rasterize triangle start/end y") + { + const std::vector triangles = { // down to right + glm::vec2(2.7, 1.5), + glm::vec2(3.7, 2.5), + glm::vec2(3.7, 2.7), + // down to left + glm::vec2(8.7, 1.5), + glm::vec2(7.7, 2.5), + glm::vec2(7.7, 2.7), + + // 1 lines + // down to right + glm::vec2(3.5, 7.3), + glm::vec2(26.5, 7.5), + glm::vec2(24.5, 7.6), + // down to left + glm::vec2(26.5, 12.3), + glm::vec2(3.5, 12.5), + glm::vec2(5.5, 12.6), + + // 2 lines + // down to right + glm::vec2(3.5, 16.7), + glm::vec2(6.5, 17.5), + glm::vec2(4.5, 17.6), + // down to left + glm::vec2(6.5, 21.7), + glm::vec2(3.5, 22.5), + glm::vec2(5.5, 22.6) + }; + + auto size = glm::vec2(30, 30); + nucleus::Raster output(size, 0u); + float distance = 0.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_triangle_sdf(pixel_writer2, triangles, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_triangle_start_end.png")); +#endif + } + + SECTION("rasterize line start/end y") + { + const std::vector line_lower_min = { glm::vec2(2.7, 2.5), glm::vec2(3.7, 1.5) }; + const std::vector line_upper_min = { glm::vec2(7.3, 2.5), glm::vec2(8.5, 1.3) }; + const std::vector line_left_to_right_1pixel = { glm::vec2(2.3, 7.3), glm::vec2(28.5, 7.7) }; + const std::vector line_right_to_left_1pixel = { glm::vec2(2.3, 12.7), glm::vec2(28.5, 12.3) }; + const std::vector line_left_to_right_2pixel = { glm::vec2(2.3, 17.3), glm::vec2(6.5, 18.7) }; + const std::vector line_right_to_left_2pixel = { glm::vec2(2.3, 23.7), glm::vec2(6.5, 22.3) }; + + auto size = glm::vec2(30, 30); + nucleus::Raster output(size, 0u); + float distance = 0.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_lower_min, distance); + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_upper_min, distance); + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_left_to_right_1pixel, distance); + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_right_to_left_1pixel, distance); + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_left_to_right_2pixel, distance); + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line_right_to_left_2pixel, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_line_sdf(pixel_writer2, line_lower_min, distance); + rasterize_line_sdf(pixel_writer2, line_upper_min, distance); + rasterize_line_sdf(pixel_writer2, line_left_to_right_1pixel, distance); + rasterize_line_sdf(pixel_writer2, line_right_to_left_1pixel, distance); + rasterize_line_sdf(pixel_writer2, line_left_to_right_2pixel, distance); + rasterize_line_sdf(pixel_writer2, line_right_to_left_2pixel, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_line_start_end.png")); +#endif + } + + SECTION("rasterize line straight") + { + const std::vector line = { glm::vec2(10.5, 10.5), glm::vec2(10.5, 50.5), glm::vec2(50.5, 50.5), glm::vec2(50.5, 10.5), glm::vec2(10.5, 10.5) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 0.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_line_sdf(pixel_writer2, line, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_line_straight.png")); +#endif + } + + SECTION("rasterize line diagonal") + { + const std::vector line = { glm::vec2(30.5, 10.5), glm::vec2(50.5, 30.5), glm::vec2(30.5, 50.5), glm::vec2(10.5, 30.5), glm::vec2(30.5, 10.5) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 0.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_line_sdf(pixel_writer2, line, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_line_diagonal.png")); +#endif + } + + SECTION("rasterize line straight enlarged") + { + const std::vector line = { glm::vec2(10.5, 10.5), glm::vec2(10.5, 50.5), glm::vec2(50.5, 50.5), glm::vec2(50.5, 10.5), glm::vec2(10.5, 10.5) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 2.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_line_sdf(pixel_writer2, line, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_line_straight_enlarged.png")); +#endif + } + + SECTION("rasterize line diagonal enlarged") + { + const std::vector line = { glm::vec2(30.5, 10.5), glm::vec2(50.5, 30.5), glm::vec2(30.5, 50.5), glm::vec2(10.5, 30.5), glm::vec2(30.5, 10.5) }; + // const std::vector line = { glm::vec2(30.5, 10.5), glm::vec2(50.5, 30.5) }; + auto size = glm::vec2(64, 64); + nucleus::Raster output(size, 0u); + float distance = 2.0; + radix::geometry::Aabb2 bounds = { { 0, 0 }, size }; + + const auto pixel_writer = [&output, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output.pixel(pos) = 255; + }; + nucleus::utils::rasterizer::rasterize_line(pixel_writer, line, distance); + + nucleus::Raster output2(size, 0u); + const auto pixel_writer2 = [&output2, bounds](glm::ivec2 pos) { + if (bounds.contains(pos)) + output2.pixel(pos) = 255; + }; + rasterize_line_sdf(pixel_writer2, line, distance); + + CHECK(output.buffer() == output2.buffer()); + +#ifdef WRITE_RASTERIZER_DEBUG_IMAGE + // DEBUG: save image (image saved to build/Desktop-Profile/unittests/nucleus) + auto image = nucleus::tile::conversion::u8raster_2_to_qimage(output, output2); + image.save(QString("rasterizer_output_line_diagonal_enlarged.png")); +#endif + } +} +TEST_CASE("nucleus/utils/rasterizer benchmarks") +{ + + BENCHMARK("triangulize polygons") + { + const std::vector polygon_points = { glm::vec2(10.5, 10.5), glm::vec2(30.5, 10.5), glm::vec2(50.5, 50.5), glm::vec2(10.5, 30.5) }; + const auto edges = nucleus::utils::rasterizer::generate_neighbour_edges(polygon_points); + nucleus::utils::rasterizer::triangulize(polygon_points, edges); + }; + + BENCHMARK("triangulize polygons 2") + { + std::vector points = { glm::vec2(0, 0), glm::vec2(1, 1), glm::vec2(0, 2), glm::vec2(2, 2), glm::vec2(2, 0) }; + std::vector edges = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 4), glm::ivec2(4, 0) }; + + nucleus::utils::rasterizer::triangulize(points, edges); + }; + + BENCHMARK("triangulize polygons + remove duplicates (no duplicates)") + { + std::vector points = { glm::vec2(0, 0), glm::vec2(1, 1), glm::vec2(0, 2), glm::vec2(2, 2), glm::vec2(2, 0) }; + std::vector edges = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 4), glm::ivec2(4, 0) }; + + nucleus::utils::rasterizer::triangulize(points, edges, true); + }; + BENCHMARK("triangulize polygons + remove duplicates (with duplicates)") + { + std::vector points = { glm::vec2(0, 0), glm::vec2(1, 1), glm::vec2(0, 2), glm::vec2(2, 2), glm::vec2(1, 1), glm::vec2(2, 0) }; + std::vector edges = { glm::ivec2(0, 1), glm::ivec2(1, 2), glm::ivec2(2, 3), glm::ivec2(3, 4), glm::ivec2(4, 5), glm::ivec2(5, 0) }; + + nucleus::utils::rasterizer::triangulize(points, edges, true); + }; + + BENCHMARK("Rasterize triangle") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5), glm::vec2(5.5, 5.5), glm::vec2(15.5, 10.5), glm::vec2(5.5, 15.5) }; + + // we dont particular care how long it takes to write to raster -> so do nothing + const auto pixel_writer = [](glm::ivec2) { /*do nothing*/ }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles); + }; + + BENCHMARK("Rasterize triangle distance") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5), glm::vec2(5.5, 5.5), glm::vec2(15.5, 10.5), glm::vec2(5.5, 15.5) }; + + // we dont particular care how long it takes to write to raster -> so do nothing + const auto pixel_writer = [](glm::ivec2) { /*do nothing*/ }; + nucleus::utils::rasterizer::rasterize_triangle(pixel_writer, triangles, 5.0); + }; + + BENCHMARK("Rasterize triangle SDF") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5), glm::vec2(5.5, 5.5), glm::vec2(15.5, 10.5), glm::vec2(5.5, 15.5) }; + + // we dont particular care how long it takes to write to raster -> so do nothing + const auto pixel_writer = [](glm::ivec2) { /*do nothing*/ }; + rasterize_triangle_sdf(pixel_writer, triangles, 0); + }; + + BENCHMARK("Rasterize triangle SDF distance") + { + const std::vector triangles = { glm::vec2(30.5, 10.5), glm::vec2(10.5, 30.5), glm::vec2(50.5, 50.5), glm::vec2(5.5, 5.5), glm::vec2(15.5, 10.5), glm::vec2(5.5, 15.5) }; + + // we dont particular care how long it takes to write to raster -> so do nothing + const auto pixel_writer = [](glm::ivec2) { /*do nothing*/ }; + rasterize_triangle_sdf(pixel_writer, triangles, 5.0); + }; +}