Skip to content

Commit

Permalink
Added CreateGeoSphere
Browse files Browse the repository at this point in the history
  • Loading branch information
walbourn_cp authored and walbourn_cp committed Oct 2, 2012
1 parent 7386715 commit 84a2ce3
Show file tree
Hide file tree
Showing 2 changed files with 342 additions and 9 deletions.
11 changes: 6 additions & 5 deletions Inc/GeometricPrimitive.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ namespace DirectX
~GeometricPrimitive();

// Factory methods.
static std::unique_ptr<GeometricPrimitive> CreateCube (_In_ ID3D11DeviceContext* deviceContext, float size = 1);
static std::unique_ptr<GeometricPrimitive> CreateSphere (_In_ ID3D11DeviceContext* deviceContext, float diameter = 1, size_t tessellation = 16);
static std::unique_ptr<GeometricPrimitive> CreateCylinder(_In_ ID3D11DeviceContext* deviceContext, float height = 1, float diameter = 1, size_t tessellation = 32);
static std::unique_ptr<GeometricPrimitive> CreateTorus (_In_ ID3D11DeviceContext* deviceContext, float diameter = 1, float thickness = 0.333f, size_t tessellation = 32);
static std::unique_ptr<GeometricPrimitive> CreateTeapot (_In_ ID3D11DeviceContext* deviceContext, float size = 1, size_t tessellation = 8);
static std::unique_ptr<GeometricPrimitive> CreateCube (_In_ ID3D11DeviceContext* deviceContext, float size = 1);
static std::unique_ptr<GeometricPrimitive> CreateSphere (_In_ ID3D11DeviceContext* deviceContext, float diameter = 1, size_t tessellation = 16);
static std::unique_ptr<GeometricPrimitive> CreateGeoSphere(_In_ ID3D11DeviceContext* deviceContext, float diameter = 1, size_t tessellation = 3);
static std::unique_ptr<GeometricPrimitive> CreateCylinder (_In_ ID3D11DeviceContext* deviceContext, float height = 1, float diameter = 1, size_t tessellation = 32);
static std::unique_ptr<GeometricPrimitive> CreateTorus (_In_ ID3D11DeviceContext* deviceContext, float diameter = 1, float thickness = 0.333f, size_t tessellation = 32);
static std::unique_ptr<GeometricPrimitive> 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<void()> setCustomState = nullptr);
Expand Down
340 changes: 336 additions & 4 deletions Src/GeometricPrimitive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,22 @@
#include "SharedResourcePool.h"
#include "Bezier.h"
#include <vector>
#include <map>

using namespace DirectX;
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<VertexPositionNormalTexture> VertexCollection;

Expand All @@ -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);
}
};
Expand Down Expand Up @@ -417,6 +423,332 @@ std::unique_ptr<GeometricPrimitive> GeometricPrimitive::CreateSphere(_In_ ID3D11
}


// Creates a geosphere primitive.
std::unique_ptr<GeometricPrimitive> 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<uint16_t, uint16_t> 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<UndirectedEdge, uint16_t> 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<XMFLOAT3> 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<uint32_t>( 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<uint16_t>(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<uint16_t>(vertices.size());
vertices.push_back(newPoleVertex);
}
}
};

fixPole(northPoleIndex);
fixPole(southPoleIndex);

// Create the primitive object.
std::unique_ptr<GeometricPrimitive> 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)
{
Expand Down

0 comments on commit 84a2ce3

Please sign in to comment.