53.도형 예제
구(sphere)와 원기둥(cylinder)은 하늘 돔(sky dome) 그리기, 디버깅, 충돌 검출의 시각확, 지연 렌더링 등에 유용하다. 이번 예제는 여러 개의 물체를 배치해서 그리는 방법, 기하구조 전체를 하나의 커다란 정점 버퍼와 인덱스 버퍼에 담고, DrawIndexed 메소드를 이용해서 하나씩 그리는 방법을 볼 수 있다.
1.원기둥 메시 생성
원기둥은 밑면 반지름과 윗면 반지름, 높이, 그리고 조각(slice) 개수와 더미(stack) 개수로 정의된다.

더미와 조각이란 삼각형의 밀도를 의미한다. 즉, 조각이 많을 수록 윗면과 밑면의 삼각형의 수가 많으므로 원에 가까워지고, 더미가 많을 수록 각 더미가 갖는 높이가 줄어든다. 위 그림에서 조각은 8개 더미는 4개이다.
원기둥 옆면 기하구조의 코드는 아래와 같다.
void GeometryGenerator::CreateCylinder(float bottomRadius, float topRadius, float height, UINT sliceCount, UINT stackCount, MeshData& meshData)
{
meshData.Vertices.clear();
meshData.Indices.clear();
//
// Build Stacks.
//
float stackHeight = height / stackCount;
// Amount to increment radius as we move up each stack level from bottom to top.
float radiusStep = (topRadius - bottomRadius) / stackCount;
UINT ringCount = stackCount+1;
// Compute vertices for each stack ring starting at the bottom and moving up.
for(UINT i = 0; i < ringCount; ++i)
{
float y = -0.5f*height + i*stackHeight;
float r = bottomRadius + i*radiusStep;
// vertices of ring
float dTheta = 2.0f*XM_PI/sliceCount;
for(UINT j = 0; j <= sliceCount; ++j)
{
Vertex vertex;
float c = cosf(j*dTheta);
float s = sinf(j*dTheta);
vertex.Position = XMFLOAT3(r*c, y, r*s);
vertex.TexC.x = (float)j/sliceCount;
vertex.TexC.y = 1.0f - (float)i/stackCount;
// Cylinder can be parameterized as follows, where we introduce v
// parameter that goes in the same direction as the v tex-coord
// so that the bitangent goes in the same direction as the v tex-coord.
// Let r0 be the bottom radius and let r1 be the top radius.
// y(v) = h - hv for v in [0,1].
// r(v) = r1 + (r0-r1)v
//
// x(t, v) = r(v)*cos(t)
// y(t, v) = h - hv
// z(t, v) = r(v)*sin(t)
//
// dx/dt = -r(v)*sin(t)
// dy/dt = 0
// dz/dt = +r(v)*cos(t)
//
// dx/dv = (r0-r1)*cos(t)
// dy/dv = -h
// dz/dv = (r0-r1)*sin(t)
// This is unit length.
vertex.TangentU = XMFLOAT3(-s, 0.0f, c);
float dr = bottomRadius-topRadius;
XMFLOAT3 bitangent(dr*c, -height, dr*s);
XMVECTOR T = XMLoadFloat3(&vertex.TangentU);
XMVECTOR B = XMLoadFloat3(&bitangent);
XMVECTOR N = XMVector3Normalize(XMVector3Cross(T, B));
XMStoreFloat3(&vertex.Normal, N);
meshData.Vertices.push_back(vertex);
}
}
// Add one because we duplicate the first and last vertex per ring
// since the texture coordinates are different.
UINT ringVertexCount = sliceCount+1;
// Compute indices for each stack.
for(UINT i = 0; i < stackCount; ++i)
{
for(UINT j = 0; j < sliceCount; ++j)
{
meshData.Indices.push_back(i*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j+1);
meshData.Indices.push_back(i*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j+1);
meshData.Indices.push_back(i*ringVertexCount + j+1);
}
}
BuildCylinderTopCap(bottomRadius, topRadius, height, sliceCount, stackCount, meshData);
BuildCylinderBottomCap(bottomRadius, topRadius, height, sliceCount, stackCount, meshData);
}
CreateCylinder 함수의 매개변수인 stackCount는 더미의 개수 + 1이다. 즉 높이를 정점의 개수이다. 원기둥은 팬케이크을 쌓는 형식으로 구현되어 있다. 즉, stackCount는 팬케이크의 수라고 봐도 된다. 이웃한 팬케이크의 반지름 차이는 다음과 같다.

