diff --git a/Inc/GeometricPrimitive.h b/Inc/GeometricPrimitive.h index d672badb3..cbcfb7f78 100644 --- a/Inc/GeometricPrimitive.h +++ b/Inc/GeometricPrimitive.h @@ -28,11 +28,12 @@ namespace DirectX ~GeometricPrimitive(); // Factory methods. - static std::unique_ptr CreateCube (_In_ ID3D11DeviceContext* deviceContext, float size = 1); - static std::unique_ptr CreateSphere (_In_ ID3D11DeviceContext* deviceContext, float diameter = 1, size_t tessellation = 16); - static std::unique_ptr CreateCylinder(_In_ ID3D11DeviceContext* deviceContext, float height = 1, float diameter = 1, size_t tessellation = 32); - static std::unique_ptr CreateTorus (_In_ ID3D11DeviceContext* deviceContext, float diameter = 1, float thickness = 0.333f, size_t tessellation = 32); - static std::unique_ptr CreateTeapot (_In_ ID3D11DeviceContext* deviceContext, float size = 1, size_t tessellation = 8); + static std::unique_ptr CreateCube (_In_ ID3D11DeviceContext* deviceContext, float size = 1); + static std::unique_ptr CreateSphere (_In_ ID3D11DeviceContext* deviceContext, float diameter = 1, size_t tessellation = 16); + static std::unique_ptr CreateGeoSphere(_In_ ID3D11DeviceContext* deviceContext, float diameter = 1, size_t tessellation = 3); + static std::unique_ptr CreateCylinder (_In_ ID3D11DeviceContext* deviceContext, float height = 1, float diameter = 1, size_t tessellation = 32); + static std::unique_ptr CreateTorus (_In_ ID3D11DeviceContext* deviceContext, float diameter = 1, float thickness = 0.333f, size_t tessellation = 32); + static std::unique_ptr CreateTeapot (_In_ ID3D11DeviceContext* deviceContext, float size = 1, size_t tessellation = 8); // Draw the primitive. void Draw(CXMMATRIX world, CXMMATRIX view, CXMMATRIX projection, FXMVECTOR color = Colors::White, _In_opt_ ID3D11ShaderResourceView* texture = nullptr, bool wireframe = false, _In_opt_ std::function setCustomState = nullptr); diff --git a/Src/GeometricPrimitive.cpp b/Src/GeometricPrimitive.cpp index 112f6c283..2b6de57a3 100644 --- a/Src/GeometricPrimitive.cpp +++ b/Src/GeometricPrimitive.cpp @@ -19,6 +19,7 @@ #include "SharedResourcePool.h" #include "Bezier.h" #include +#include using namespace DirectX; using namespace Microsoft::WRL; @@ -26,6 +27,14 @@ using namespace Microsoft::WRL; namespace { + void CheckIndexOverflow(size_t value) + { + // Use >=, not > comparison, because some D3D level 9_x hardware does not support 0xFFFF index values. + if (value >= USHRT_MAX) + throw std::exception("Index value out of range: cannot tesselate primitive so finely"); + } + + // Temporary collection types used when generating the geometry. typedef std::vector VertexCollection; @@ -36,10 +45,7 @@ namespace // Sanity check the range of 16 bit index values. void push_back(size_t value) { - // Use >=, not > comparison, because some D3D level 9_x hardware does not support 0xFFFF index values. - if (value >= USHRT_MAX) - throw std::exception("Index value out of range: cannot tesselate primitive so finely"); - + CheckIndexOverflow(value); vector::push_back((uint16_t)value); } }; @@ -417,6 +423,332 @@ std::unique_ptr GeometricPrimitive::CreateSphere(_In_ ID3D11 } +// Creates a geosphere primitive. +std::unique_ptr GeometricPrimitive::CreateGeoSphere(_In_ ID3D11DeviceContext* deviceContext, float diameter, size_t tessellation) +{ + // An undirected edge between two vertices, represented by a pair of indexes into a vertex array. + // Becuse this edge is undirected, (a,b) is the same as (b,a). + typedef std::pair UndirectedEdge; + + // Makes an undirected edge. Rather than overloading comparison operators to give us the (a,b)==(b,a) property, + // we'll just ensure that the larger of the two goes first. This'll simplify things greatly. + auto makeUndirectedEdge = [](uint16_t a, uint16_t b) + { + return std::make_pair(std::max(a, b), std::min(a, b)); + }; + + // Key: an edge + // Value: the index of the vertex which lies midway between the two vertices pointed to by the key value + // This map is used to avoid duplicating vertices when subdividing triangles along edges. + typedef std::map EdgeSubdivisionMap; + + + static const XMFLOAT3 OctahedronVertices[] = + { + // when looking down the negative z-axis (into the screen) + XMFLOAT3( 0, 1, 0), // 0 top + XMFLOAT3( 0, 0, -1), // 1 front + XMFLOAT3( 1, 0, 0), // 2 right + XMFLOAT3( 0, 0, 1), // 3 back + XMFLOAT3(-1, 0, 0), // 4 left + XMFLOAT3( 0, -1, 0), // 5 bottom + }; + static const uint16_t OctahedronIndices[] = + { + 0, 1, 2, // top front-right face + 0, 2, 3, // top back-right face + 0, 3, 4, // top back-left face + 0, 4, 1, // top front-left face + 5, 1, 4, // bottom front-left face + 5, 4, 3, // bottom back-left face + 5, 3, 2, // bottom back-right face + 5, 2, 1, // bottom front-right face + }; + + const float radius = diameter / 2.0f; + + // Start with an octahedron; copy the data into the vertex/index collection. + + std::vector vertexPositions(std::begin(OctahedronVertices), std::end(OctahedronVertices)); + + IndexCollection indices; + indices.insert(indices.begin(), std::begin(OctahedronIndices), std::end(OctahedronIndices)); + + // We know these values by looking at the above index list for the octahedron. Despite the subdivisions that are + // about to go on, these values aren't ever going to change because the vertices don't move around in the array. + // We'll need these values later on to fix the singularities that show up at the poles. + const uint16_t northPoleIndex = 0; + const uint16_t southPoleIndex = 5; + + for (size_t iSubdivision = 0; iSubdivision < tessellation; ++iSubdivision) + { + assert(indices.size() % 3 == 0); // sanity + + // We use this to keep track of which edges have already been subdivided. + EdgeSubdivisionMap subdividedEdges; + + // The new index collection after subdivision. + IndexCollection newIndices; + + const size_t triangleCount = indices.size() / 3; + for (size_t iTriangle = 0; iTriangle < triangleCount; ++iTriangle) + { + // For each edge on this triangle, create a new vertex in the middle of that edge. + // The winding order of the triangles we output are the same as the winding order of the inputs. + + // Indices of the vertices making up this triangle + uint16_t iv0 = indices[iTriangle*3+0]; + uint16_t iv1 = indices[iTriangle*3+1]; + uint16_t iv2 = indices[iTriangle*3+2]; + + // The existing vertices + XMFLOAT3 v0 = vertexPositions[iv0]; + XMFLOAT3 v1 = vertexPositions[iv1]; + XMFLOAT3 v2 = vertexPositions[iv2]; + + // Get the new vertices + + XMFLOAT3 v01; // vertex on the midpoint of v0 and v1 + XMFLOAT3 v12; // ditto v1 and v2 + XMFLOAT3 v20; // ditto v2 and v0 + uint32_t iv01; // index of v01 + uint32_t iv12; // index of v12 + uint32_t iv20; // index of v20 + + // Function that, when given the index of two vertices, creates a new vertex at the midpoint of those vertices. + auto divideEdge = [&](uint32_t i0, uint32_t i1, XMFLOAT3& outVertex, uint32_t& outIndex) + { + const UndirectedEdge edge = makeUndirectedEdge(i0, i1); + + // Check to see if we've already generated this vertex + auto it = subdividedEdges.find(edge); + if (it != subdividedEdges.end()) + { + // We've already generated this vertex before + outIndex = it->second; // the index of this vertex + outVertex = vertexPositions[outIndex]; // and the vertex itself + } + else + { + // Haven't generated this vertex before: so add it now + + // outVertex = (vertices[i0] + vertices[i1]) / 2 + XMStoreFloat3( + &outVertex, + XMVectorScale( + XMVectorAdd(XMLoadFloat3(&vertexPositions[i0]), XMLoadFloat3(&vertexPositions[i1])), + 0.5f + ) + ); + + outIndex = static_cast( vertexPositions.size() ); + CheckIndexOverflow(outIndex); + vertexPositions.push_back(outVertex); + + // Now add it to the map. + subdividedEdges.insert(std::make_pair(edge, outIndex)); + } + }; + + // Add/get new vertices and their indices + divideEdge(iv0, iv1, v01, iv01); + divideEdge(iv1, iv2, v12, iv12); + divideEdge(iv0, iv2, v20, iv20); + + // Add the new indices. We have four new triangles from our original one: + // v0 + // o + // /a\ + // v20 o---o v01 + // /b\c/d\ + // v2 o---o---o v1 + // v12 + const uint32_t indicesToAdd[] = + { + iv0, iv01, iv20, // a + iv20, iv12, iv2, // b + iv20, iv01, iv12, // c + iv01, iv1, iv12, // d + }; + newIndices.insert(newIndices.end(), std::begin(indicesToAdd), std::end(indicesToAdd)); + } + + indices = std::move(newIndices); + } + + // Now that we've completed subdivision, fill in the final vertex collection + VertexCollection vertices; + vertices.reserve(vertexPositions.size()); + for (auto it = vertexPositions.begin(); it != vertexPositions.end(); ++it) + { + auto vertexValue = *it; + + auto normal = XMVector3Normalize(XMLoadFloat3(&vertexValue)); + auto pos = XMVectorScale(normal, radius); + + XMFLOAT3 normalFloat3; + XMStoreFloat3(&normalFloat3, normal); + + // calculate texture coordinates for this vertex + float longitude = atan2(normalFloat3.x, -normalFloat3.z); + float latitude = acos(normalFloat3.y); + + float u = longitude / XM_2PI + 0.5f; + float v = latitude / XM_PI; + + auto texcoord = XMVectorSet(1.0f - u, v, 0.0f, 0.0f); + vertices.push_back(VertexPositionNormalTexture(pos, normal, texcoord)); + } + + // There are a couple of fixes to do. One is a texture coordinate wraparound fixup. At some point, there will be + // a set of triangles somewhere in the mesh with texture coordinates such that the wraparound across 0.0/1.0 + // occurs across that triangle. Eg. when the left hand side of the triangle has a U coordinate of 0.98 and the + // right hand side has a U coordinate of 0.0. The intent is that such a triangle should render with a U of 0.98 to + // 1.0, not 0.98 to 0.0. If we don't do this fixup, there will be a visible seam across one side of the sphere. + // + // Luckily this is relatively easy to fix. There is a straight edge which runs down the prime meridian of the + // completed sphere. If you imagine the vertices along that edge, they circumscribe a semicircular arc starting at + // y=1 and ending at y=-1, and sweeping across the range of z=0 to z=1. x stays zero. It's along this edge that we + // need to duplicate our vertices - and provide the correct texture coordinates. + size_t preFixupVertexCount = vertices.size(); + for (size_t i = 0; i < preFixupVertexCount; ++i) + { + // This vertex is on the prime meridian if position.x and texcoord.u are both zero (allowing for small epsilon). + bool isOnPrimeMeridian = XMVector2NearEqual( + XMVectorSet(vertices[i].position.x, vertices[i].textureCoordinate.x, 0.0f, 0.0f), + XMVectorZero(), + XMVectorSplatEpsilon()); + + if (isOnPrimeMeridian) + { + size_t newIndex = vertices.size(); // the index of this vertex that we're about to add + CheckIndexOverflow(newIndex); + + // copy this vertex, correct the texture coordinate, and add the vertex + VertexPositionNormalTexture v = vertices[i]; + v.textureCoordinate.x = 1.0f; + vertices.push_back(v); + + // Now find all the triangles which contain this vertex and update them if necessary + for (size_t j = 0; j < indices.size(); j += 3) + { + uint16_t* triIndex0 = &indices[j+0]; + uint16_t* triIndex1 = &indices[j+1]; + uint16_t* triIndex2 = &indices[j+2]; + + if (*triIndex0 == i) + { + // nothing; just keep going + } + else if (*triIndex1 == i) + { + std::swap(triIndex0, triIndex1); // swap the pointers (not the values) + } + else if (*triIndex2 == i) + { + std::swap(triIndex0, triIndex2); // swap the pointers (not the values) + } + else + { + // this triangle doesn't use the vertex we're interested in + continue; + } + + // If we got to this point then triIndex0 is the pointer to the index to the vertex we're looking at + assert(*triIndex0 == i); + assert(*triIndex1 != i && *triIndex2 != i); // assume no degenerate triangles + + const VertexPositionNormalTexture& v0 = vertices[*triIndex0]; + const VertexPositionNormalTexture& v1 = vertices[*triIndex1]; + const VertexPositionNormalTexture& v2 = vertices[*triIndex2]; + + // check the other two vertices to see if we might need to fix this triangle + + if (abs(v0.textureCoordinate.x - v1.textureCoordinate.x) > 0.5f || + abs(v0.textureCoordinate.x - v2.textureCoordinate.x) > 0.5f) + { + // yep; replace the specified index to point to the new, corrected vertex + *triIndex0 = static_cast(newIndex); + } + } + } + } + + // And one last fix we need to do: the poles. A common use-case of a sphere mesh is to map a rectangular texture onto + // it. If that happens, then the poles become singularities which map the entire top and bottom rows of the texture + // onto a single point. In general there's no real way to do that right. But to match the behavior of non-geodesic + // spheres, we need to duplicate the pole vertex for every triangle that uses it. This will introduce seams near the + // poles, but reduce stretching. + auto fixPole = [&](size_t poleIndex) + { + auto poleVertex = vertices[poleIndex]; + bool overwrittenPoleVertex = false; // overwriting the original pole vertex saves us one vertex + + for (size_t i = 0; i < indices.size(); i += 3) + { + // These pointers point to the three indices which make up this triangle. pPoleIndex is the pointer to the + // entry in the index array which represents the pole index, and the other two pointers point to the other + // two indices making up this triangle. + uint16_t* pPoleIndex; + uint16_t* pOtherIndex0; + uint16_t* pOtherIndex1; + if (indices[i + 0] == poleIndex) + { + pPoleIndex = &indices[i + 0]; + pOtherIndex0 = &indices[i + 1]; + pOtherIndex1 = &indices[i + 2]; + } + else if (indices[i + 1] == poleIndex) + { + pPoleIndex = &indices[i + 1]; + pOtherIndex0 = &indices[i + 2]; + pOtherIndex1 = &indices[i + 0]; + } + else if (indices[i + 2] == poleIndex) + { + pPoleIndex = &indices[i + 2]; + pOtherIndex0 = &indices[i + 0]; + pOtherIndex1 = &indices[i + 1]; + } + else + { + continue; + } + + const auto& otherVertex0 = vertices[*pOtherIndex0]; + const auto& otherVertex1 = vertices[*pOtherIndex1]; + + // Calculate the texcoords for the new pole vertex, add it to the vertices and update the index + VertexPositionNormalTexture newPoleVertex = poleVertex; + newPoleVertex.textureCoordinate.x = (otherVertex0.textureCoordinate.x + otherVertex1.textureCoordinate.x) / 2; + newPoleVertex.textureCoordinate.y = poleVertex.textureCoordinate.y; + + if (!overwrittenPoleVertex) + { + vertices[poleIndex] = newPoleVertex; + overwrittenPoleVertex = true; + } + else + { + CheckIndexOverflow(vertices.size()); + + *pPoleIndex = static_cast(vertices.size()); + vertices.push_back(newPoleVertex); + } + } + }; + + fixPole(northPoleIndex); + fixPole(southPoleIndex); + + // Create the primitive object. + std::unique_ptr primitive(new GeometricPrimitive()); + + primitive->pImpl->Initialize(deviceContext, vertices, indices); + return primitive; +} + + // Helper computes a point on a unit circle, aligned to the x/z plane and centered on the origin. static XMVECTOR GetCircleVector(size_t i, size_t tessellation) {