Game Programming II 기본적인지형렌더링 October 26, 2006 지형메쉬 (Terrain Mesh) 격자내에버텍스에높이를부여하여생성된메쉬 실제지형과같이산에서계곡으로의부드러운전환가능 텍스처를추가하면, 모래로덮인해변이나풀로덮인언덕, 눈덮인산등표현가능 Terrain 클래스구현아주작은지형을이용하는게임의경우, 버텍스프로세싱을지원하는그래픽카드를이용해충분히적용가능
Sample: Terrain 주요내용 산이나계곡과같은자연스러운지형을표현하기위해지형의높이정보생성방법 지형을위한버텍스와삼각형데이터의생성방법 지형에조명과텍스처의적용방법 카메라를지형표면에위치시키고, 지형을따라걷거나뛰는방법
높이맵 (Heightmaps) 지형격자내의버텍스의높이값을요소가가지고있는배열 ( 일대일대응관계 ) 보통각요소에한바이트의메모리할당 (0~255) 필요에따라원하는배율로값을조정 그레이스케일맵 (grayscale map) 그레이스케일로표현된높이맵 높이맵만들기 이미지편집기사용 ( 예 : Adobe Photoshop) 이미지포맷 그레이스케일 8-bit RAW 파일로저장 ( 헤더없음 )
RAW 파일로딩하기 std::vector<int> _heightmap; bool Terrain::readRawFile(std::string filename) // Restriction: RAW file dimensions must be >= to the // dimensions of the terrain. That is a 128x128 RAW file // can only be used with a terrain constructed with at most // 128x128 vertices. // A height for each vertex std::vector<byte> in( _numvertices ); std::ifstream infile(filename.c_str(), std::ios_base::binary); if( infile == 0 ) return false; infile.read( (char*)&in[0], // buffer in.size());// number of bytes to read into buffer infile.close(); // copy BYTE vector to int vector _heightmap.resize( _numvertices ); for(int i = 0; i < in.size(); i++) _heightmap[i] = in[i]; return true; 높이맵접근과수정 행과열을지정하여항목을참조 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;
지형기하정보생성하기 (1) Start=( w/2, d/2) +Z CellSpacing +X d=depth Cell/Quad End=(w/2, d/2) w=width 지형기하정보생성하기 (2) class Terrain public: Terrain( IDirect3DDevice9* device, std::string heightmapfilename, int numvertsperrow, int numvertspercol, int cellspacing, // space between cells float heightscale); ~Terrain();... method snipped private:... device/vertex buffer etc snipped int _numvertsperrow; int _numvertspercol; int _cellspacing; int _numcellsperrow; int _numcellspercol; int _width; int _depth; int _numvertices; int _numtriangles; float _heightscale; std::vector<int> _heightmap; ;
Terrain 클래스의생성자 Terrain::Terrain(IDirect3DDevice9* device, std::string heightmapfilename, int numvertsperrow, int numvertspercol, int cellspacing, float heightscale) _device = device; _numvertsperrow = numvertsperrow; _numvertspercol = numvertspercol; _cellspacing = cellspacing; _numcellsperrow = _numvertsperrow - 1; _numcellspercol = _numvertspercol - 1; _width = _numcellsperrow * _cellspacing; _depth = _numcellspercol * _cellspacing; _numvertices = _numvertsperrow * _numvertspercol; _numtriangles = _numcellsperrow * _numcellspercol * 2; _heightscale = heightscale; // load heightmap // scale heights // compute the vertices // compute the indices 지형을위한버텍스구조체 Terrain 클래스내부에중첩됨 Terrain 클래스외부에서는필요치않기때문 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;
버텍스계산하기 (1) (x, y, z) 좌표 x 와 z 좌표 : 시작점 (start) 에서버텍스를생성하여끝점 (end) 에이를때까지셀간격 (cell spacing) 만큼간격을비우면서버텍스들생성 y 좌표 : 읽어들인높이맵 (hightmap) 데이터구조체내의대응되는항목 텍스처좌표 (u, v) v = u = i ucoordincrementsize j vcoordincrementsize 1 ucoordincrementsize = numcellcols 1 vcoordincrementsize = numcellrows (0, 0) (1, 0) (0, 1) (1, 1) +V +U 버텍스계산하기 (2) 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; // coordinates to start generating vertices at int startx = -_width / 2; int startz = _depth / 2; // coordinates to end generating vertices at int endx = _width / 2; int endz = -_depth / 2; // compute the increment size of the texture coordinates // from one vertex to the next. float ucoordincrementsize = 1.0f / (float)_numcellsperrow; float vcoordincrementsize = 1.0f / (float)_numcellspercol;
버텍스계산하기 (3) TerrainVertex* v = 0; _vb->lock(0, 0, (void**)&v, 0); int i = 0; for(int z = startz; z >= endz; z -= _cellspacing) int j = 0; for(int x = startx; x <= endx; x += _cellspacing) // compute the correct index into the vertex buffer and heightmap // based on where we are in the nested loop. int index = i * _numvertsperrow + j; 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; 인덱스계산 삼각형정의하기 (1) 각사각형을구성하는두개의삼각형을차례로계산 Vertex Column j Vertex Column j+1 (0, 0) Vertex Row i Vertex Row i+1 A C ijth cell B D ABC = CBD = (m, n) i numvertsperrow + j, i numvertsperrow + j + 1, ( i + 1) numvertsperrow + j ( i + 1) numvertsperrow + j, i numvertsperrow + j + 1, ( i + 1) numvertsperrow + j + 1
인덱스계산 삼각형정의하기 (2) bool Terrain::computeIndices() HRESULT hr = 0; hr = _device->createindexbuffer( _numtriangles * 3 * sizeof(word), // 3 indices per tri. D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, &_ib, 0); if(failed(hr)) return false; 인덱스계산 삼각형정의하기 (3) WORD* indices = 0; _ib->lock(0, 0, (void**)&indices, 0); // index to start of a group of 6 indices that describe the // two triangles that make up a quad int baseindex = 0; // loop through and compute the triangles of each quad for(int i = 0; i < _numcellspercol; i++) 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; _ib->unlock(); return true; // next quad baseindex += 6;
텍스처링 (1) 지형에텍스처를입히는두가지방법 미리만들어둔텍스처파일을읽어들여이용 절차적으로텍스처를계산 텍스처파일로부터 _tex 데이터멤버로텍스처를읽어들임 (IDirect3DTexture9) bool Terrain::loadTexture(std::string filename) HRESULT hr = 0; hr = D3DXCreateTextureFromFile( _device, filename.c_str(), &_tex); if(failed(hr)) return false; return true; 텍스처링 (2) 절차적방식 빈 텍스처를만든다음, 인자를바탕으로코드에서텍셀의컬러를계산 방법 예 ) 지형의높이를인자로이용 낮은고도 ( 모래해변색상 ), 중간고도 ( 풀덮인언덕색상 ), 높은고도 ( 눈덮인산색상 ) 빈텍스처를만들고최상위레벨 ( 밉맵 ) 을잠금 각사각형의높이에따라텍셀의컬러를결정 텍셀을비추는태양빛의각도에따라텍셀의밝기를조정 조명 하위밉맵레벨의텍셀을게산
텍스처링 (3) bool Terrain::genTexture(D3DXVECTOR3* directiontolight) // Method fills the top surface of a texture procedurally. Then // lights the top surface. Finally, it fills the other mipmap // surfaces based on the top surface data using D3DXFilterTexture. HRESULT hr = 0; // texel for each quad cell int texwidth = _numcellsperrow; int texheight = _numcellspercol; // create an empty texture hr = D3DXCreateTexture( _device, texwidth, texheight, 0, // create a complete mipmap chain 0, // usage D3DFMT_X8R8G8B8,// 32 bit XRGB format D3DPOOL_MANAGED, &_tex); if(failed(hr)) return false; D3DSURFACE_DESC texturedesc; _tex->getleveldesc(0 /*level*/, &texturedesc); // make sure we got the requested format because our code // that fills the texture is hard coded to a 32 bit pixel depth. if( texturedesc.format!= D3DFMT_X8R8G8B8 ) return false; 텍스처링 (4) D3DLOCKED_RECT lockedrect; _tex->lockrect(0 /*lock top surface*/, &lockedrect, 0 /*lock entire tex*/, 0 /*flags*/); DWORD* imagedata = (DWORD*)lockedRect.pBits; for(int i = 0; i < texheight; i++) for(int j = 0; j < texwidth; j++) D3DXCOLOR c; // get height of upper left vertex of quad. float height = (float)getheightmapentry(i, j) / _heightscale; if( (height) < 42.5f ) else if( (height) < 85.0f ) else if( (height) < 127.5f ) else if( (height) < 170.0f ) else if( (height) < 212.5f ) else c = d3d::beach_sand; c = d3d::light_yellow_green; c = d3d::puregreen; c = d3d::dark_yellow_green; c = d3d::darkbrown; c = d3d::white; // fill locked data, note we divide the pitch by four because the // pitch is given in bytes and there are 4 bytes per DWORD. imagedata[i * lockedrect.pitch / 4 + j] = (D3DCOLOR)c; _tex->unlockrect(0);
텍스처링 (5) if(!lightterrain(directiontolight)) ::MessageBox(0, "lightterrain() - FAILED", 0, 0); return false; hr = D3DXFilterTexture( _tex, 0, // default palette 0, // use top level as source level D3DX_DEFAULT); // default filter if(failed(hr)) ::MessageBox(0, "D3DXFilterTexture() - FAILED", 0, 0); return false; return true; 조명 (1) 광원에따른지형의음영계산 사실감증가 세가지장점 버텍스법선을저장하지않아도되므로메모리절약 Direct3D가지형에실시간으로조명을처리하는부담을덜수있음 지형은정적이며조명을움직이지않을것이므로, 미리조명계산이가능 약간의수학연습을할수있으며, 기본적인조명의개념과 Direct3D 함수에익숙해질수있음
조명 (2) 난반사광 (diffuse lighting) 방향성광원 ( 빛의방향이평행한평행광원 ) 방향 : 빛이발산되는방향과는반대방향 lightraysdirection=(0, -1, 0) directiontolight=(0, 1, 0) 빛을향하는벡터 L과사각형표면의법선벡터 N 사이의각도를계산 N L L N [0, 1] 범위의음영스칼라 표면이받는빛의양 컬러에음영스칼라를곱하면, 어두운또는밝은값이만들어짐 (a) (b) 사각형의음영계산하기 (1) 정규화된빛을향하는벡터벡터 Nˆ 사이의각도 외적을이용하여벡터 N 계산 u = v = ( cellspacing, by ay, 0) ( 0, c a, cellspacing) s = L ˆ N ˆ s: [-1, 1] [0, 1] y y N = u v N N ˆ = N Lˆ 과사각형의법선 +Y v=c-a c a +Z float cosine = D3DXVec3Dot(&n, directiontolight); if(cosine < 0.0f) cosine = 0.0f; u=b-a b +X
사각형의음영계산하기 (2) float Terrain::computeShade(int cellrow, int cellcol, D3DXVECTOR3* directiontolight) // get heights of three vertices on the quad float heighta = getheightmapentry(cellrow, cellcol); float heightb = getheightmapentry(cellrow, cellcol+1); float heightc = getheightmapentry(cellrow+1, cellcol); // build two vectors on the quad D3DXVECTOR3 u(_cellspacing, heightb - heighta, 0.0f); D3DXVECTOR3 v(0.0f, heightc - heighta, -_cellspacing); // find the normal by taking the cross product of two // vectors on the quad. D3DXVECTOR3 n; D3DXVec3Cross(&n, &u, &v); D3DXVec3Normalize(&n, &n); float cosine = D3DXVec3Dot(&n, directiontolight); if(cosine < 0.0f) cosine = 0.0f; return cosine; 지형에음영입히기 bool Terrain::lightTerrain(D3DXVECTOR3* directiontolight) HRESULT hr = 0; D3DSURFACE_DESC texturedesc; _tex->getleveldesc(0 /*level*/, &texturedesc); // make sure we got the requested format because our code that fills the // texture is hard coded to a 32 bit pixel depth. if( texturedesc.format!= D3DFMT_X8R8G8B8 ) return false; D3DLOCKED_RECT lockedrect; _tex->lockrect( 0, // lock top surface level in mipmap chain &lockedrect,// pointer to receive locked data 0, // lock entire texture image 0); // no lock flags specified DWORD* imagedata = (DWORD*)lockedRect.pBits; for(int i = 0; i < texturedesc.height; i++) for(int j = 0; j < texturedesc.width; j++) // index into texture, note we use the pitch and divide by // four since the pitch is given in bytes and there are // 4 bytes per DWORD. int index = i * lockedrect.pitch / 4 + j; // get current color of quad D3DXCOLOR c( imagedata[index] ); // shade current quad c *= computeshade(i, j, directiontolight);; // save shaded color imagedata[index] = (D3DCOLOR)c; _tex->unlockrect(0); return true;
지형위를 걷기 (1) 지형에따라카메라의높이 (y 좌표 ) 를조정 카메라의위치의 x 와 z 좌표를이용하여, 카메라가놓인셀을찾아내서높이를얻어냄 float Terrain::getHeight(float x, float z) // Translate on xz-plane by the transformation that takes // the terrain START point to the origin. x = ((float)_width / 2.0f) + x; z = ((float)_depth / 2.0f) - z; // Scale down by the transformation that makes the // cellspacing equal to one. This is given by // 1 / cellspacing since; cellspacing * 1 / cellspacing = 1. x /= (float)_cellspacing; z /= (float)_cellspacing; 지형위를 걷기 (2) Start +Z CellSpacing 1.0 +X +X p p start origin cell spacing 1.0 -z +z +Z
지형위를 걷기 (3) // From now on, we will interpret our positive z-axis as // going in the 'down' direction, rather than the 'up' direction. // This allows to extract the row and column simply by 'flooring' // x and z: float col = ::floorf(x); float row = ::floorf(z); // get the heights of the quad we're in: // // A B // *---* // / // *---* // C D float A = getheightmapentry(row, col); float B = getheightmapentry(row, col+1); float C = getheightmapentry(row+1, col); float D = getheightmapentry(row+1, col+1); floor(t) = 가장큰정수 t 셀을구성하는네버텍스의높이를얻어낼수있음 지형위를 걷기 (4) 셀의어떤삼각형에있는지? +X +Y v1 +Z (1,2) v0 y (x, z) v3 v2 +X +Z (0,0) // Translate by the transformation that takes the upper-left // corner of the cell we are in to the origin. Recall that our // cellspacing was nomalized to 1. Thus we have a unit square // at the origin of our +x -> 'right' and +z -> 'down' system. float dx = x - col; float dz = z - row; p p (1,1)
지형위를 걷기 (5) 선형보간 (linear interpolation) +Y vy A uy +Z B +Y dzvy A dxuy y +Z B C (x, z) C (x, z) +X +X 지형위를 걷기 (6) // Note the below computations of u and v are unnecessary, we really // only need the height, but we compute the entire vector to emphasis // the books discussion. float height = 0.0f; if(dz < 1.0f - dx) // upper triangle ABC float uy = B - A; // A->B float vy = C - A; // A->C // Linearly interpolate on each vector. The height is the vertex // height the vectors u and v originate from A, plus the heights // found by interpolating on each vector u and v. height = A + d3d::lerp(0.0f, uy, dx) + d3d::lerp(0.0f, vy, dz); else // lower triangle DCB float uy = C - D; // D->C float vy = B - D; // D->B // Linearly interpolate on each vector. The height is the vertex // height the vectors u and v originate from D, plus the heights // found by interpolating on each vector u and v. height = D + d3d::lerp(0.0f, uy, 1.0f-dx) + d3d::lerp(0.0f, vy, 1.0f-dz); return height; float d3d::lerp(float a, float b, float t) return a - (a*t) + (b*t);
Sample: Terrain Setup ( ) Terrain* TheTerrain = 0; Camera TheCamera(Camera::LANDOBJECT); FPSCounter* FPS = 0; bool Setup() // Create the terrain. D3DXVECTOR3 lightdirection(0.0f, 1.0f, 0.0f); TheTerrain = new Terrain(Device, "coastmountain64.raw", 64, 64, 10, 0.5f); TheTerrain->genTexture(&lightDirection); // Set texture filters. Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR); Device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR); // Set projection matrix. D3DXMATRIX proj; D3DXMatrixPerspectiveFovLH( &proj, D3DX_PI * 0.25f, // 45 - degree (float)width / (float)height, 1.0f, 1000.0f); Device->SetTransform(D3DTS_PROJECTION, &proj); return true; void Cleanup() d3d::delete<terrain*>(theterrain); d3d::delete<fpscounter*>(fps);
Display ( ) bool Display(float timedelta) // Update the scene: if( Device )... [snipped input checking] D3DXVECTOR3 pos; TheCamera.getPosition(&pos); float height = TheTerrain->getHeight( pos.x, pos.z ); pos.y = height + 5.0f; // add height because we're standing up TheCamera.setPosition(&pos); D3DXMATRIX V; TheCamera.getViewMatrix(&V); Device->SetTransform(D3DTS_VIEW, &V); // Draw the scene: Device->Clear(0, 0, D3DCLEAR_TARGET D3DCLEAR_ZBUFFER, 0xff000000, 1.0f, 0); Device->BeginScene(); D3DXMATRIX I; D3DXMatrixIdentity(&I); if( TheTerrain ) TheTerrain->draw(&I, false); if( FPS ) FPS->render(0xffffffff, timedelta); Device->EndScene(); Device->Present(0, 0, 0, 0); return true; 약간의개선점 지형기하정보를다수의버텍스버퍼로나누는것이적합 최적의버텍스버퍼크기는? 하드웨어에따라다르기때문에실험을통해알아내야함 블록 (block) 지형의특정사각형영역을담당 큰메쉬를다수의작은메쉬로나누는함수 void D3DXSplitMesh( LPD3DXMESH pmeshin, const DWORD *padjacencyin, const DWORD MaxSize, const DWORD Options, DWORD *pmeshesout, LPD3DXBUFFER *ppmesharrayout, LPD3DXBUFFER *ppadjacencyarrayout, LPD3DXBUFFER *ppfaceremaparrayout, LPD3DXBUFFER *ppvertremaparrayout );
연습문제 Assignment #2 상대방비행기를격추시키는게임 기본지형모델 + 비행시뮬레이션 + 슈팅게임 요구사항 필수 새로운지형메쉬생성 2대이상의비행기 유연한비행시뮬레이션 미사일발사 옵션 미사일및비행기충돌체크 격추된비행기폭파 (14장파티클참조 ) 점수합산및레벨디자인 기타 (+α) 장애물, 보너스아이템등
기간및제출방법 11 월 14 일 ( 화 ) 10:00AM 수업시간전까지 지각제출은감점 (-10점/ 일 5일후받지않음 ) 제출방법 아래파일들을압축하여 e-mail 제출 Source code (Debug 혹은 Release 폴더제외 ) Screen shot (jpg 이미지 1~2개 ) Self-check table ( 뒷장참조 ) 11월 14일 ( 화 ) 수업시간에시연할것 Self-Check Table 학번 : 이름 : 스스로채점후제출하시오. (O,, X 표기 ) 비행시뮬레이션 (50점) 1. 새로운지형메쉬를생성했는가? (10점) 2. 2대이상의비행기를렌더링했는가? (10점) 3. 유연한비행시뮬레이션이이루어지는가? (20점) 4. 미사일이발사되는가? (10점) Options (+α점) 1. 적군과아군의분류가가능한새로운비행기모델 (5점) 2. 조종하는방향으로비행기가기울어짐 (5점) 3. 비행기들사이의충돌체크 (10점) 4. 비행기와미사일사이의충돌체크 (10점) 5. 격추된비행기의폭파 (20점) 6. 점수합산및레벨디자인 난이도증가 (20점) 7. 기타 (+α점) :