/******************************************************************************* Copyright (c) 2014 David Williams and Matt Williams This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. *******************************************************************************/ #include "PolyVoxCore/SurfaceMesh.h" #include "PolyVoxCore/VertexTypes.h" #include "Impl/Timer.h" #include "Impl/QEF.h" #include //BUG We will get duplucation of edges if the surface is along region boundaries namespace PolyVox { namespace { template struct EdgeData { EdgeData() : intersects(false) {} Vector3DFloat normal; float fraction; /// struct CellData { EdgeData edges[3]; uint32_t vertexIndex; }; template EdgeData calculateEdge(const VoxelType& vA, const VoxelType& vB, const Vector3DFloat& gA, const Vector3DFloat& gB, const ThresholdType& threshold) { EdgeData edge; edge.fraction = static_cast(vA - threshold) / static_cast(vA - vB); if(std::min(vA,vB) <= threshold && std::max(vA,vB) > threshold) { edge.intersects = true; } else { edge.intersects = false; return edge; } edge.normal = (gA * edge.fraction + gB * (1.0f-edge.fraction)); if(edge.normal.lengthSquared() != 0.0f) { edge.normal.normalise(); } return edge; } template PositionMaterialNormal computeVertex(EdgeData* edges[12]) { Vector3DFloat massPoint{0,0,0}; //The average of the intersection vertices Vector3DFloat vertices[12]; vertices[0] = {edges[0]->fraction, 0, 0}; vertices[1] = {0, edges[1]->fraction, 0}; vertices[2] = {0, 0, edges[2]->fraction}; vertices[3] = {1, edges[3]->fraction, 0}; vertices[4] = {1, 0, edges[4]->fraction}; vertices[5] = {0, 1, edges[5]->fraction}; vertices[6] = {edges[6]->fraction, 1, 0}; vertices[7] = {edges[7]->fraction, 0, 1}; vertices[8] = {0, edges[8]->fraction, 1}; vertices[9] = {1, 1, edges[9]->fraction}; vertices[10] = {1, edges[10]->fraction, 1}; vertices[11] = {edges[11]->fraction, 1, 1}; int numIntersections = 0; for(int i = 0; i < 12; ++i) { if(!edges[i]->intersects) { continue; } ++numIntersections; massPoint += vertices[i]; } massPoint /= numIntersections; //Make the average Vector3DFloat cellVertexNormal{0,0,0}; double matrix[12][3]; double vector[12]; int rows = 0; for(int i = 0; i < 12; ++i) { if(!edges[i]->intersects) { continue; } Vector3DFloat normal = edges[i]->normal; matrix[rows][0] = normal.getX(); matrix[rows][1] = normal.getY(); matrix[rows][2] = normal.getZ(); const Vector3DFloat product = normal * (vertices[i] - massPoint); vector[rows] = product.getX() + product.getY() + product.getZ(); cellVertexNormal += normal; ++rows; } const auto& vertexPosition = evaluateQEF(matrix, vector, rows) + massPoint; POLYVOX_ASSERT(vertexPosition.getX() >= -0.01 && vertexPosition.getY() >= -0.01 && vertexPosition.getZ() >= -0.01 && vertexPosition.getX() <= 1.01 && vertexPosition.getY() <= 1.01 && vertexPosition.getZ() <= 1.01, "Vertex is outside unit cell"); //0.01 to give a little leniency if(cellVertexNormal.lengthSquared() != 0.0f) { cellVertexNormal.normalise(); } return {vertexPosition, cellVertexNormal, 0}; } uint32_t convert(uint32_t x, uint32_t y, uint32_t z, uint32_t X, uint32_t Y) { return z*Y*X+y*X+x; } } template SurfaceMesh dualContouringSurfaceExtractor(VolumeType* volData, const Region& region, const ControllerType& controller) { static_assert(std::is_signed::value, "Voxel type must be signed"); const auto threshold = controller.getThreshold(); //Timer timer; Timer totalTimer; const auto regionXDimension = region.getDimensionsInVoxels().getX(); const auto regionYDimension = region.getDimensionsInVoxels().getY(); const auto regionZDimension = region.getDimensionsInVoxels().getZ(); const auto gradientRegionXDimension = regionXDimension+2; const auto gradientRegionYDimension = regionYDimension+2; const auto gradientRegionZDimension = regionZDimension+2; std::vector> gradients; gradients.reserve(gradientRegionXDimension * gradientRegionYDimension * gradientRegionZDimension); typename VolumeType::Sampler volSampler{volData}; volSampler.setPosition(region.getLowerCorner() - Vector3DInt32{1,1,1}); volSampler.setWrapMode(WrapMode::Border, -100.0); // -100.0 is well below the threshold const auto lowerCornerX = region.getLowerCorner().getX(); const auto lowerCornerY = region.getLowerCorner().getZ(); const auto lowerCornerZ = region.getLowerCorner().getX(); //logTrace() << "Setup took " << timer.elapsedTimeInMilliSeconds(); //timer.start(); for(int32_t z = 0; z < gradientRegionZDimension; z++) { volSampler.setPosition(lowerCornerX-1, lowerCornerY-1, lowerCornerZ+z-1); //Reset x and y and increment z for(int32_t y = 0; y < gradientRegionYDimension; y++) { volSampler.setPosition(lowerCornerX-1, lowerCornerY+y-1, lowerCornerZ+z-1); //Reset x and increment y (z remains the same) for(int32_t x = 0; x < gradientRegionXDimension; x++) { volSampler.movePositiveX(); //Increment x const auto& voxel = controller.convertToDensity(volSampler.getVoxel()); const auto& voxel1px = controller.convertToDensity(volSampler.peekVoxel1px0py0pz()); const auto& voxel1py = controller.convertToDensity(volSampler.peekVoxel0px1py0pz()); const auto& voxel1pz = controller.convertToDensity(volSampler.peekVoxel0px0py1pz()); const auto& voxel1nx = controller.convertToDensity(volSampler.peekVoxel1nx0py0pz()); const auto& voxel1ny = controller.convertToDensity(volSampler.peekVoxel0px1ny0pz()); const auto& voxel1nz = controller.convertToDensity(volSampler.peekVoxel0px0py1nz()); gradients.emplace_back(voxel, Vector3DFloat(voxel1nx - voxel1px, voxel1ny - voxel1py, voxel1nz - voxel1pz)); } } } //logTrace() << "Gradients took " << timer.elapsedTimeInMilliSeconds(); //timer.start(); const auto cellRegionXDimension = regionXDimension+2; const auto cellRegionYDimension = regionYDimension+2; const auto cellRegionZDimension = regionZDimension+2; std::vector> cells; cells.reserve(cellRegionXDimension * cellRegionYDimension * cellRegionZDimension); for(int32_t cellZ = 0; cellZ < cellRegionZDimension; cellZ++) { for(int32_t cellY = 0; cellY < cellRegionYDimension; cellY++) { for(int32_t cellX = 0; cellX < cellRegionXDimension; cellX++) { //For each cell, calculate the edge intersection points and normals const auto& g000 = gradients[convert(cellX, cellY, cellZ, cellRegionXDimension, cellRegionYDimension)]; //For the last columns/rows, only calculate the interior edge if(cellX < cellRegionXDimension-1 && cellY < cellRegionYDimension-1 && cellZ < cellRegionZDimension-1) //This is the main bulk { const auto& g100 = gradients[convert(cellX+1, cellY, cellZ, cellRegionXDimension, cellRegionYDimension)]; const auto& g010 = gradients[convert(cellX, cellY+1, cellZ, cellRegionXDimension, cellRegionYDimension)]; const auto& g001 = gradients[convert(cellX, cellY, cellZ+1, cellRegionXDimension, cellRegionYDimension)]; cells.push_back({calculateEdge(g000.first, g100.first, g000.second, g100.second, threshold), calculateEdge(g000.first, g010.first, g000.second, g010.second, threshold), calculateEdge(g000.first, g001.first, g000.second, g001.second, threshold)}); } else if(cellX == cellRegionXDimension-1 || cellY == cellRegionYDimension-1 || cellZ == cellRegionZDimension-1) //This is the three far edges and the far corner { cells.push_back({}); //Default and empty } else if(cellX == cellRegionXDimension-1) //Far x side { const auto& g100 = gradients[convert(cellX+1, cellY, cellZ, cellRegionXDimension, cellRegionYDimension)]; cells.push_back({calculateEdge(g000.first, g100.first, g000.second, g100.second, threshold), EdgeData(), EdgeData()}); } else if(cellY == cellRegionYDimension-1) //Far y side { const auto& g010 = gradients[convert(cellX+1, cellY, cellZ, cellRegionXDimension, cellRegionYDimension)]; cells.push_back({EdgeData(), calculateEdge(g000.first, g010.first, g000.second, g010.second, threshold), EdgeData()}); } else if(cellZ == cellRegionZDimension-1) //Far z side { const auto& g001 = gradients[convert(cellX+1, cellY, cellZ, cellRegionXDimension, cellRegionYDimension)]; cells.push_back({EdgeData(), EdgeData(), calculateEdge(g000.first, g001.first, g000.second, g001.second, threshold)}); } } } } //logTrace() << "Edges took " << timer.elapsedTimeInMilliSeconds(); //timer.start(); EdgeData* edges[12]; //Create this now but it will be overwritten for each cell SurfaceMesh mesh; for(int32_t cellZ = 0; cellZ < cellRegionZDimension; cellZ++) { for(int32_t cellY = 0; cellY < cellRegionYDimension; cellY++) { for(int32_t cellX = 0; cellX < cellRegionXDimension; cellX++) { if(cellZ >= 1 && cellY >= 1 && cellX >= 1) { //After the first rows and columns are done, start calculating vertex positions const int32_t cellXVertex = cellX-1; const int32_t cellYVertex = cellY-1; const int32_t cellZVertex = cellZ-1; auto& cell = cells[convert(cellXVertex, cellYVertex, cellZVertex, cellRegionXDimension, cellRegionYDimension)]; edges[0] = &cell.edges[0]; edges[1] = &cell.edges[1]; edges[2] = &cell.edges[2]; edges[3] = &cells[convert(cellXVertex+1, cellYVertex, cellZVertex, cellRegionXDimension, cellRegionYDimension)].edges[1]; edges[4] = &cells[convert(cellXVertex+1, cellYVertex, cellZVertex, cellRegionXDimension, cellRegionYDimension)].edges[2]; edges[5] = &cells[convert(cellXVertex, cellYVertex+1, cellZVertex, cellRegionXDimension, cellRegionYDimension)].edges[2]; edges[6] = &cells[convert(cellXVertex, cellYVertex+1, cellZVertex, cellRegionXDimension, cellRegionYDimension)].edges[0]; edges[7] = &cells[convert(cellXVertex, cellYVertex, cellZVertex+1, cellRegionXDimension, cellRegionYDimension)].edges[0]; edges[8] = &cells[convert(cellXVertex, cellYVertex, cellZVertex+1, cellRegionXDimension, cellRegionYDimension)].edges[1]; edges[9] = &cells[convert(cellXVertex+1, cellYVertex+1, cellZVertex, cellRegionXDimension, cellRegionYDimension)].edges[2]; edges[10] = &cells[convert(cellXVertex+1, cellYVertex, cellZVertex+1, cellRegionXDimension, cellRegionYDimension)].edges[1]; edges[11] = &cells[convert(cellXVertex, cellYVertex+1, cellZVertex+1, cellRegionXDimension, cellRegionYDimension)].edges[0]; if(edges[0]->intersects || edges[1]->intersects || edges[2]->intersects || edges[3]->intersects || edges[4]->intersects || edges[5]->intersects || edges[6]->intersects || edges[7]->intersects || edges[8]->intersects || edges[9]->intersects || edges[10]->intersects || edges[11]->intersects) //'if' Maybe not needed? { auto vertex = computeVertex(edges); vertex.setPosition({vertex.getPosition().getX()+cellXVertex, vertex.getPosition().getY()+cellYVertex, vertex.getPosition().getZ()+cellZVertex}); cell.vertexIndex = mesh.addVertex(vertex); if(cellZVertex >= 1 && cellYVertex >= 1 && cellXVertex >= 1) { //Once the second rows and colums are done, start connecting up edges if(cell.edges[0].intersects) { const auto& v1 = cells[convert(cellXVertex, cellYVertex-1, cellZVertex, cellRegionXDimension, cellRegionYDimension)]; const auto& v2 = cells[convert(cellXVertex, cellYVertex, cellZVertex-1, cellRegionXDimension, cellRegionYDimension)]; const auto& v3 = cells[convert(cellXVertex, cellYVertex-1, cellZVertex-1, cellRegionXDimension, cellRegionYDimension)]; mesh.addTriangle(cell.vertexIndex, v1.vertexIndex, v2.vertexIndex); mesh.addTriangle(v3.vertexIndex, v2.vertexIndex, v1.vertexIndex); } if(cell.edges[1].intersects) { const auto& v1 = cells[convert(cellXVertex-1, cellYVertex, cellZVertex, cellRegionXDimension, cellRegionYDimension)]; const auto& v2 = cells[convert(cellXVertex, cellYVertex, cellZVertex-1, cellRegionXDimension, cellRegionYDimension)]; const auto& v3 = cells[convert(cellXVertex-1, cellYVertex, cellZVertex-1, cellRegionXDimension, cellRegionYDimension)]; mesh.addTriangle(cell.vertexIndex, v1.vertexIndex, v2.vertexIndex); mesh.addTriangle(v3.vertexIndex, v2.vertexIndex, v1.vertexIndex); } if(cell.edges[2].intersects) { const auto& v1 = cells[convert(cellXVertex-1, cellYVertex, cellZVertex, cellRegionXDimension, cellRegionYDimension)]; const auto& v2 = cells[convert(cellXVertex, cellYVertex-1, cellZVertex, cellRegionXDimension, cellRegionYDimension)]; const auto& v3 = cells[convert(cellXVertex-1, cellYVertex-1, cellZVertex, cellRegionXDimension, cellRegionYDimension)]; mesh.addTriangle(cell.vertexIndex, v1.vertexIndex, v2.vertexIndex); mesh.addTriangle(v3.vertexIndex, v2.vertexIndex, v1.vertexIndex); } } } } } } } //logTrace() << "Vertices and quads took " << timer.elapsedTimeInMilliSeconds(); //timer.start(); logTrace() << "Dual contouring surface extraction took " << totalTimer.elapsedTimeInMilliSeconds() << "ms (Region size = " << region.getWidthInVoxels() << "x" << region.getHeightInVoxels() << "x" << region.getDepthInVoxels() << ")"; logTrace() << mesh.getNoOfVertices(); return mesh; } }