즉, 최하단 팬케이크이 0번째라고 할 때, i번째 팬케이크의 반지름은

i번째 팬케이크의 y좌표 즉 높이는

하나의 더미는 여러 개의 사각형들로 이루어져 있다. 사각형은 삼각형 두 개로 이루어져 있다. i번째 더미의 j번째 조각의 인덱스를 구해보면 다음과 같은 공식을 얻을 수 있다.

n은 한 팬케이크의 정점의 개수이다.
윗면, 밑면의 기하구조는 원을 최상단, 최하단 팬케이크의 정점들로 근사한 것이다. 윗, 밑면 기하구조의 코드는 다음과 같다.
void GeometryGenerator::BuildCylinderTopCap(float bottomRadius, float topRadius, float height, UINT sliceCount, UINT stackCount, MeshData& meshData)
{
UINT baseIndex = (UINT)meshData.Vertices.size();
float y = 0.5f*height;
float dTheta = 2.0f*XM_PI/sliceCount;
// Duplicate cap ring vertices because the texture coordinates and normals differ.
for(UINT i = 0; i <= sliceCount; ++i)
{
float x = topRadius*cosf(i*dTheta);
float z = topRadius*sinf(i*dTheta);
// Scale down by the height to try and make top cap texture coord area
// proportional to base.
float u = x/height + 0.5f;
float v = z/height + 0.5f;
meshData.Vertices.push_back( Vertex(x, y, z, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, u, v) );
}
// Cap center vertex.
meshData.Vertices.push_back( Vertex(0.0f, y, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f) );
// Index of center vertex.
UINT centerIndex = (UINT)meshData.Vertices.size()-1;
for(UINT i = 0; i < sliceCount; ++i)
{
meshData.Indices.push_back(centerIndex);
meshData.Indices.push_back(baseIndex + i+1);
meshData.Indices.push_back(baseIndex + i);
}
}
void GeometryGenerator::BuildCylinderBottomCap(float bottomRadius, float topRadius, float height, UINT sliceCount, UINT stackCount, MeshData& meshData)
{
UINT baseIndex = (UINT)meshData.Vertices.size();
float y = -0.5f*height;
// vertices of ring
float dTheta = 2.0f*XM_PI/sliceCount;
for(UINT i = 0; i <= sliceCount; ++i)
{
float x = bottomRadius*cosf(i*dTheta);
float z = bottomRadius*sinf(i*dTheta);
// Scale down by the height to try and make top cap texture coord area
// proportional to base.
float u = x/height + 0.5f;
float v = z/height + 0.5f;
meshData.Vertices.push_back( Vertex(x, y, z, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, u, v) );
}
// Cap center vertex.
meshData.Vertices.push_back( Vertex(0.0f, y, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f) );
// Cache the index of center vertex.
UINT centerIndex = (UINT)meshData.Vertices.size()-1;
for(UINT i = 0; i < sliceCount; ++i)
{
meshData.Indices.push_back(centerIndex);
meshData.Indices.push_back(baseIndex + i);
meshData.Indices.push_back(baseIndex + i+1);
}
}
2.구 메시의 생성
구를 반지름과 조각 개수 및 더미 개수로 정의한다. 방식은 원기둥 생성과 비슷하다. 차이점으로 밑면과 밑면이 없고, 옆면의 반지름이 늘거나 줄기만 하는 것이 아닌, 늘었다가 줄었다로 구현하면 될 것이다. 즉, 팬케이크의 반지름이 삼각함수에 따라 비선형으로 변한다는 것이다.

