Company
교육 철학

메쉬(Mesh) 클래스

개요

Mesh(메쉬)는 3D 오브젝트의 형태를 정의하는 기하학적 데이터입니다. 정점(Vertex)과 삼각형(Triangle)의 집합으로 구성되며, 3D 그래픽의 가장 기본적인 구성 요소입니다. 이 문서에서는 폴리곤 메쉬의 개념과 효율적인 표현 방법, 그리고 Mesh 클래스 구현을 다룹니다.

Part 1: 폴리곤 메쉬의 개념

폴리곤 메쉬(Polygon Mesh)란?

이러한 객체는 여러 기법으로 구성될 수 있지만, 게임과 같은 실시간으로 처리되는 물체들은 거의 대부분 폴리곤(polygon) 모델링 기법을 사용합니다. (GPU가 폴리곤 메쉬 처리에 최적화되어 있기 때문입니다.)
폴리곤의 의미
원래 뜻: 다각형 (Polygon)
컴퓨터 그래픽스: 주로 삼각형(Triangle)을 의미
삼각형은 평면을 보장하는 가장 단순한 도형
여러 개의 폴리곤으로 구성된 하나의 객체를 폴리곤 메쉬라고 합니다. 그림에서 볼 수 있듯이 더 많은 폴리곤을 사용할수록 물체를 더욱 정교하게 표현할 수 있습니다. 이러한 폴리곤 메쉬는 정확한 곡면이 아닌 곡면에 근사한 표현이라는 것을 알아두자.

폴리곤 메쉬의 특징

실시간 렌더링에 최적
GPU의 하드웨어 가속 지원
고정된 렌더링 파이프라인에 적합
빠른 래스터화(Rasterization) 처리
근사적 표현
곡면을 평면 삼각형의 조합으로 표현
폴리곤 수가 많을수록 정교함
LOD (Level of Detail)로 거리에 따라 조절
다양한 활용
캐릭터 모델
환경 오브젝트
UI 요소
파티클 시스템

Part 2: 폴리곤 메쉬를 표현하는 방법

정점(Vertex)과 버퍼

3ds Max, Maya와 같은 프로그램을 통해 그래픽 아티스트가 만든 폴리곤 메쉬를 파일로서 저장하면 게임의 입력으로 사용할 수 있습니다. 이러한 폴리곤 메쉬를 컴퓨터 프로그램에선 정점(Vertex)의 집합으로 표현합니다.
정점 버퍼(Vertex Buffer)
정점 데이터를 저장하는 GPU 메모리 공간
위치, 법선, UV 좌표, 색상 등 포함
연속된 메모리 블록으로 효율적 접근
예를 들어 폴리곤이 1개면 정점(꼭지점)이 3개이므로 3개의 점을 저장함으로써 폴리곤을 표현합니다. 이처럼 정점을 저장하는 공간을 Direct3D에선 정점 버퍼라고 합니다. 다음은 폴리곤 3개로 구성된 폴리곤 메쉬와 그에 대응하는 정점 버퍼입니다.

방법 1: Triangle List (삼각형 리스트)

그런데 위 정점 버퍼엔 비효율적인 부분이 존재합니다. 삼각형이 3개이기 때문에 총 9개의 정점이 존재하는 것은 맞지만 삼각형이 서로 접해있기 때문에 중복되는 정점이 존재합니다.
이렇게 중복이 되더라도 하나의 정점 버퍼로 각 삼각형을 표현하는 방법을 삼각형 리스트(Triangle List)라고 합니다.
장점
간단한 구조: 연속으로 3개의 정점을 읽어 하나의 삼각형 구성
독립적인 삼각형: 각 삼각형이 서로 영향을 주지 않음
편집 용이: 삼각형 단위로 추가/삭제 가능
단점
중복되는 정점들에 의해 메모리 낭비
전송해야 할 데이터 양 증가
캐시 효율 낮음
사용 사례
서로 떨어진 삼각형들
동적으로 생성되는 지오메트리
파티클 시스템

