├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── src ├── IO.cpp ├── IO.h ├── tridexel.cpp └── tridexel.h └── test ├── main.cpp └── sphere.cpp /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | 3 | project(tridexel) 4 | 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | # find packages 9 | find_package(Boost REQUIRED COMPONENTS system filesystem) 10 | include_directories(${Boost_INCLUDE_DIRS}) 11 | 12 | find_package(glm CONFIG REQUIRED) 13 | find_package(GTest MODULE REQUIRED) 14 | 15 | # library 16 | file(GLOB source_files src/*) 17 | add_library(${PROJECT_NAME} ${source_files}) 18 | source_group("" FILES ${source_files}) 19 | source_group(TREE ${CMAKE_CURRENT_LIST_DIR} FILES ${source_files}) 20 | 21 | target_link_libraries(${PROJECT_NAME} PRIVATE 22 | ${Boost_LIBRARIES} 23 | glm 24 | ) 25 | 26 | if(MSVC) 27 | target_compile_options(${PROJECT_NAME} PRIVATE /W4) 28 | else() 29 | target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra) 30 | endif() 31 | 32 | # tests 33 | file(GLOB test_files test/*) 34 | add_executable(tests ${test_files}) 35 | source_group("" FILES ${test_files}) 36 | source_group(TREE ${CMAKE_CURRENT_LIST_DIR} FILES ${test_files}) 37 | 38 | target_link_libraries(tests PRIVATE 39 | ${PROJECT_NAME} 40 | GTest::GTest GTest::Main 41 | ${Boost_LIBRARIES} 42 | glm 43 | ) 44 | 45 | # clang-format 46 | find_program(CLANG_FORMAT NAMES "clang-format") 47 | if(NOT CLANG_FORMAT) 48 | message("clang-format not found") 49 | else() 50 | message("clang-format found: ${CLANG_FORMAT}") 51 | add_custom_target(clang-format COMMAND ${CLANG_FORMAT} -style=file -i ${source_files}) 52 | endif() 53 | 54 | # clang-tidy 55 | find_program(CLANG_TIDY NAMES "clang-tidy") 56 | if(NOT CLANG_TIDY) 57 | message("clang-tidy not found") 58 | else() 59 | message("clang-tidy found: ${CLANG_TIDY}") 60 | set_target_properties(${PROJECT_NAME} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY}") 61 | endif() 62 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tri-dexel 2 | 3 | This is an exemplary implementation of a tri-dexel approach for creating triangle meshes from surfaces which can be sampled using raycasting. 4 | The implementation is intentionally kept simple and is based on: 5 | 6 | - *Surface reconstruction from models for subtractive manufacturing simulation* by Bernhard Manfred Gruber (https://github.com/bernhardmgruber/master_thesis) 7 | - *Feature Conservation and Conversion of Tri-dexel Volumetric Models to Polyhedral Surface Models for Product Prototyping* by Yongfu Ren, Weihang Zhu and Yuan-Shin Lee. 8 | -------------------------------------------------------------------------------- /src/IO.cpp: -------------------------------------------------------------------------------- 1 | #include "IO.h" 2 | 3 | #include 4 | 5 | namespace { 6 | auto isFinite(glm::vec3 v) -> bool { 7 | return std::isfinite(v.x) || std::isfinite(v.y) || std::isfinite(v.z); 8 | } 9 | 10 | auto isFinite(const Triangle& t) -> bool { 11 | return isFinite(t[0]) || isFinite(t[1]) || isFinite(t[2]); 12 | } 13 | } 14 | 15 | void saveTriangles(std::filesystem::path path, const std::vector& triangles) { 16 | if (path.has_parent_path()) 17 | create_directories(path.parent_path()); 18 | std::ofstream f{path, std::ios::binary}; 19 | const char header[80] = "STL whatever"; 20 | f.write(header, sizeof(header)); 21 | 22 | uint32_t count = static_cast(triangles.size()); 23 | f.write(reinterpret_cast(&count), sizeof(count)); 24 | 25 | const uint16_t attributeCount = 0; 26 | for (const auto& t : triangles) { 27 | const auto normal = glm::normalize(glm::cross(t[0] - t[1], t[0] - t[2])); 28 | if (!isFinite(t)) { 29 | count--; 30 | continue; 31 | } 32 | f.write(reinterpret_cast(&normal), sizeof(normal)); 33 | f.write(reinterpret_cast(&t), sizeof(t)); 34 | f.write(reinterpret_cast(&attributeCount), sizeof(attributeCount)); 35 | } 36 | 37 | f.seekp(sizeof(header), std::ios::beg); 38 | f.write(reinterpret_cast(&count), sizeof(count)); 39 | 40 | f.close(); 41 | } 42 | 43 | void savePoints(std::filesystem::path path, const std::vector& points) { 44 | if (path.has_parent_path()) 45 | create_directories(path.parent_path()); 46 | std::ofstream f{path, std::ios::binary}; 47 | f << "ply\n"; 48 | f << "format binary_little_endian 1.0\n"; 49 | f << "element vertex " << points.size() << "\n"; 50 | f << "property float x\n"; 51 | f << "property float y\n"; 52 | f << "property float z\n"; 53 | f << "end_header\n"; 54 | f.write(reinterpret_cast(points.data()), points.size() * sizeof(glm::vec3)); 55 | f.close(); 56 | } 57 | -------------------------------------------------------------------------------- /src/IO.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "../src/tridexel.h" 6 | 7 | void saveTriangles(std::filesystem::path path, const std::vector& triangles); 8 | void savePoints(std::filesystem::path path, const std::vector& points); 9 | -------------------------------------------------------------------------------- /src/tridexel.cpp: -------------------------------------------------------------------------------- 1 | #include "tridexel.h" 2 | 3 | #include "IO.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace { 10 | using glm::vec3; 11 | using glm::uvec3; 12 | 13 | struct Plane { 14 | Plane(vec3 n, float d) : n(n), d(d) {} 15 | Plane(vec3 n, vec3 p) : n(n), d(glm::dot(n, p)) {} 16 | 17 | vec3 n; 18 | float d; 19 | }; 20 | 21 | struct Node { 22 | float depth; 23 | vec3 normal; 24 | }; 25 | 26 | struct Segment { 27 | Node start; 28 | Node end; 29 | }; 30 | 31 | struct Dexel { 32 | void regularize() { 33 | // TODO: improve this 34 | if (nodes.size() % 2 != 0) 35 | nodes.clear(); 36 | } 37 | 38 | auto segments() const -> std::vector { 39 | assert(nodes.size() % 2 == 0); 40 | std::vector segs; 41 | segs.reserve(nodes.size() / 2); 42 | for (size_t i = 0; i < nodes.size(); i += 2) 43 | segs.push_back({nodes[i], nodes[i + 1]}); 44 | return segs; 45 | } 46 | 47 | std::vector nodes; 48 | }; 49 | 50 | struct DexelImage { 51 | unsigned int axis0; 52 | unsigned int axis1Res; 53 | unsigned int axis2Res; 54 | std::vector dexels; 55 | 56 | auto dexel(unsigned int axis1, unsigned int axis2) -> Dexel & { 57 | return dexels[axis1Res * axis1 + axis2]; 58 | } 59 | 60 | auto dexel(unsigned int axis1, unsigned int axis2) const -> const Dexel & { 61 | return dexels[axis1Res * axis1 + axis2]; 62 | } 63 | }; 64 | 65 | class TriDexelImage { 66 | public: 67 | TriDexelImage(uvec3 resolution) 68 | : resolution(resolution) { 69 | for (auto axis0 : {0, 1, 2}) { 70 | const auto axis1 = (axis0 + 1) % 3; 71 | const auto axis2 = (axis0 + 2) % 3; 72 | images[axis0].axis0 = axis0; 73 | images[axis0].axis1Res = resolution[axis1]; 74 | images[axis0].axis2Res = resolution[axis2]; 75 | images[axis0].dexels.resize(resolution[axis1] * resolution[axis2]); 76 | } 77 | } 78 | 79 | std::array images; 80 | uvec3 resolution; 81 | }; 82 | 83 | struct boool { 84 | bool value; 85 | 86 | auto operator=(bool b) -> boool & { 87 | value = b; 88 | return *this; 89 | } 90 | 91 | operator bool() const { 92 | return value; 93 | } 94 | }; 95 | 96 | struct Cell { 97 | BoundingBox box; 98 | std::array occupancies; 99 | std::array, 12> edges; 100 | 101 | auto realPoint(unsigned int i) const -> vec3 { 102 | const auto& u = box.upper; 103 | const auto& l = box.lower; 104 | switch (i) { 105 | case 0: return {l.x, l.y, l.z}; 106 | case 1: return {u.x, l.y, l.z}; 107 | case 2: return {l.x, u.y, l.z}; 108 | case 3: return {u.x, u.y, l.z}; 109 | case 4: return {l.x, l.y, u.z}; 110 | case 5: return {u.x, l.y, u.z}; 111 | case 6: return {l.x, u.y, u.z}; 112 | case 7: return {u.x, u.y, u.z}; 113 | default: std::terminate(); 114 | } 115 | } 116 | 117 | auto empty() const { 118 | return std::all_of(begin(occupancies), end(occupancies), [](bool o) { return o == false; }); 119 | } 120 | }; 121 | 122 | constexpr auto edgeIndexOffsets = std::array{ { 123 | {0, 0, 0}, 124 | {0, 1, 0}, 125 | {0, 0, 1}, 126 | {0, 1, 1}, 127 | {0, 0, 0}, 128 | {1, 0, 0}, 129 | {0, 0, 1}, 130 | {1, 0, 1}, 131 | {0, 0, 0}, 132 | {1, 0, 0}, 133 | {0, 1, 0}, 134 | {1, 1, 0} 135 | }}; 136 | 137 | auto gridToDepth(unsigned int coord, unsigned int axis, uvec3 res, BoundingBox box) -> float { 138 | return (coord / (float)(res[axis] - 1)) * (box.upper[axis] - box.lower[axis]) + box.lower[axis]; 139 | } 140 | 141 | auto depthToGrid(float depth, unsigned int axis, uvec3 res, BoundingBox box) -> unsigned { 142 | return static_cast((depth - box.lower[axis]) / (box.upper[axis] - box.lower[axis]) * (res[axis] - 1)); 143 | } 144 | 145 | constexpr std::array, 12> edgeToPointIds = {{ 146 | {{0, 1}}, 147 | {{2, 3}}, 148 | {{4, 5}}, 149 | {{6, 7}}, 150 | {{0, 2}}, 151 | {{1, 3}}, 152 | {{4, 6}}, 153 | {{5, 7}}, 154 | {{0, 4}}, 155 | {{1, 5}}, 156 | {{2, 6}}, 157 | {{3, 7}}, 158 | }}; 159 | 160 | constexpr auto edgeToAxis = []() constexpr { 161 | std::array, 8> t{}; 162 | t[0][1] = t[1][0] = 0; 163 | t[2][3] = t[3][2] = 0; 164 | t[4][5] = t[5][4] = 0; 165 | t[6][7] = t[7][6] = 0; 166 | t[0][2] = t[2][0] = 1; 167 | t[1][3] = t[3][1] = 1; 168 | t[4][6] = t[6][4] = 1; 169 | t[5][7] = t[7][5] = 1; 170 | t[0][4] = t[4][0] = 2; 171 | t[1][5] = t[5][1] = 2; 172 | t[2][6] = t[6][2] = 2; 173 | t[3][7] = t[7][3] = 2; 174 | return t; 175 | }(); 176 | 177 | auto clampSegments(std::vector segments, float min, float max) { 178 | for (auto& seg : segments) { 179 | seg.start.depth = std::clamp(seg.start.depth, min, max); 180 | seg.end.depth = std::clamp(seg.end.depth, min, max); 181 | } 182 | 183 | segments.erase(std::remove_if(begin(segments), end(segments), [](const Segment& seg) { 184 | return seg.start.depth == seg.end.depth; 185 | }), end(segments)); 186 | 187 | return segments; 188 | } 189 | 190 | class TriDexelGrid { 191 | public: 192 | TriDexelGrid(TriDexelImage& image, BoundingBox box) 193 | : image(image), box(box) { 194 | const auto r = image.resolution + 1u; 195 | occupancies.resize(r.x * r.y * r.z); 196 | } 197 | 198 | auto& occupancy(uvec3 index) { 199 | const auto& r = image.resolution; 200 | return occupancies[index.z * (r.x * r.y) + index.y * r.x + index.x]; 201 | } 202 | 203 | auto cell(uvec3 index) -> Cell { 204 | Cell c; 205 | for (auto axis : {0, 1, 2}) { 206 | c.box.lower[axis] = gridToDepth(index[axis] + 0, axis, image.resolution, box); 207 | c.box.upper[axis] = gridToDepth(index[axis] + 1, axis, image.resolution, box); 208 | } 209 | 210 | c.occupancies[0] = occupancy(index + uvec3{0, 0, 0}); 211 | c.occupancies[1] = occupancy(index + uvec3{1, 0, 0}); 212 | c.occupancies[2] = occupancy(index + uvec3{0, 1, 0}); 213 | c.occupancies[3] = occupancy(index + uvec3{1, 1, 0}); 214 | c.occupancies[4] = occupancy(index + uvec3{0, 0, 1}); 215 | c.occupancies[5] = occupancy(index + uvec3{1, 0, 1}); 216 | c.occupancies[6] = occupancy(index + uvec3{0, 1, 1}); 217 | c.occupancies[7] = occupancy(index + uvec3{1, 1, 1}); 218 | 219 | for (auto i = 0; i < 12; i++) { 220 | const auto [src, dst] = edgeToPointIds[i]; 221 | const auto axis0 = edgeToAxis[src][dst]; 222 | const auto axis1 = (axis0 + 1) % 3; 223 | const auto axis2 = (axis0 + 2) % 3; 224 | const auto edgeIndex = index + edgeIndexOffsets[i]; 225 | c.edges[i] = clampSegments(image.images[axis0].dexel(edgeIndex[axis1], edgeIndex[axis2]).segments(), c.box.lower[axis0], c.box.upper[axis0]); 226 | } 227 | 228 | return c; 229 | } 230 | 231 | TriDexelImage& image; 232 | BoundingBox box; 233 | 234 | std::vector occupancies; 235 | }; 236 | 237 | auto uniformResolution(BoundingBox box, unsigned int resolution) { 238 | const auto boxSizes = box.upper - box.lower; 239 | const auto largest = std::max({boxSizes.x, boxSizes.y, boxSizes.z}); 240 | const auto cellSize = largest / resolution; 241 | return uvec3{glm::round(boxSizes / cellSize)}; 242 | } 243 | 244 | auto createRay(BoundingBox box, unsigned int axis0, unsigned int axis1, unsigned int axis2, float deltaX, float deltaY, unsigned int x, unsigned int y) { 245 | auto origin = box.lower; 246 | origin[axis1] += x * deltaX; 247 | origin[axis2] += y * deltaY; 248 | auto direction = vec3{}; 249 | direction[axis0] = 1; 250 | return Ray{origin, direction}; 251 | } 252 | 253 | template 254 | void axisParallelRaycast(BoundingBox box, uvec3 res, RaycastCallback rcc, HitFunc hitFunc) { 255 | for (auto axis0 : {0, 1, 2}) { 256 | const auto axis1 = (axis0 + 1) % 3; 257 | const auto axis2 = (axis0 + 2) % 3; 258 | const auto xCount = res[axis1]; 259 | const auto yCount = res[axis2]; 260 | const auto deltaX = (box.upper[axis1] - box.lower[axis1]) / (xCount - 1); 261 | const auto deltaY = (box.upper[axis2] - box.lower[axis2]) / (yCount - 1); 262 | for (auto y = 0u; y < yCount; y++) { 263 | for (auto x = 0u; x < xCount; x++) { 264 | const auto ray = createRay(box, axis0, axis1, axis2, deltaX, deltaY, x, y); 265 | rcc(ray, [&](float depth, vec3 normal) { 266 | hitFunc(axis0, x, y, depth, normal); 267 | }); 268 | } 269 | } 270 | } 271 | } 272 | 273 | void assignOccupancies(TriDexelGrid& grid) { 274 | for (auto& image : grid.image.images) { 275 | const auto axis0 = image.axis0; 276 | const auto axis1 = (axis0 + 1) % 3; 277 | const auto axis2 = (axis0 + 2) % 3; 278 | for (auto axis2Val = 0u; axis2Val < image.axis2Res; axis2Val++) { 279 | for (auto axis1Val = 0u; axis1Val < image.axis1Res; axis1Val++) { 280 | auto& dexel = image.dexels[axis2Val * image.axis1Res + axis1Val]; 281 | dexel.regularize(); 282 | for (const auto& seg : dexel.segments()) { 283 | const auto start = std::max(0u, depthToGrid(seg.start.depth, image.axis0, grid.image.resolution, grid.box)); 284 | const auto end = std::min(grid.image.resolution[axis0] - 1, depthToGrid(seg.end.depth, image.axis0, grid.image.resolution, grid.box) + 1); 285 | for (auto axis0Val = start; axis0Val <= end; axis0Val++) { 286 | const auto exactAxis0Depth = gridToDepth(axis0Val, axis0, grid.image.resolution, grid.box); 287 | if (seg.start.depth > exactAxis0Depth || seg.end.depth < exactAxis0Depth) 288 | continue; 289 | uvec3 index; 290 | index[axis0] = axis0Val; 291 | index[axis1] = axis1Val; 292 | index[axis2] = axis2Val; 293 | grid.occupancy(index) = true; 294 | } 295 | } 296 | } 297 | } 298 | } 299 | } 300 | 301 | constexpr auto rho = 1e-1f; 302 | 303 | void regularizeCell(Cell& cell) { 304 | for (auto i = 0; i < 12; i++) { 305 | auto& segs = cell.edges[i]; 306 | const auto [src, dst] = edgeToPointIds[i]; 307 | const auto axis = i / 4; 308 | const auto srcDepth = cell.box.lower[axis]; 309 | const auto dstDepth = cell.box.upper[axis]; 310 | const auto srcOcc = cell.occupancies[src]; 311 | const auto dstOcc = cell.occupancies[dst]; 312 | 313 | // rule 1 314 | if (srcOcc && dstOcc) { 315 | segs.clear(); 316 | segs.push_back({{srcDepth, {}}, {dstDepth, {}}}); 317 | } 318 | 319 | // rule 2 320 | if (!srcOcc && !dstOcc) { 321 | segs.clear(); 322 | } 323 | 324 | // rule 3 325 | glm::vec3 n{0, 0, 0}; 326 | n[axis] = 1; 327 | if (srcOcc && !dstOcc) { 328 | if (segs.size() == 0 || segs.front().start.depth - rho > srcDepth) 329 | segs.push_back({{srcDepth, -n}, {srcDepth + rho, n}}); 330 | else if (segs.size() > 0 && segs.front().start.depth > srcDepth && segs.front().start.depth - rho <= srcDepth) 331 | segs.front().start.depth = srcDepth; 332 | } 333 | if (!srcOcc && dstOcc) { 334 | if (segs.size() == 0 || segs.back().end.depth + rho < dstDepth) 335 | segs.push_back({{dstDepth - rho, -n}, {dstDepth, n}}); 336 | else if (segs.size() > 0 && segs.back().end.depth < dstDepth && segs.back().end.depth + rho >= dstDepth) 337 | segs.back().end.depth = dstDepth; 338 | } 339 | 340 | // rule 4 341 | segs.erase(std::remove_if(begin(segs), end(segs), [&](const Segment& s) { 342 | return s.start.depth != srcDepth && s.end.depth != dstDepth; 343 | }), end(segs)); 344 | } 345 | } 346 | 347 | auto occupancyToCase(const std::array & occupancies) -> unsigned int { 348 | unsigned int c = 0; 349 | for (auto i = 0u; i < 8; i++) 350 | if (occupancies[i]) 351 | c |= 1u << i; 352 | return c; 353 | } 354 | 355 | auto isProblematicCase(const Cell& cell) { 356 | switch (occupancyToCase(cell.occupancies)) { 357 | case 24: 358 | case 36: 359 | case 66: 360 | case 129: 361 | return true; 362 | default: 363 | return false; 364 | } 365 | } 366 | 367 | struct EdgeVertex { 368 | unsigned int point; 369 | unsigned int neighbor; 370 | vec3 position; 371 | vec3 normal; 372 | }; 373 | 374 | const auto pointsToEdgeId = []() { 375 | std::array, 8> t{}; 376 | for (auto& tt : t) 377 | tt.fill(std::numeric_limits::max()); 378 | t[0][1] = t[1][0] = 0; 379 | t[2][3] = t[3][2] = 1; 380 | t[4][5] = t[5][4] = 2; 381 | t[6][7] = t[7][6] = 3; 382 | t[0][2] = t[2][0] = 4; 383 | t[1][3] = t[3][1] = 5; 384 | t[4][6] = t[6][4] = 6; 385 | t[5][7] = t[7][5] = 7; 386 | t[0][4] = t[4][0] = 8; 387 | t[1][5] = t[5][1] = 9; 388 | t[2][6] = t[6][2] = 10; 389 | t[3][7] = t[7][3] = 11; 390 | return t; 391 | }(); 392 | 393 | constexpr std::array, 8> neighborIds = {{ 394 | {{1, 4, 2}}, 395 | {{0, 3, 5}}, 396 | {{0, 6, 3}}, 397 | {{1, 2, 7}}, 398 | {{0, 5, 6}}, 399 | {{1, 7, 4}}, 400 | {{2, 4, 7}}, 401 | {{3, 6, 5}}, 402 | }}; 403 | 404 | auto depthFirstSearch(bool occupied, unsigned int cur, unsigned int last, const Cell& cell, std::array & visited, std::vector& loop) { 405 | if (cell.occupancies[cur] != occupied || visited[cur]) 406 | return; 407 | visited[cur] = true; 408 | auto neighbors = neighborIds[cur]; 409 | const auto lastIndex = std::distance(begin(neighbors), std::find(begin(neighbors), end(neighbors), last)); 410 | std::rotate(begin(neighbors), begin(neighbors) + (lastIndex + 1) % 3, end(neighbors)); 411 | 412 | for (const auto n : neighbors) { 413 | if (cell.occupancies[n] == occupied) { 414 | depthFirstSearch(occupied, n, cur, cell, visited, loop); 415 | continue; 416 | } 417 | 418 | const auto axis = edgeToAxis[cur][n]; 419 | const auto& seg = cell.edges[pointsToEdgeId[cur][n]].front(); 420 | const auto curReal = cell.realPoint(cur); 421 | const auto nReal = cell.realPoint(n); 422 | Node node; 423 | if (seg.start.depth == curReal[axis] || seg.start.depth == nReal[axis]) 424 | node = seg.end; 425 | else 426 | node = seg.start; 427 | auto v = curReal; 428 | v[axis] = node.depth; 429 | loop.push_back({cur, n, v, node.normal}); 430 | } 431 | } 432 | 433 | auto findLoops(const Cell& cell, bool occupied) { 434 | std::array visited{}; 435 | std::vector> loops; 436 | for (auto start = 0u; start < 8; start++) { 437 | std::vector loop; 438 | depthFirstSearch(occupied, start, 0, cell, visited, loop); 439 | if (!loop.empty()) 440 | loops.push_back(std::move(loop)); 441 | } 442 | return loops; 443 | } 444 | 445 | void triangulateLoopIntoFan(const std::vector& loop, vec3 center, std::vector& triangles) { 446 | if (loop.size() < 3) 447 | return; 448 | for (auto a = 0; a < loop.size(); a++) { 449 | auto b = (a + 1) % loop.size(); 450 | triangles.push_back({center, loop[a], loop[b]}); 451 | } 452 | } 453 | 454 | void triangulateLoopIntoFan(const std::vector& loop, std::vector& triangles) { 455 | const auto center = std::accumulate(begin(loop), end(loop), vec3{0, 0, 0}) / static_cast(loop.size()); 456 | return triangulateLoopIntoFan(loop, center, triangles); 457 | } 458 | 459 | void triangulateLoopAtFirst(const std::vector& loop, std::vector& triangles) { 460 | if (loop.size() < 3) 461 | return; 462 | const auto center = loop.front(); 463 | for (auto b = 2; b < loop.size() - 1; b++) { 464 | const auto a = b - 1; 465 | triangles.push_back({center, loop[a], loop[b]}); 466 | } 467 | } 468 | 469 | constexpr auto planeEpsilon = 1e-6f; 470 | 471 | auto intersectPlanes(Plane a, Plane b, Plane c) -> std::optional { 472 | const auto det = glm::dot(glm::cross(a.n, b.n), c.n); 473 | if (std::abs(det) < planeEpsilon) 474 | return {}; 475 | return (glm::cross(b.n, c.n) * a.d + glm::cross(c.n, a.n) * b.d + glm::cross(a.n, b.n) * c.d) / det; 476 | } 477 | 478 | auto pointLineDistance(vec3 a, vec3 b, vec3 p) { 479 | return glm::length(glm::cross(p - a, p - b)) / glm::length(a - b); 480 | } 481 | 482 | template 483 | void removeCyclicRange(std::vector& loop, int first, int last) { 484 | if (first <= last) 485 | loop.erase(begin(loop) + first, begin(loop) + last + 1); 486 | else { 487 | loop.erase(begin(loop), begin(loop) + last + 1); 488 | loop.erase(begin(loop) + first - last, end(loop)); 489 | } 490 | } 491 | 492 | const auto cellSideNormals = []() { 493 | std::array, 8>, 8> t{}; 494 | 495 | auto assign3of4 = [&](int a, int b, int c, int d, vec3 n) { 496 | t[a][b][c] = n; 497 | t[a][b][d] = n; 498 | t[a][c][d] = n; 499 | t[b][c][d] = n; 500 | }; 501 | assign3of4(0, 2, 4, 6, {-1, 0, 0}); 502 | assign3of4(1, 3, 5, 7, {1, 0, 0}); 503 | assign3of4(0, 1, 4, 5, {0, -1, 0}); 504 | assign3of4(2, 3, 6, 7, {0, 1, 0}); 505 | assign3of4(0, 1, 2, 3, {0, 0, -1}); 506 | assign3of4(4, 5, 6, 7, {0, 0, 1}); 507 | return t; 508 | }(); 509 | 510 | constexpr auto lineEpsilon = 1e-6f; 511 | 512 | void triangulateLoopRefined(const std::vector& loop, const Cell& cell, std::vector& triangles) { 513 | const auto inside = [&](vec3 p) { 514 | return 515 | cell.box.lower.x < p.x && p.x < cell.box.upper.x && 516 | cell.box.lower.y < p.y && p.y < cell.box.upper.y && 517 | cell.box.lower.z < p.z && p.z < cell.box.upper.z; 518 | }; 519 | std::vector apexVertices; 520 | std::vector finalLoop; 521 | std::vector isEdgeVertex; 522 | auto intermedCount = 0; 523 | 524 | // create new boundary loop with intermediate vertices 525 | for (auto i = 0; i < loop.size(); i++) { 526 | const auto& a = loop[i]; 527 | const auto& b = loop[(i + 1) % loop.size()]; 528 | finalLoop.push_back(a.position); 529 | isEdgeVertex.push_back(true); 530 | std::vector points{a.point, a.neighbor, b.point, b.neighbor}; 531 | std::sort(begin(points), end(points)); 532 | points.erase(std::unique(begin(points), end(points)), end(points)); 533 | assert(points.size() >= 3); 534 | auto n = cellSideNormals[points[0]][points[1]][points[2]]; 535 | auto cellPlane = Plane{n, cell.realPoint(points[0])}; 536 | auto aPlane = Plane{a.normal, a.position}; 537 | auto bPlane = Plane{b.normal, b.position}; 538 | auto intermed = intersectPlanes(cellPlane, aPlane, bPlane); 539 | if (intermed) { 540 | const auto cellPlaneAxis = std::fabs(n[0]) > 0 ? 0 : (std::fabs(n[1]) > 0 ? 1 : 2); 541 | (*intermed)[cellPlaneAxis] = cell.realPoint(points[0])[cellPlaneAxis]; 542 | if (inside(*intermed)) { 543 | const auto dist = pointLineDistance(a.position, b.position, *intermed); 544 | if (dist > lineEpsilon) { 545 | finalLoop.push_back(*intermed); 546 | isEdgeVertex.push_back(false); 547 | intermedCount++; 548 | apexVertices.push_back(&a); 549 | apexVertices.push_back(&b); 550 | } 551 | } 552 | } 553 | } 554 | 555 | // try to create an apex vertex 556 | std::optional apex; 557 | if (intermedCount >= 3) { 558 | std::sort(begin(apexVertices), end(apexVertices)); 559 | apexVertices.erase(std::unique(begin(apexVertices), end(apexVertices)), end(apexVertices)); 560 | assert(apexVertices.size() >= 3); 561 | std::vector planes; 562 | for (auto i : {0, 1, 2}) 563 | planes.push_back({apexVertices[i]->normal, apexVertices[i]->position}); 564 | const auto a = intersectPlanes(planes[0], planes[1], planes[2]); 565 | if (a && inside(*a)) 566 | apex = a; 567 | } 568 | 569 | if (apex) 570 | triangulateLoopIntoFan(finalLoop, *apex, triangles); 571 | else if (intermedCount == 0) 572 | triangulateLoopIntoFan(finalLoop, triangles); 573 | else if (intermedCount == 1) { 574 | std::rotate(begin(finalLoop), begin(finalLoop) + std::distance(begin(isEdgeVertex), std::find(begin(isEdgeVertex), end(isEdgeVertex), false)), end(finalLoop)); 575 | triangulateLoopAtFirst(finalLoop, triangles); 576 | } else { 577 | for (auto first = 0u; first < finalLoop.size(); first++) { 578 | if (isEdgeVertex[first]) 579 | continue; 580 | auto last = (first + 1) % (unsigned int)finalLoop.size(); 581 | while (isEdgeVertex[last]) 582 | last = (last + 1) % (unsigned int)finalLoop.size(); 583 | std::vector subLoop; 584 | for (auto i = first; i != last; i = (i + 1) % (unsigned int)finalLoop.size()) 585 | subLoop.push_back(finalLoop[i]); 586 | subLoop.push_back(finalLoop[last]); 587 | assert(subLoop.size() >= 3); 588 | triangulateLoopIntoFan(subLoop, triangles); 589 | removeCyclicRange(finalLoop, first + 1, last - 1); 590 | removeCyclicRange(isEdgeVertex, first + 1, last - 1); 591 | } 592 | triangulateLoopIntoFan(finalLoop, triangles); 593 | } 594 | } 595 | 596 | auto reverseLoops(std::vector> loops) { 597 | for (auto& loop : loops) 598 | std::reverse(begin(loop), end(loop)); 599 | return loops; 600 | } 601 | 602 | void triangulateCell(const Cell& cell, std::vector& triangles) { 603 | std::vector> loops; 604 | if (!isProblematicCase(cell)) 605 | loops = findLoops(cell, false); 606 | else 607 | loops = reverseLoops(findLoops(cell, true)); 608 | for (const auto& loop : loops) 609 | triangulateLoopRefined(loop, cell, triangles); 610 | } 611 | 612 | void dumpDexels(const TriDexelGrid& grid, std::filesystem::path path) { 613 | std::vector result; 614 | 615 | for (auto axis0 : {0, 1, 2}) { 616 | const auto& img = grid.image.images[axis0]; 617 | const auto axis1 = (axis0 + 1) % 3; 618 | const auto axis2 = (axis0 + 2) % 3; 619 | const auto xCount = img.axis1Res; 620 | const auto yCount = img.axis2Res; 621 | const auto deltaX = (grid.box.upper[axis1] - grid.box.lower[axis1]) / (xCount - 1); 622 | const auto deltaY = (grid.box.upper[axis2] - grid.box.lower[axis2]) / (yCount - 1); 623 | 624 | for (auto y = 0u; y < yCount; y++) { 625 | for (auto x = 0u; x < xCount; x++) { 626 | const auto origin = createRay(grid.box, axis0, axis1, axis2, deltaX, deltaY, x, y).origin; 627 | 628 | const auto& dexel = img.dexel(x, y); 629 | for (const auto& s : dexel.segments()) { 630 | auto start = origin; 631 | start[axis0] = s.start.depth; 632 | auto end = origin; 633 | end[axis0] = s.end.depth; 634 | result.push_back({start, (start + end) / 2.0f, end}); 635 | } 636 | } 637 | } 638 | } 639 | 640 | saveTriangles(path, result); 641 | } 642 | 643 | void dumpCell(const Cell& cell, std::filesystem::path path) { 644 | savePoints(path / "box.ply", {cell.box.lower, cell.box.upper}); 645 | 646 | std::vector realPoints; 647 | for (auto i = 0; i < 8; i++) 648 | realPoints.push_back(cell.realPoint(i)); 649 | savePoints(path / "realPoints.ply", realPoints); 650 | 651 | std::vector edges; 652 | for (auto i = 0; i < 12; i++) { 653 | const auto [src, dst] = edgeToPointIds[i]; 654 | const auto axis = i / 4; 655 | const auto p = cell.realPoint(src); 656 | for (const auto& e : cell.edges[i]) { 657 | vec3 start = p; 658 | start[axis] = e.start.depth; 659 | vec3 end = p; 660 | end[axis] = e.end.depth; 661 | edges.push_back({start, (start + end) / 2.0f, end}); 662 | } 663 | } 664 | saveTriangles(path / "edges.stl", edges); 665 | } 666 | } 667 | 668 | auto tridexel(BoundingBox box, unsigned int resolution, RaycastCallback rcc) -> std::vector { 669 | const auto res = uniformResolution(box, resolution); 670 | 671 | auto img = TriDexelImage{ res }; 672 | axisParallelRaycast(box, res, rcc, [&](unsigned int axis, unsigned int x, unsigned int y, float depth, vec3 normal) { 673 | auto& im = img.images[axis]; 674 | im.dexel(x, y).nodes.push_back({depth, normal}); 675 | }); 676 | 677 | auto grid = TriDexelGrid{ img, box }; 678 | assignOccupancies(grid); 679 | 680 | //dumpDexels(grid, "dexels.stl"); 681 | 682 | std::vector triangles; 683 | for (auto z = 0u; z < grid.image.resolution.z - 1; z++) { 684 | for (auto y = 0u; y < grid.image.resolution.y - 1; y++) { 685 | for (auto x = 0u; x < grid.image.resolution.x - 1; x++) { 686 | auto cell = grid.cell({ x, y, z }); 687 | if (!cell.empty()) { 688 | //const auto folder = "cell_" + std::to_string(x) + "_" + std::to_string(y) + "_" + std::to_string(z); 689 | //dumpCell(cell, folder + "_before"); 690 | regularizeCell(cell); 691 | //dumpCell(cell, folder + "_after"); 692 | 693 | std::vector cellTriangles; 694 | triangulateCell(cell, cellTriangles); 695 | //saveTriangles(folder + "_after/triangles.stl", cellTriangles); 696 | 697 | triangles.insert(end(triangles), begin(cellTriangles), end(cellTriangles)); 698 | } 699 | } 700 | } 701 | } 702 | 703 | return triangles; 704 | } 705 | -------------------------------------------------------------------------------- /src/tridexel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | struct Ray { 11 | glm::vec3 origin; 12 | glm::vec3 direction; 13 | }; 14 | 15 | struct BoundingBox { 16 | glm::vec3 lower; 17 | glm::vec3 upper; 18 | }; 19 | 20 | struct Triangle : std::array {}; 21 | 22 | using HitCallback = std::function; 23 | using RaycastCallback = std::function; 24 | 25 | auto tridexel(BoundingBox box, unsigned int resolution, RaycastCallback rcc) -> std::vector; 26 | -------------------------------------------------------------------------------- /test/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char** argv) { 4 | ::testing::InitGoogleTest(&argc, argv); 5 | return RUN_ALL_TESTS(); 6 | } 7 | -------------------------------------------------------------------------------- /test/sphere.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "../src/tridexel.h" 8 | #include "../src/IO.h" 9 | 10 | namespace { 11 | struct Sphere { 12 | glm::vec3 center; 13 | float radius; 14 | }; 15 | 16 | auto solveQuadraticEquation(double a, double b, double c) -> boost::container::static_vector { 17 | const double discriminat = std::pow(b, 2) - 4 * a * c; 18 | if (discriminat < 0) 19 | return {}; 20 | 21 | if (discriminat == 0) 22 | return { -b / 2 * a }; 23 | 24 | const auto x1 = (-b - std::sqrt(discriminat)) / 2 * a; 25 | const auto x2 = (-b + std::sqrt(discriminat)) / 2 * a; 26 | return { x1, x2 }; 27 | } 28 | 29 | void extractSphere(int resolution) { 30 | const auto sphere = Sphere{glm::vec3{0, 0, 0}, 5}; 31 | const auto box = BoundingBox{sphere.center - sphere.radius, sphere.center + sphere.radius}; 32 | const auto triangles = tridexel(box, resolution, [&](Ray ray, HitCallback hc) { 33 | // from https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-sphere-intersection 34 | // solve quadratic equation 35 | const auto L = ray.origin - sphere.center; 36 | const auto a = 1.0; 37 | const auto b = 2 * glm::dot(ray.direction, L); 38 | const auto c = glm::dot(L, L) - std::pow(sphere.radius, 2); 39 | const auto solutions = solveQuadraticEquation(a, b, c); 40 | for (const auto& t : solutions) { 41 | const auto point = ray.origin + (float)t * ray.direction; 42 | const auto normal = glm::normalize(point - sphere.center); 43 | hc(glm::dot(ray.origin, ray.direction) + (float)t, normal); 44 | } 45 | }); 46 | saveTriangles("sphere" + std::to_string(resolution) + ".stl", triangles); 47 | } 48 | } 49 | 50 | TEST(sphere, extract5) { 51 | extractSphere(5); 52 | } 53 | 54 | TEST(sphere, extract10) { 55 | extractSphere(10); 56 | } 57 | 58 | TEST(sphere, extract100) { 59 | extractSphere(100); 60 | } 61 | 62 | TEST(sphere, extract200) { 63 | extractSphere(200); 64 | } 65 | --------------------------------------------------------------------------------