750 likes | 1.14k Views
제 13 장 기본적인 지형 렌더링. 기본적인 지형 렌더링. 지형 렌더링 지형 메쉬 : 삼각형 격자 격자 내 각 버텍스에 높이 부여하여 지형 표현 텍스처 추가하여 해변 , 언덕 , 눈 덮힌 산 등 표현 이 장의 내용 Terrain 클래스 구현 과정을 보여줌 전체 지형의 버텍스 / 인덱스 데이터를 보관하고 한 번에 렌더링 작은 지형의 경우에는 적용할 수 있으나 큰 지형에는 세부레벨조정 (Level of Detail) 이나 추려내기 기능 필요. 목표
E N D
제13장 기본적인 지형 렌더링 컴퓨터게임(DirectX)
기본적인 지형 렌더링 • 지형 렌더링 • 지형 메쉬: 삼각형 격자 • 격자 내 각 버텍스에 높이 부여하여 지형 표현 • 텍스처 추가하여 해변, 언덕, 눈 덮힌 산 등 표현 • 이 장의 내용 • Terrain 클래스 구현 과정을 보여줌 • 전체 지형의 버텍스/인덱스 데이터를 보관하고 한 번에 렌더링 • 작은 지형의 경우에는 적용할 수 있으나 큰 지형에는 세부레벨조정(Level of Detail) 이나 추려내기 기능 필요 컴퓨터게임(DirectX)
목표 • 산이나 계곡과 같은 자연스러운 지형을 표현하기 위한 지형의 높이 정보 생성 방법 • 지형을 위한 버텍스와 삼각형 데이터 생성 방법 • 지형에 조명과 텍스처 적용을 위한 테크닉 • 지형 표면에 카메라를 이식, 지형 위를 걷거나 뛰는 효과 구현 컴퓨터게임(DirectX)
높 이 맵 • 지형의 언덕이나 계곡을 표현하는 데 이용되는 것이 높이맵(heightmap) • 각각의 항목이 지형 격자 내의 특정 버텍스의 높이와 대응 • 각 요소가 지형 격자 내의 각 버텍스와 일대일 대응관계를 가지는 행렬로 생각 • 높이맵을 디스크에 보관할 때 • 각 높이에 한 바이트의 메모리를 할당 • 0 부터 255까지의 높이를 가짐 • 높이맵의 그래픽 표현 중 하나로 • 낮은 고도를 어두운 값으로, 높은 고도를 밝은 값으로 표현하는 그레이스케일 맵이 있다. 컴퓨터게임(DirectX)
높이맵 만들기 • 높이맵은 절차적인 방법을 이용하거나 포토샵과 같은 이미지 편집기를 이용해 만들 수 있다. • 이미지 편집기로 높이맵에 이용할 이미지를 만들 때 • 먼저 이미지 포맷으로 그레이 스케일을 지정 • 높이맵 작성을 마친 뒤, 8비트 RAW파일로 저장 • 경우에 따라서는 다른 파일 포맷으로 저장 가능 컴퓨터게임(DirectX)
(그림13.3) 포토샵에서 만든 그레이스케일 이미지 컴퓨터게임(DirectX)
RAW 파일 로딩하기 • RAW파일은 일련의 연속된 바이트 블록으로 다음의 메서드를 이용해 손쉽게 블록을 읽기 가능 std::vector<int> _heightmap; bool Terrain::readRawFile(std::string fileName) { // 각 벡터를 위한 높이 std::vector<BYTE> in( _numVertices); std::ifstream inFile(fileName.c_str(), std::ios_base::binary); if(inFile == 0) return false; 컴퓨터게임(DirectX)
inFile.read( (char*) &in[0]; // 버퍼 in.size()); // 버퍼로 읽어들일 바이트 수 inFile.close(); // byte 벡터를 int 벡터로 복사 _heightmap.resize( _numVertices); for(int i = 0; i < in.size(); i++) _heightmap[i] = in[i]; return true; } 컴퓨터게임(DirectX)
코드에서 바이트 벡터를 정수 벡터로 복사하고 있다. • 이것은 이후에 높이값을 [0,255] 범위 밖으로 배율을 변경 할 수 있도록 하기 위한 것이다. • 이 메서드의 유일한 제한 • RAW파일에 포함된 바이트 수만큼 지형 내의 버텍스가 준비되어 있어야 함 컴퓨터게임(DirectX)
Terrain 클래스는 높이맵 내의 항목에 접근하고 수정하기 위한 다음의 두 메서드를 제공 int Terrain::getHeightmapEntry(int row, int col) { return _heightmap[row*_numVertsPerRow+col]; } void Terrain::setHeightmapEntry(int row, int col, int value) { _heightmap[row*_numVertsPerRow+col] = value; } 컴퓨터게임(DirectX)
지형 기하정보 생성하기 Z 시작=(-w/2,d/2) 셀 간격 X 셀/ 사각형 d=깊이 끝=(w/2, -d/2) W=너비 컴퓨터게임(DirectX)
지형의 크기를 정의하는 요소 • 행당 버텍스 수 • 열당 버텍스 수 • 셀 간격 • 지형과 연결될 장치 • 높이맵 데이터를 포함하는 파일명 문자열 • 높이맵 요소들의 배율을 조정하는데 이용될 높이 스케일 값 • 예제에서 이 값들을 Terrain 클래스의 생성자에 전달 컴퓨터게임(DirectX)
class Terrain { public: Terrain( IDirect3DDevice9* device, std::string heightmapFileName, int numVertsPerRow, int numVertsPerCol, int cellSpacing, // 셀 간의 간격 float heightScale); // 높이 배율을 조정하는 값 ….. // 메서드 생략 private: … //장치/버텍스 버퍼 생략 컴퓨터게임(DirectX)
int _numVertsPerRow; int _numVertsPerCol; int _cellSpacing; int _numCellsPerRow; int _numCellsPerCol; int _width; int _depth; int _numVertices; int _numTriangles; float _heightScale; }; 전체 코드는 예제 참고 컴퓨터게임(DirectX)
생성자에 전달된 값들을 이용해 다음과 같은 방법으로 다른 변수들의 값을 계산함. _numCellsPerRow = _numVertsPerRow – 1; _numCellsPerCol = _numVertsPerCol – 1; _width = _numCellsPerCol * _cellSpacing; _depth = _numCellsPerCol * _cellSpacing; _numVertices = _numvertsPerRow * _numvertsPerCol; _numTriangles = _numCellsPerRow * _numCellsPerCol * 2; 컴퓨터게임(DirectX)
우리가 이용할 지형의 버텍스 구조체 struct TerrainVertex{ TerrainVertex(){} TerrainVertex(float x, float y, float z, float u, float v){ _x = x; _y = y; _z = z; _u = u; _v = v; } float _x, _y, _z; float _u, _v; static const DWORD FVF; }; const DWORD Terrain::TerrainVertex::FVF = D3DFVF_XYZ | D3DFVF_TEX1; 컴퓨터게임(DirectX)
TerrainVertex 는 Terrain 클래스 내부의 중첩된 클래스임 • Terrain 클래스 외부에서는 이 클래스가 필요하지 않기 때문이다. 컴퓨터게임(DirectX)
버텍스 계산하기 • 삼각형 격자의 버텍스들을 계산하기 위해서는 • 시작점의 버텍스를 생성하기 시작, • 끝점에 이를 때까지 셀 간격만큼 버텍스의 간격을 비우고 버텍스들을 만들면 된다. • Y좌표는 읽어들인 높이맵 데이터 구조체 내의 대응되는 항목에서 간단하게 얻을 수 있다. 컴퓨터게임(DirectX)
지형 버텍스와 텍스춰 버텍스간의 대응 • 텍스처 좌표 계산방법은 아래 그림을 참고 • 이 그림은 버텍스(i,j)에 해당하는 텍스처 좌표(u,v)를 얻는 간단한 시나리오를 보여줌 (1,0) (0,0) +U (0,1) (1,1) +V 컴퓨터게임(DirectX)
u = j · uCoordIncreamentSize • v = i · vCoordIncreamentSize 여기에서 • uCoordIncreamentSize = • vCoordIncreamentSize = 1 numCellCols 1 numCellRows 컴퓨터게임(DirectX)
최종적으로 버텍스를 생성하는 코드 bool Terrain::computeVertices(){ HRESULT hr = 0; hr = _device->CreateVertexBuffer( _numVertices * sizeof(TerrainVertex), D3DUSAGE_WRITEONLY, TerrainVertex::FVF, D3DPOOL_MANAGED, &_vb, 0); if(FAILED(hr)) return false; int startX = -_width / 2; // 버텍스 생성을 시작할 좌표 int startZ = _depth / 2; 컴퓨터게임(DirectX)
int endX = _width / 2; // 버텍스 생성을 마칠 좌표 int endZ = -_depth / 2; // 하나의 버텍스에서 다음 버텍스로 증가할 // 텍스처 좌표의 크기를 계산 float uCoordIncrementSize = 1.0f / (float)_numCellsPerRow; float vCoordIncrementSize = 1.0f / (float)_numCellsPerCol; TerrainVertex* v = 0; _vb->Lock(0, 0, (void**)&v, 0); int i = 0; 컴퓨터게임(DirectX)
for(int z = startZ; z >= endZ; z -= _cellSpacing) { int j = 0; for(int x = startX; x <= endX; x += _cellSpacing) { // 중첩된 루프 내의 위치에 따라 // 버텍스 버퍼와 높이맵으로의 올바른 // 인덱스를 계산한다. int index = i * _numVertsPerRow + j; 컴퓨터게임(DirectX)
v[index] = TerrainVertex( (float)x, (float)_heightmap[index], (float)z, (float)j * uCoordIncrementSize, (float)i * vCoordIncrementSize); j++; // next column } i++; // next row } _vb->Unlock(); return true; } 컴퓨터게임(DirectX)
인덱스 계산 –삼각형 정의하기 • 삼각형 격자의 인덱스를 계산하기 위해서는 • 그림의 좌측 상단에서 시작 • 우측 하단으로 각각의 사각형을 구성하는 두 개의 삼각형들을 차례대로 계산 버텍스열 j (0,0) 버텍스열 j + 1 버텍스행 i A B C D ij번째 셀 버텍스행 i+1 (m,n) 컴퓨터게임(DirectX)
사각형(i,j)를 찾기 위한 범용 공식 ∆ ABC = {i·numVertsPerRow+j i·numVertsPerRow+j+1 (i+1) ·numVertsPerRow+J} ∆ CBD = {(i+1)·numVertsPerRow+j i·numVertsPerRow+j+1 (i+1) ·numVertsPerRow+j+1} 컴퓨터게임(DirectX)
인덱스를 계산하는 코드 Bool Terrain::computeIndices() { HRESULT hr = 0; hr = _device->CreateIndexBuffer( _numTriangles * 3 * sizeof(WORD), //삼각형당 3개의 인덱스 D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOLMANAGED, &_ib, 0); 컴퓨터게임(DirectX)
If(FAILED(hr)) return false; WORD* indices = 0; _ib->Lock(0, 0, (void**)&indices, 0); // 하나의 사각형을 구성하는 두 개의 삼각형을 나타내기 위한 // 6개의 인덱스 그룹의 시작점 int baseIndex = 0; // 각 사각형의 삼각형을 계산하는 루프 for(int i = 0; i < _numCellsPerCol; i++) { 컴퓨터게임(DirectX)
for(int j = 0; j < _numCellsPerRow; j++) { indices[baseIndex] = i * _numVertsPerRow + j; indices[baseIndex + 1] = i * _numVertsPerRow + j + 1; indices[baseIndex + 2] = (i+1) * _numVertsPerRow + j; indices[baseIndex + 3] = (i+1) * _numVertsPerRow + j; indices[baseIndex + 4] = i * _numVertsPerRow + j + 1; indices[baseIndex + 5] = (i+1) * _numVertsPerRow + j + 1; baseIndex += 6; } } _ib->Unlock(); return true; } 컴퓨터게임(DirectX)
텍 스 처 링 • Terrain 클래스는 지형에 텍스처를 입히는 두 가지 방법을 제공 1) 미리 만들어둔 텍스처 파일을 읽어들이고 이를 이용하는 방법 • IDirect3DTexture9 인터페이스로의 포인터인 _tex데이터 멤버로 텍스처 파일을 읽어들임 • bool loadTexture(std::string fileName); 컴퓨터게임(DirectX)
bool loadTexture(std::string fileName); bool Terrain::loadTexture(std::string fileName) { HRESULT hr = 0; hr = D3DXCreateTextureFromFile( _device, fileName.c_str(), &_tex); if(FAILED(hr)) return false; return true; } 컴퓨터게임(DirectX)
2) 절차적 방식 • 지형에 텍스처를 입히는 두 번째 방법 • 절차적으로 텍스처를 계산해 내는 것 • 먼저 “빈” 텍스처를 만들고 미리 정의된 인자를 바탕으로 코드에서 각 텍셀의 컬러를 계산 • 예제에서는 지형의 높이를 인자로 이용 컴퓨터게임(DirectX)
Terrain:genTexture 메서드를 이용한 절차적 텍스처 • D3DXCreateTexture를 이용해 빈 텍스처를 만들고 • 최상의 레벨(밉맵 레벨)을 잠근 다음, 각각의 텍셀을 대상으로 반복하여 컬러를 입힘 • 대응되는 사각형의 높이에 따라 텍셀의 컬러를 결정하여 각 텍셀을 대상으로 반복하여 컬러를 입힘 • 각각의 텍셀에 색상을 입힌 뒤에는 텍셀을 비추는 태양 빛의 각도에 따라 텍셀의 밝기를 조정 • Terrain::lightTerrain 메서드에서 이루어짐 • 하위 밉맵 레벨의 텍셀을 계산하는 것으로 끝을 맺음 • D3DXFilterTexture 함수가 담당 컴퓨터게임(DirectX)
텍스처를 만들어내는 코드 bool Terrain::genTexture(D3DXVECTOR3* directionToLight) { HRESULT hr = 0; // 각각의 사각형 셀을 위한 텍셀 int texWidth = _numCellsPerRow; int texHeight = _numCellsPerCol; // 빈 텍스처를 만든다. hr = D3DXCreateTexture( _device, texWidth, texHeight, 0, // create a complete mipmap chain 0, // usage D3DFMT_X8R8G8B8,// 32 bit XRGB format D3DPOOL_MANAGED, &_tex); 컴퓨터게임(DirectX)
if(FAILED(hr)) return false; D3DSURFACE_DESC textureDesc; _tex->GetLevelDesc(0 /*level*/, &textureDesc); //직접 32비트 픽셀을 이용해 텍스처를 채우므로 //올바른 포맷인지를 먼저 확인 if( textureDesc.Format != D3DFMT_X8R8G8B8 ) return false; D3DLOCKED_RECT lockedRect; _tex->LockRect(0/*lock top surface*/, &lockedRect, 0 /* lock entire tex*/, 0/*flags*/); // 텍스처를 채운다. DWORD* imageData = (DWORD*)lockedRect.pBits; 컴퓨터게임(DirectX)
for(int i = 0; i < texHeight; i++) { for(int j = 0; j < texWidth; j++) { D3DXCOLOR c; // 사각형의 우측 상단 높이를 구한다. float height = (float)getHeightmapEntry(i, j) / _heightScale; // 대응되는 사각형 높이에 따라 텍셀의 컬러를 지정한다. if( (height) < 42.5f ) c = d3d::BEACH_SAND; else if( (height) < 85.0f ) c = d3d::LIGHT_YELLOW_GREEN; else if( (height) < 127.5f ) c = d3d::PUREGREEN; else if( (height) < 170.0f ) c = d3d::DARK_YELLOW_GREEN; else if( (height) < 212.5f ) c = d3d::DARKBROWN; else c = d3d::WHITE; 컴퓨터게임(DirectX)
imageData[i * lockedRect.Pitch / 4 + j] = (D3DCOLOR)c; } } _tex->UnlockRect(0); // 지형을 밝힌다. if(!lightTerrain(directionToLight)) { ::MessageBox(0, "lightTerrain() - FAILED", 0, 0); return false; } hr = D3DXFilterTexture( _tex, // 밉맵 레벨을 채울 텍스처 0, // 디폴트 팔레트 0, // 하위 레벨의 원본으로 최상위 레벨을 이용 D3DX_DEFAULT); // 디폴트 필터 컴퓨터게임(DirectX)
if(FAILED(hr)) { ::MessageBox(0, "D3DXFilterTexture() - FAILED", 0, 0); return false; } return true; } 컴퓨터게임(DirectX)
조 명 • Terrain::genTexture 메서드는 지형에 조명을 추가, 사실감을 더하는 Terrain::lightTerrain 메서드 호출 • Direct3D에게 지형의 음영만 계산하는 것을 맡기지 않고 지형의 조명을 계산하는 이유 • 버텍스 법선을 저장하지 않아도 되므로 메모리 절약 • 지형은 정적이며 조명을 움직이지도 않을 것이므로 조명을 미리 계산하여 지형에 실시간으로 조명을 적용하는데 따르는 처리 부담을 덜 수 있다. • 약간의 수학 연습을 할 수 있으며 기본적인 조명의 개념과 Direct3D 함수들에 익숙해짐 컴퓨터게임(DirectX)
조명의 개요 • 우리가 지형의 음영을 계산하는데 이용하는 조명 테크닉은 가장 기본적인 것 • 난반사광(Diffuse lighting) • 빛으로의 방향을 지정하여 하나의 평행한 광원을 정의 • 이 방향은 평행 광원에서의 빛이 발산되는 방향과는 반대의 방향 • 예를 들어 lightRaysDirection=(0, -1, 0) 방향으로 하늘에서 아래로 빛이 발산되기를 원한다면, 평행 광원으로의 방향은 반대방향인 directionToLight=(0, 1, 0) 이 된다. 컴퓨터게임(DirectX)
다음은 지형 내의 각각의 사각형에 대해 빛 벡터 L과 사각형 표면 법선 N간의 각도 계산 • 각도가 커지면 사각형 면이 점차 광원에서 멀어져 빛을 덜 받음 • 작아지면 사각형 면이 점차 광원으로 향하며 더욱 많은 빛을 받음 • 90도가 넘으면 표면은 더 이상 빛을 못 받음 L N L N 컴퓨터게임(DirectX)
빛 벡터와 표면 법선과의 각도 관계를 이용해 표면이 받는 빛의 양을 결정하는 [0, 1] 범위의 음영 스칼라를 만들 수 있다. • 큰 각도는 0에 가까운 스칼라로 표현되며 음영 스칼라와 컬러를 곱하면 0에 가까운 값으로 어두운 컬러가 된다. • 작은 각도는 1에 가까운 스칼라로 표현되며 컬러와 곱하면 1에 가까운 밝은 값이 만들어진다. 컴퓨터게임(DirectX)
사각형의 음영 계산하기 • 광원으로의 방향은 우리가 L 이라고 부르는 정규화된 벡터로 얻을 수 있다. • L 과 사각형의 법선 벡터 N 간의 각도를 계산하기 위해서는 먼저 N을 찾아야 함 • 먼저 사각형과 공면인 0이 아니면서 평행이 아닌 두개의 벡터를 찾아야 함 컴퓨터게임(DirectX)
다음은 이 두 개의 벡터 u와 v를 보여줌 a Y u = b - a b v = c - a Z c X 컴퓨터게임(DirectX)
u = (cellSpacing, by– ay, 0) • v = (0, cy– ay, -cellSpacing) • u와 v가 있다면 사각형의 법선 N은 n = u x v로 얻을 수 있다. • N은 정규화 되어야 한다. • N = • L과 N간의 각도를 찾기 위해서는 3-공간 내 두 단위 벡터의 내적이 두 벡터 간 각도의 코사인 이다. N |N| 컴퓨터게임(DirectX)
L · N = s • 스칼라 s는 [-1, 1] 범위 내에 위치한다. • [-1, 0) 내의 s값은 [그림 13.7]에서 빛을 받지 못하는 90도 이상의 L과 N간의 각도에 대응하므로, [-1,0) 내일 경우 s를 0으로 고정 float cosine = D3DXVec3Dot(&n, directionToLight); If(cosine < 0.0f) cosine = 0.0f; 컴퓨터게임(DirectX)
이제 90도 이상의 각도에 대해 s를 고정하면 s는 [0,1] 범위의 스칼라가 된다. • 0도에서 90도까지의 각도가 1에서 0값에 대응됨 • 각각의 사각형을 위한 음영 인수는 Terrrain::computeShade 에서 계산한다. • 이 메서드는 사각형을 지정하는 열과 행, 그리고 평행 광원의 방향을 인자로 받는다. 컴퓨터게임(DirectX)
float Terrain::computeShade(int cellRow, int cellCol, D3DXVECTOR3* directionToLight) { // 사각형의 새 버텍스 높이를 얻는다. float heightA = getHeightmapEntry(cellRow, cellCol); float heightB = getHeightmapEntry(cellRow, cellCol+1); float heightC = getHeightmapEntry(cellRow+1, cellCol); // 사각형의 두 벡터 외적을 이용해 // 법선을 찾아낸다. D3DXVECTOR3 u(_cellSpacing, heightB - heightA, 0.0f); D3DXVECTOR3 v(0.0f, heightC - heightA, -_cellSpacing); 컴퓨터게임(DirectX)
D3DXVECTOR3 n; D3DXVec3Cross(&n, &u, &v); D3DXVec3Normalize(&n, &n); float cosine = D3DXVec3Dot(&n, directionToLight); if(cosine < 0.0f) cosine = 0.0f; return cosine; } 컴퓨터게임(DirectX)