구 기하구조의 코드는 다음과 같다.
void GeometryGenerator::CreateSphere(float radius, UINT sliceCount, UINT stackCount, MeshData& meshData)
{
meshData.Vertices.clear();
meshData.Indices.clear();
//
// Compute the vertices stating at the top pole and moving down the stacks.
//
// Poles: note that there will be texture coordinate distortion as there is
// not a unique point on the texture map to assign to the pole when mapping
// a rectangular texture onto a sphere.
Vertex topVertex(0.0f, +radius, 0.0f, 0.0f, +1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f);
Vertex bottomVertex(0.0f, -radius, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f);
meshData.Vertices.push_back( topVertex );
float phiStep = XM_PI/stackCount;
float thetaStep = 2.0f*XM_PI/sliceCount;
// Compute vertices for each stack ring (do not count the poles as rings).
for(UINT i = 1; i <= stackCount-1; ++i)
{
float phi = i*phiStep;
// Vertices of ring.
for(UINT j = 0; j <= sliceCount; ++j)
{
float theta = j*thetaStep;
Vertex v;
// spherical to cartesian
v.Position.x = radius*sinf(phi)*cosf(theta);
v.Position.y = radius*cosf(phi);
v.Position.z = radius*sinf(phi)*sinf(theta);
// Partial derivative of P with respect to theta
v.TangentU.x = -radius*sinf(phi)*sinf(theta);
v.TangentU.y = 0.0f;
v.TangentU.z = +radius*sinf(phi)*cosf(theta);
XMVECTOR T = XMLoadFloat3(&v.TangentU);
XMStoreFloat3(&v.TangentU, XMVector3Normalize(T));
XMVECTOR p = XMLoadFloat3(&v.Position);
XMStoreFloat3(&v.Normal, XMVector3Normalize(p));
v.TexC.x = theta / XM_2PI;
v.TexC.y = phi / XM_PI;
meshData.Vertices.push_back( v );
}
}
meshData.Vertices.push_back( bottomVertex );
//
// Compute indices for top stack. The top stack was written first to the vertex buffer
// and connects the top pole to the first ring.
//
for(UINT i = 1; i <= sliceCount; ++i)
{
meshData.Indices.push_back(0);
meshData.Indices.push_back(i+1);
meshData.Indices.push_back(i);
}
//
// Compute indices for inner stacks (not connected to poles).
//
// Offset the indices to the index of the first vertex in the first ring.
// This is just skipping the top pole vertex.
UINT baseIndex = 1;
UINT ringVertexCount = sliceCount+1;
for(UINT i = 0; i < stackCount-2; ++i)
{
for(UINT j = 0; j < sliceCount; ++j)
{
meshData.Indices.push_back(baseIndex + i*ringVertexCount + j);
meshData.Indices.push_back(baseIndex + i*ringVertexCount + j+1);
meshData.Indices.push_back(baseIndex + (i+1)*ringVertexCount + j);
meshData.Indices.push_back(baseIndex + (i+1)*ringVertexCount + j);
meshData.Indices.push_back(baseIndex + i*ringVertexCount + j+1);
meshData.Indices.push_back(baseIndex + (i+1)*ringVertexCount + j+1);
}
}
//
// Compute indices for bottom stack. The bottom stack was written last to the vertex buffer
// and connects the bottom pole to the bottom ring.
//
// South pole vertex was added last.
UINT southPoleIndex = (UINT)meshData.Vertices.size()-1;
// Offset the indices to the index of the first vertex in the last ring.
baseIndex = southPoleIndex - ringVertexCount;
for(UINT i = 0; i < sliceCount; ++i)
{
meshData.Indices.push_back(southPoleIndex);
meshData.Indices.push_back(baseIndex+i);
meshData.Indices.push_back(baseIndex+i+1);
}
}
3.측지구 메시의 생성
측지구(geosphere)란 구를 이루고 있는 삼각형의 크기와 변의 길이가 거의 같은 삼각형들로 이뤄진 구를 말한다. 구의 경우 각 팬케이크의 삼각형의 크기가 일정하지 않는다. 53.2챕터의 구 그림만 봐도 구와 측지구의 차이점은 이해가 된다.