방법 2: Indexed Triangle List (인덱스 삼각형 리스트)

이러한 메모리 낭비를 막기 위해 2가지의 버퍼를 사용하는 방법이 있습니다. 바로 다음과 같이 정점 버퍼와 인덱스 버퍼를 이용하여 중복되는 정점 저장을 막습니다. 이러한 방식을 인덱스 삼각형 리스트(Indexed Triangle List)라고 합니다.
동작 원리
1.
정점 버퍼: 유일한(unique) 정점을 모두 저장
2.
인덱스 버퍼: 정점 버퍼의 인덱스를 이용하여 삼각형의 세 정점을 연속으로 저장
메모리 비교
버퍼가 2개라 오히려 더 많은 메모리를 사용하는 것이 아니냐고 생각할 수 있지만, 실제로는 더 효율적입니다:
정점 데이터 크기
struct Vertex { Vector3 position; // 12 bytes (float × 3) Vector3 normal; // 12 bytes Vector2 uv; // 8 bytes Vector4 color; // 16 bytes // 총 48 bytes per vertex };
C++
복사
인덱스 데이터 크기
UINT (4 bytes) 또는 USHORT (2 bytes)
정점 데이터보다 훨씬 작음
비교 예시 (삼각형 3개, 정점 5개)
정점은 부동 소수점을 이용하고 실제론 2차원 좌표가 아닌 3차원 좌표를 저장하게 됩니다. 그에 비해 인덱스 버퍼의 각 요소는 정수 값이기 때문에 실제로 더 적은 메모리 공간을 사용하는 것이 맞습니다.
추가 이점
정점 캐시 효율: GPU가 최근 처리한 정점을 캐시에 보관
대역폭 절약: GPU로 전송할 데이터 양 감소
변환 연산 감소: 같은 정점을 여러 번 변환하지 않음
그리고 정점 버퍼엔 실제로 정점만 저장되는 것은 아닙니다. 정점 외에도 vertex normal(정점의 법선 벡터), 텍스처 좌표 등 다양한 데이터가 저장됩니다.

방법 3: Triangle Strip (삼각형 스트립)

또 다른 방법으로 삼각형 스트립(Triangle Strip)이 있습니다. 삼각형 스트립은 다음과 같이 정점 버퍼 하나만 이용합니다.
인덱스 버퍼를 사용하지 않는 삼각형 리스트와 다른 점
중복되는 정점을 저장하지 않음
정점을 저장하는 순서가 중요
동작 원리
1.
t1 삼각형 렌더링: 첫 세 정점(v0, v1, v2)을 처리 후 캐시에 저장
2.
t2 삼각형 렌더링: 첫 두 정점(v1, v2)은 캐시에서 읽고, 마지막 정점(v3)만 정점 버퍼에서 가져와 처리
3.
t3 삼각형 렌더링: (v2, v3, v4) 동일한 방식으로 처리
정점 순서 규칙
장점
최소한의 정점 데이터
최고의 캐시 효율
대역폭 최소화
단점
정점 순서 제약
연결되지 않은 삼각형 표현 어려움
복잡한 메쉬에 적용 제한적
사용 사례
지형 메쉬 (Terrain)
리본 효과 (Ribbon)
긴 연속된 구조물

세 가지 방법 비교

실무 권장 사항
일반 메쉬: Indexed Triangle List (가장 균형적)
동적 메쉬: Triangle List (유연성 우선)
특수 메쉬: Triangle Strip (성능 극대화)
인덱스 삼각형 리스트와 삼각형 스트립이 가장 많이 사용됩니다.

Part 3: Mesh 클래스 설계

설계 철학

앞서 설명한 내용에 맞추어 이전에 구현한 Vertex Buffer, Index Buffer 클래스를 Mesh 클래스 안에 감싸서 구현했습니다. 또한 정점 정보를 어떻게 그릴 것인지에 대한 Primitive Topology 정보도 추가해주었습니다.
Mesh 클래스의 역할
정점 버퍼와 인덱스 버퍼의 관리
프리미티브 토폴로지 설정
렌더링 파이프라인에 바인딩
메쉬 데이터의 저장 및 로드

Mesh 클래스 구조

#pragma once #include "yaResource.h" #include "yaVertexBuffer.h" #include "yaIndexBuffer.h" namespace ya { class Mesh : public Resource { public: struct Data { Data(); ~Data(); D3D11_PRIMITIVE_TOPOLOGY mTopology; std::vector<graphics::Vertex> vertices; std::vector<UINT> indices; }; Mesh(); ~Mesh(); virtual HRESULT Save(const std::wstring& path) override; virtual HRESULT Load(const std::wstring& path) override; bool CreateVB(const std::vector<graphics::Vertex>& vertices); bool CreateIB(const std::vector<UINT>& indices); void Bind(); private: graphics::VertexBuffer mVB; graphics::IndexBuffer mIB; Data mData; }; }
C++
복사

클래스 멤버 상세 설명

Data 구조체
또한 정점 데이터와 인덱스 데이터를 따로 저장해서 보관해두었습니다. 나중에 수정하여 사용 가능할 수도 있기 때문입니다.
struct Data { D3D11_PRIMITIVE_TOPOLOGY mTopology; std::vector<graphics::Vertex> vertices; std::vector<UINT> indices; };
C++
복사
각 멤버의 역할
mTopology (프리미티브 토폴로지)
정점들을 어떻게 연결하여 도형을 만들지 정의
Triangle List, Triangle Strip, Line List 등
렌더링 방식 결정
vertices (정점 배열)
CPU 메모리에 저장된 정점 데이터 백업
수정, 재생성, 저장/로드에 사용
GPU의 Vertex Buffer 생성 소스
indices (인덱스 배열)
CPU 메모리에 저장된 인덱스 데이터 백업
삼각형 구성 정보 보관
GPU의 Index Buffer 생성 소스
데이터 보관 이유
런타임 수정 가능
메쉬 저장/로드 구현
물리 엔진 연동 (충돌 검사)
디버깅 및 시각화
VertexBuffer mVB와 IndexBuffer mIB
GPU 메모리에 실제로 할당된 버퍼
렌더링 파이프라인에 바인딩되는 객체
Data의 정점/인덱스로부터 생성

Part 4: Mesh 클래스 구현

생성자와 소멸자

Mesh::Data::Data() : mTopology(D3D11_PRIMITIVE_TOPOLOGY::D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST) , vertices{} , indices{} { } Mesh::Data::~Data() { } Mesh::Mesh() : Resource(enums::eResourceType::Mesh) { } Mesh::~Mesh() { }
C++
복사
기본 토폴로지
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST: 가장 일반적인 삼각형 리스트
안정적이고 범용적인 선택
필요 시 다른 토폴로지로 변경 가능

CreateVB 함수 (정점 버퍼 생성)

bool Mesh::CreateVB(const std::vector<graphics::Vertex>& vertices) { mData.vertices = vertices; return mVB.Create(vertices); }
C++
복사
동작 과정
1.
CPU 데이터 백업: mData.vertices에 정점 배열 저장
2.
GPU 버퍼 생성: mVB.Create()로 Vertex Buffer 생성
3.
메모리 전송: CPU 메모리의 정점 데이터를 GPU로 복사
VertexBuffer::Create 내부 동작
bool VertexBuffer::Create(const std::vector<Vertex>& vertices) { D3D11_BUFFER_DESC bufferDesc = {}; bufferDesc.ByteWidth = sizeof(Vertex) * vertices.size(); bufferDesc.Usage = D3D11_USAGE_DEFAULT; bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; D3D11_SUBRESOURCE_DATA initData = {}; initData.pSysMem = vertices.data(); return SUCCEEDED(GetDevice()->GetID3D11Device()->CreateBuffer( &bufferDesc, &initData, mBuffer.GetAddressOf() )); }
C++
복사

CreateIB 함수 (인덱스 버퍼 생성)

bool Mesh::CreateIB(const std::vector<UINT>& indices) { mData.indices = indices; return mIB.Create(indices); }
C++
복사
동작 과정
1.
CPU 데이터 백업: mData.indices에 인덱스 배열 저장
2.
GPU 버퍼 생성: mIB.Create()로 Index Buffer 생성
3.
메모리 전송: CPU 메모리의 인덱스 데이터를 GPU로 복사
IndexBuffer::Create 내부 동작
bool IndexBuffer::Create(const std::vector<UINT>& indices) { D3D11_BUFFER_DESC bufferDesc = {}; bufferDesc.ByteWidth = sizeof(UINT) * indices.size(); bufferDesc.Usage = D3D11_USAGE_DEFAULT; bufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER; D3D11_SUBRESOURCE_DATA initData = {}; initData.pSysMem = indices.data(); return SUCCEEDED(GetDevice()->GetID3D11Device()->CreateBuffer( &bufferDesc, &initData, mBuffer.GetAddressOf() )); }
C++
복사

Bind 함수 (파이프라인 바인딩)

Bind에는 정점, 인덱스, 프리미티브 토폴로지 3가지 정보를 파이프라인에 묶을 수 있도록 설계되었습니다.
void Mesh::Bind() { mVB.Bind(); mIB.Bind(); graphics::GetDevice()->BindPrimitiveTopology(mData.mTopology); }
C++
복사
바인딩 순서와 역할
1. Vertex Buffer 바인딩
void VertexBuffer::Bind() { UINT stride = sizeof(Vertex); UINT offset = 0; GetDevice()->GetContext()->IASetVertexBuffers( 0, // 시작 슬롯 1, // 버퍼 개수 mBuffer.GetAddressOf(), // 버퍼 포인터 &stride, // 정점 크기 &offset // 오프셋 ); }
C++
복사
2. Index Buffer 바인딩
void IndexBuffer::Bind() { GetDevice()->GetContext()->IASetIndexBuffer( mBuffer.Get(), // 버퍼 DXGI_FORMAT_R32_UINT, // 인덱스 포맷 (UINT) 0 // 오프셋 ); }
C++
복사
3. Primitive Topology 설정
void GraphicDevice_DX11::BindPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY topology) { mContext->IASetPrimitiveTopology(topology); }
C++
복사
바인딩 후 상태
Input Assembler (IA) 스테이지 설정 완료
Draw Call 준비 완료
실제 렌더링은 DrawIndexed() 호출 시 시작

Save와 Load 함수

HRESULT Mesh::Save(const std::wstring& path) { return S_OK; } HRESULT Mesh::Load(const std::wstring& path) { return S_OK; }
C++
복사
현재 상태
기본 틀만 구현됨
실제 저장/로드 로직은 미구현
구현 가이드
Save 구현 예시
HRESULT Mesh::Save(const std::wstring& path) { std::ofstream file(path, std::ios::binary); if (!file.is_open()) return E_FAIL; // 1. 토폴로지 저장 file.write((char*)&mData.mTopology, sizeof(D3D11_PRIMITIVE_TOPOLOGY)); // 2. 정점 개수 및 데이터 저장 size_t vertexCount = mData.vertices.size(); file.write((char*)&vertexCount, sizeof(size_t)); file.write((char*)mData.vertices.data(), sizeof(Vertex) * vertexCount); // 3. 인덱스 개수 및 데이터 저장 size_t indexCount = mData.indices.size(); file.write((char*)&indexCount, sizeof(size_t)); file.write((char*)mData.indices.data(), sizeof(UINT) * indexCount); file.close(); return S_OK; }
C++
복사
Load 구현 예시
HRESULT Mesh::Load(const std::wstring& path) { std::ifstream file(path, std::ios::binary); if (!file.is_open()) return E_FAIL; // 1. 토폴로지 로드 file.read((char*)&mData.mTopology, sizeof(D3D11_PRIMITIVE_TOPOLOGY)); // 2. 정점 로드 size_t vertexCount; file.read((char*)&vertexCount, sizeof(size_t)); mData.vertices.resize(vertexCount); file.read((char*)mData.vertices.data(), sizeof(Vertex) * vertexCount); // 3. 인덱스 로드 size_t indexCount; file.read((char*)&indexCount, sizeof(size_t)); mData.indices.resize(indexCount); file.read((char*)mData.indices.data(), sizeof(UINT) * indexCount); file.close(); // 4. GPU 버퍼 생성 CreateVB(mData.vertices); CreateIB(mData.indices); return S_OK; }
C++
복사

Part 5: Mesh 사용 예시

기본 도형 생성

사각형 메쉬
Mesh* CreateRectMesh() { std::vector<Vertex> vertices; vertices.resize(4); // 정점 위치 및 UV 설정 vertices[0].position = Vector3(-0.5f, -0.5f, 0.0f); vertices[0].uv = Vector2(0.0f, 1.0f); vertices[1].position = Vector3(0.5f, -0.5f, 0.0f); vertices[1].uv = Vector2(1.0f, 1.0f); vertices[2].position = Vector3(0.5f, 0.5f, 0.0f); vertices[2].uv = Vector2(1.0f, 0.0f); vertices[3].position = Vector3(-0.5f, 0.5f, 0.0f); vertices[3].uv = Vector2(0.0f, 0.0f); // 인덱스 (2개의 삼각형) std::vector<UINT> indices = { 0, 1, 2, // 첫 번째 삼각형 0, 2, 3 // 두 번째 삼각형 }; Mesh* mesh = new Mesh(); mesh->CreateVB(vertices); mesh->CreateIB(indices); return mesh; }
C++
복사
큐브 메쉬
Mesh* CreateCubeMesh() { std::vector<Vertex> vertices; vertices.resize(24); // 6면 × 4정점 // 앞면 vertices[0].position = Vector3(-0.5f, -0.5f, -0.5f); vertices[1].position = Vector3(0.5f, -0.5f, -0.5f); vertices[2].position = Vector3(0.5f, 0.5f, -0.5f); vertices[3].position = Vector3(-0.5f, 0.5f, -0.5f); // ... 나머지 5개 면 설정 // 인덱스 (6면 × 2삼각형 × 3인덱스 = 36개) std::vector<UINT> indices = { 0, 1, 2, 0, 2, 3, // 앞면 4, 5, 6, 4, 6, 7, // 뒷면 // ... 나머지 4개 면 }; Mesh* mesh = new Mesh(); mesh->CreateVB(vertices); mesh->CreateIB(indices); return mesh; }
C++
복사

동적 메쉬 생성

절차적 지오메트리
Mesh* CreateGridMesh(int width, int height) { std::vector<Vertex> vertices; std::vector<UINT> indices; // 정점 생성 for (int y = 0; y <= height; y++) { for (int x = 0; x <= width; x++) { Vertex v; v.position = Vector3( (float)x / width - 0.5f, (float)y / height - 0.5f, 0.0f ); v.uv = Vector2((float)x / width, (float)y / height); vertices.push_back(v); } } // 인덱스 생성 for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int topLeft = y * (width + 1) + x; int topRight = topLeft + 1; int bottomLeft = (y + 1) * (width + 1) + x; int bottomRight = bottomLeft + 1; // 첫 번째 삼각형 indices.push_back(topLeft); indices.push_back(bottomLeft); indices.push_back(topRight); // 두 번째 삼각형 indices.push_back(topRight); indices.push_back(bottomLeft); indices.push_back(bottomRight); } } Mesh* mesh = new Mesh(); mesh->CreateVB(vertices); mesh->CreateIB(indices); return mesh; }
C++
복사

렌더링

void GameObject::Render() { // 1. Transform 설정 UpdateTransformBuffer(); // 2. Material 바인딩 mMaterial->Bind(); // 3. Mesh 바인딩 mMesh->Bind(); // 4. Draw Call UINT indexCount = mMesh->GetIndexCount(); GetDevice()->GetContext()->DrawIndexed(indexCount, 0, 0); }
C++
복사

Part 6: 최적화 및 모범 사례

정점 캐시 최적화

인덱스 순서 최적화
GPU는 최근 처리한 정점을 캐시에 보관
인접한 삼각형이 정점을 공유하도록 배치
캐시 히트율 향상으로 성능 개선
최적화 도구
// 예시: Tom Forsyth의 정점 캐시 최적화 알고리즘 void OptimizeVertexCache(std::vector<UINT>& indices) { // 인덱스 순서를 재배치하여 캐시 효율 향상 // 라이브러리: DirectXMesh 등 활용 가능 }
C++
복사

메모리 관리

정점 압축
// 일반 정점 (48 bytes) struct Vertex { Vector3 position; // 12 bytes Vector3 normal; // 12 bytes Vector2 uv; // 8 bytes Vector4 color; // 16 bytes }; // 압축된 정점 (20 bytes) struct CompressedVertex { Vector3 position; // 12 bytes UINT packedNormal; // 4 bytes (각 축 10비트) USHORT uv[2]; // 4 bytes (half float) // 58% 메모리 절약 };
C++
복사
LOD (Level of Detail)
struct Mesh { std::vector<Mesh*> mLODLevels; // LOD별 메쉬 Mesh* GetLOD(float distance) { if (distance < 10.0f) return mLODLevels[0]; // 고품질 if (distance < 50.0f) return mLODLevels[1]; // 중품질 return mLODLevels[2]; // 저품질 } };
C++
복사

드로우 콜 최적화

인스턴싱
// 같은 메쉬를 여러 번 그릴 때 void RenderInstanced(Mesh* mesh, std::vector<Matrix>& transforms) { // Instance Buffer 생성 InstanceBuffer instanceBuffer; instanceBuffer.Create(transforms); mesh->Bind(); instanceBuffer.Bind(); GetDevice()->GetContext()->DrawIndexedInstanced( mesh->GetIndexCount(), transforms.size(), 0, 0, 0 ); }
C++
복사
배칭(Batching)
같은 Material을 사용하는 메쉬들을 하나로 합침
드로우 콜 수 감소
CPU 오버헤드 감소

결론

핵심 요약

Mesh (메쉬)
3D 오브젝트의 형태를 정의하는 기하학적 데이터
정점(Vertex)과 삼각형(Triangle)의 집합
GPU 최적화를 위한 다양한 표현 방법 제공
표현 방법
Triangle List: 단순하지만 메모리 비효율적
Indexed Triangle List: 가장 균형적이고 널리 사용
Triangle Strip: 최고 효율이지만 제한적
Mesh 클래스
Vertex Buffer와 Index Buffer 관리
Primitive Topology 설정
CPU와 GPU 데이터 동기화
렌더링 파이프라인 바인딩

실전 적용 가이드

메쉬 생성
1.
정점 배열 준비 (위치, 법선, UV 등)
2.
인덱스 배열 준비 (삼각형 구성)
3.
CreateVB와 CreateIB로 GPU 버퍼 생성
4.
적절한 Primitive Topology 설정
렌더링
1.
Mesh::Bind()로 파이프라인 설정
2.
Material 바인딩으로 셰이더 및 텍스처 설정
3.
Transform 업데이트
4.
DrawIndexed() 호출
최적화
1.
Indexed Triangle List 사용으로 메모리 절약
2.
정점 캐시 최적화로 성능 향상
3.
LOD 시스템으로 거리별 품질 조절
4.
인스턴싱으로 동일 메쉬 효율적 렌더링
Mesh는 3D 그래픽의 가장 기본적인 구성 요소이며, 효율적인 메쉬 관리는 게임 성능에 직접적인 영향을 미칩니다. 적절한 표현 방법 선택과 최적화 기법 적용을 통해 고품질 그래픽과 성능을 동시에 달성할 수 있습니다.