측지구를 생성할 때에는 정이십면체에서 출발해서 각 삼각형을 세분화하고, 새분화한 새로 생긴 정점들을 반지름의 구에 투영하고, 이 과정을 반복해서 테셀레이션 수준을 높여 나가면 실제 구에 가까운 측지구 메시가 만들어진다.

측지구 기하구조의 코드는 다음과 같다.
void GeometryGenerator::CreateGeosphere(float radius, UINT numSubdivisions, MeshData& meshData)
{
// Put a cap on the number of subdivisions.
numSubdivisions = MathHelper::Min(numSubdivisions, 5u);
// Approximate a sphere by tessellating an icosahedron.
const float X = 0.525731f;
const float Z = 0.850651f;
XMFLOAT3 pos[12] =
{
XMFLOAT3(-X, 0.0f, Z), XMFLOAT3(X, 0.0f, Z),
XMFLOAT3(-X, 0.0f, -Z), XMFLOAT3(X, 0.0f, -Z),
XMFLOAT3(0.0f, Z, X), XMFLOAT3(0.0f, Z, -X),
XMFLOAT3(0.0f, -Z, X), XMFLOAT3(0.0f, -Z, -X),
XMFLOAT3(Z, X, 0.0f), XMFLOAT3(-Z, X, 0.0f),
XMFLOAT3(Z, -X, 0.0f), XMFLOAT3(-Z, -X, 0.0f)
};
DWORD k[60] =
{
1,4,0, 4,9,0, 4,5,9, 8,5,4, 1,8,4,
1,10,8, 10,3,8, 8,3,5, 3,2,5, 3,7,2,
3,10,7, 10,6,7, 6,11,7, 6,0,11, 6,1,0,
10,1,6, 11,0,9, 2,11,9, 5,2,9, 11,2,7
};
meshData.Vertices.resize(12);
meshData.Indices.resize(60);
for(UINT i = 0; i < 12; ++i)
meshData.Vertices[i].Position = pos[i];
for(UINT i = 0; i < 60; ++i)
meshData.Indices[i] = k[i];
for(UINT i = 0; i < numSubdivisions; ++i)
Subdivide(meshData);
// Project vertices onto sphere and scale.
for(UINT i = 0; i < meshData.Vertices.size(); ++i)
{
// Project onto unit sphere.
XMVECTOR n = XMVector3Normalize(XMLoadFloat3(&meshData.Vertices[i].Position));
// Project onto sphere.
XMVECTOR p = radius*n;
XMStoreFloat3(&meshData.Vertices[i].Position, p);
XMStoreFloat3(&meshData.Vertices[i].Normal, n);
// Derive texture coordinates from spherical coordinates.
float theta = MathHelper::AngleFromXY(
meshData.Vertices[i].Position.x,
meshData.Vertices[i].Position.z);
float phi = acosf(meshData.Vertices[i].Position.y / radius);
meshData.Vertices[i].TexC.x = theta/XM_2PI;
meshData.Vertices[i].TexC.y = phi/XM_PI;
// Partial derivative of P with respect to theta
meshData.Vertices[i].TangentU.x = -radius*sinf(phi)*sinf(theta);
meshData.Vertices[i].TangentU.y = 0.0f;
meshData.Vertices[i].TangentU.z = +radius*sinf(phi)*cosf(theta);
XMVECTOR T = XMLoadFloat3(&meshData.Vertices[i].TangentU);
XMStoreFloat3(&meshData.Vertices[i].TangentU, XMVector3Normalize(T));
}
}