DirectX

39. DirectX11 3D 입문_HeightMap

devRiripong 2024. 2. 8.
반응형

테크닉 같은 걸 연습하는 작은 공간이라 할 수 있는데

 

그다음으로 하고 싶은 건 높이맵을 테스트해 볼 것이다.

지금은 평면이지만 어디는 올라고 어디는 내려가게 해서 지형지물의 모양을 표시할 것이다.

 

유니티에서는 terrain툴이 있어서 올리고 내리는 걸 할 수 있었어.

그걸 별도의 데이터로 관리를 해주면 되는데 그게 Heigt map이라고 한다.

 

이 이미지 파일들이 jpg, png 등 보여주는 용도로 그리는 경우도 있지만 경우에 따라서는 그 자체가 특정 정보를 들고 있는 경우도 생긴다.

노멀맵이라고 해서 노멀 벡터, 수직인 방향을 저장하는 그런 용도로 사용될 수 있고, 높이 맵이라고 해서 연관성이 있는 모든 높이들의 정보를 가지고 있을 수 있다.

수업자료로 제공된 height.png 의 속성을 보면

8비트로 되어 있다. RGB의 R만 있는 것이다.

이거에 따라 파싱하는게 달라지기는 해야 한다.

모든 픽셀 마다 하나에 0에서 255 사이의 숫자를 들고 있는 것이다.

 

이런 모양으로 보이고 있는 것이다.

 

어떤 부분은 높고, 어떤 부분은 낮고가 이 맵에 저장이 되어 있는 것이고, 그걸 우리가 그대로 읽어서 실제로 우리가 만든 지형지물에다가 높이에 맞게 실제로 좌표를 조절해 주면 된다.

그게 Height map의 개념이다.

원리는 단순하다 .

 

이 Height map을 저장해서 그걸로 땅의 지형지물을 올린다가 핵심이다.

그다음에 생각해야 하는 건 CPU에서 할 것인가, GPU에서 할 것인가가 관건이다.

보통은 GPU에 넘기는 경우가 많다.

경우에 따라서 CPU에 세팅을 해준 다음에 그거를 초기 단계에서 한 번만 계산해서 GPU에 넘겨주는 것도 방법이 될 수 있으니까 양방향으로 해보긴 할 것이다.

지금은 CPU에서 처리하는 방식을 알아보고 언젠가는 Tesselation이라고 지형지물 가지고 장난치는 것을 하려면 GPU에 넘겨서 해주긴 해야 한다.

 

이 이미지를 로드해서 지형에다가 적용을 해보는 실습을 해본다.

 

당장 사용하겠다라기 보다는 이런 기법들에 대한 지식을 얻는 쪽이라 생각해 주면 된다.

 

1. HeightMapDemo 클래스

1) 07. HeightMapDemo클래스 생성하기

일단 탐색기에서 06. SamplerDemo클래스를 복제해서 07. HeightMapDemo로 이름을 바꾼다.

솔루션 탐색기에서 Client/Game필터에 넣어준다.

코드 내용도 HeightMapDemo로 해준다.

 

2) _heightMap에 height.png Load 하고, Size 변수에 저장하기

shared_ptr<Texture> _heightMap;

Texture변수를 하나 늘린다.

 

HeightMapDemo::Init()에서

// Texture
_heightMap = RESOURCES->Load<Texture>(L"Height", L"..\\\\Resources\\\\Textures\\\\Terrain\\\\height.png");
_texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\veigar.jpg");

const int32 width = _heightMap->GetSize().x; 
const int32 height = _heightMap->GetSize().y;

이렇게 height.png를 Load하고, Size를 변수에 넣어준다.

 

heightMap의 크기에 따라서 뭔가를 하는 게 일반적이다. 아니면 비율을 맞춰줘서 map도 높이 맵이랑 비슷하거나 배수인 형식으로 뭔가 만드는 게 일반적이다.

 

GetSize에서 _size는

void Texture::Load(const wstring& path)
{
	DirectX::TexMetadata md;
	HRESULT hr = ::LoadFromWICFile(path.c_str(), WIC_FLAGS_NONE, &md, _img);
	CHECK(hr);

	hr = ::CreateShaderResourceView(DEVICE.Get(), _img.GetImages(), _img.GetImageCount(), md, _shaderResourveView.GetAddressOf());
	CHECK(hr);
	
	_size.x = md.width;
	_size.y = md.height;
}

Load 할 때 기입을 하게끔 유도를 해줬었다.

 

3) _heightMap의 1바이트의 픽셀 정보 얻기 

그리고 Map에 있는 픽셀 정보를 얻어야 하는데 급조했다.

Texture.h에서 보면

const DirectX::ScratchImage& GetInfo() { return _img; }
DirectX::ScratchImage _img = {};

ScratchImage에 비슷한 정보가 있는 거 같다.

 

이걸 HightMapDemo::Init에서

const DirectX::ScratchImage& info = _heightMap->GetInfo(); 
uint8* pixelBuffer = info.GetPixels();

GetPixels가 마침 1바이트짜리로 되어 있다. 원래라면 이 파일 포맷을 체크하고 그거에 따라가지고 해야 한다. 지금은 임시 테스트를 위해 하는 거니 그냥 하고 있다.

파일 포맷마다 어떤 것이냐에 따라 1픽셀마다 4바이트로 표현할 수도 있고, rgba로 표현할 수도 있고, rgb로 할 수도, 하나로만 할수도 있다. 아까 이미지는 8비트로 되어 있으니까 1바이트로만 표현하는 애라는 걸 알 수 있으니까 그냥 uint8* pixelBuffer에다가 꺼내주면 된다. 

 

4) _geometry의 size를 로드한 이미지에 맞춰주기

그다음에 Geometry를 만들어 줄 건데 여기선 사이즈를 이미지에 맞춰본다.

_geometry = make_shared<Geometry<VertexTextureData>>();
GeometryHelper::CreateGrid(_geometry, width, height);

 

5) height의 수치를 보정하기

그 다음에 하는 건 넘겨주기 전에 좌표를 한번 인위적으로 고쳐주도록 한다.

 

HeightMapDemo::Init에서

// CPU
{
    vector<VertexTextureData>& v = const_cast<vector<VertexTextureData>&>(_geometry->GetVertices()); 

    for (int32 z = 0; z < height; z++)
    {
        for (int32 x = 0; x < width; x++)
        {
            int32 idx = width * z + x; 
            uint8 height = pixelBuffer[idx] / 255.f * 25.f; 
            v[idx].position.y = height; // 높이 보정
        }
    }
}

 

6) 카메라의 위치와 회전값 수정하기

카메라도 잘 안 보이니 위로 좀 올리고 로테이션도 수정한다.

_camera->GetTransform()->SetPosition(Vec3(0.f, 5.f, 0.f)); 
_camera->GetTransform()->SetRotation(Vec3(25.f, 0.f, 0.f));

 

다음과 같이 된다.

void HeightMapDemo::Init()
{
	_shader = make_shared<Shader>(L"05. Sampler.fx");

	// Texture
	_heightMap = RESOURCES->Load<Texture>(L"Height", L"..\\\\Resources\\\\Textures\\\\Terrain\\\\height.png");
	_texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\veigar.jpg");

	const int32 width = _heightMap->GetSize().x; 
	const int32 height = _heightMap->GetSize().y; 

	const DirectX::ScratchImage& info = _heightMap->GetInfo(); 
	uint8* pixelBuffer = info.GetPixels(); 

	// Object
	_geometry = make_shared<Geometry<VertexTextureData>>(); 
	GeometryHelper::CreateGrid(_geometry, width, height); 

	// CPU
	{
		vector<VertexTextureData>& v = const_cast<vector<VertexTextureData>&>(_geometry->GetVertices()); 
	
		for (int32 z = 0; z < height; z++)
		{
			for (int32 x = 0; x < width; x++)
			{
				int32 idx = width * z + x; 
				uint8 height = pixelBuffer[idx] / 255.f * 25.f; 
				v[idx].position.y = height; // 높이 보정
			}
		}
	}

	_vertexBuffer = make_shared<VertexBuffer>(); 
	_vertexBuffer->Create(_geometry->GetVertices()); 
	_indexBuffer = make_shared<IndexBuffer>(); 
	_indexBuffer->Create(_geometry->GetIndices()); 

	// Camera
	_camera = make_shared<GameObject>(); 
	_camera->GetOrAddTransform(); 
	_camera->AddComponent(make_shared<Camera>()); 
	_camera->AddComponent(make_shared<CameraScript>()); 

	_camera->GetTransform()->SetPosition(Vec3(0.f, 5.f, 0.f)); 
	_camera->GetTransform()->SetRotation(Vec3(25.f, 0.f, 0.f)); 
}

 

7) HeightMapDemo::Render에서 이전 시간에 썼던 코드 정리하기

HeightMapDemo::Render에서 지난 시간에 사용했던 ADRESS_VALUE 부분들을 삭제한다.

void HeightMapDemo::Render()
{
	// ? 
	_shader->GetMatrix("World")->SetMatrix((float*)&_world);
	_shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
	_shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
	_shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get()); 

	uint32 stride = _vertexBuffer->GetStride(); 
	uint32 offset = _vertexBuffer->GetOffset(); 

	// DeviceContext에서 IA단계에서 사용할 VertextBuffer를 묶어주는 함수였다.
	DC->IASetVertexBuffers(0, 1, _vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
	DC->IASetIndexBuffer(_indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);

	_shader->DrawIndexed(0,0, _indexBuffer->GetCount(), 0, 0); 
}

 

2. 06. Terrain.fx 셰이더 

1) 06. Terrain.fx파일 생성하기

그리고 Shader를 Sampler를 사용하고 있었는데

탐색기에서 05. Sampler.fx를 복제해서 이름을 06. Terrain.fx로 바꾼다.

솔루션 탐색기에서 Client/Shader 필더에 넣는다.

2) 셰이더 코드 정리하기 

가장 기본적인 버전으로 바꿔준다.

matrix World; 
matrix View; 
matrix Projection; 
Texture2D Texture0;

struct VertexInput
{
    float4 position : POSITION; // POSITION을 찾아서 연결해 줄 것이다.
    float2 uv : TEXCOORD;
};

struct VertexOutput
{
    float4 position : SV_POSITION;  // System Value 라고 예약된 이름이라 명시
    float2 uv : TEXCOORD;
};

VertexOutput VS( VertexInput input)
{
    VertexOutput output;
    output.position = mul(input.position, World);
    output.position = mul(output.position, View);
    output.position = mul(output.position, Projection);
    
    output.uv = input.uv; 
    
    return output;     
}

SamplerState Sampler0
{
// U가 넘어 갔을 때, V가 넘어갔을 때 무엇을 할 것이냐
// 대부분은 둘 다 똑같은 경우로 두는 경우가 많다. 
    AddressU = Wrap;
    AddressV = Wrap;
};

float4 PS(VertexOutput input) : SV_TARGET
{
        return Texture0.Sample(Sampler0, input.uv);
}

RasterizerState FillModeWireFrame
{
    FillMode = Wireframe;
};

technique11 T0
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS())); // 버전은 5.0, main 함수는 VS다는 뜻
        SetPixelShader(CompileShader(ps_5_0, PS())); 
    }

    pass P1
    {
        SetRasterizerState(FillModeWireFrame);

        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, PS()));
    }
};

 

3) HeightMapDemo::Init()에서 셰이더 세팅하기

이렇게 만든 쉐이더가 Terrain이니까

void HeightMapDemo::Init()
{
	_shader = make_shared<Shader>(L"06. Terrain.fx");

이렇게 넣어준다.

 

3. HeightMapDemo을 Main에 세팅하고 실행하기

Main.cpp 에도

#include "07. HeightMapDemo.h"

를 추가하고

desc.app = make_shared<HeightMapDemo>(); // 실행 단위

이렇게 수정한다.

 

실행을 해보면

 

1) 기존 이미지로 테스트하기

 

지형이 울퉁불퉁하다는 것을 볼 수 있다.

 

이런 느낌으로 땅이라는 걸 그려줄 수 있다.

 

2) grass.jpg 이미지로 테스트하기

이번에 Veigar가 아닌 Grass 텍스쳐를 그려본다.

HeightMapDemo::Init에서

_texture = RESOURCES->Load<Texture>(L"Grass", L"..\\\\Resources\\\\Textures\\\\Terrain\\\\grass.jpg");

이렇게 수정한다.

 

 

땅의 높이를 높일 수 있다.

 

3) Wireframe으로 테스트하기

HeightMapDemo::Render()에서

_shader->DrawIndexed(0,1, _indexBuffer->GetCount(), 0, 0);

이렇게 P1으로 해서 WireFrame으로 보면

 

이런 식으로 된 걸 확인할 수 있다.

 

4. 맺음말

이걸 나중에 툴로 제공해서 툴에서 높이 맵을 만들어서 저장할 수 있게 만들어 줄 수 있다.

 

언리얼이나 유니티에서 terrain툴을 이용해서 조작하고 저장을 하면 실제로 texture 파일 형태로 HeightMap이라는 정보가 추출이 된다.

그걸 이용해서 딴 데서도 지형지물을 올릴 수 있다고 보면 된다.

 

앞으로 사용할 일은 없을 것이다. GPU에 던져서 쉐이더에서 연산하는 게 조금 더 편리하다고 볼 수 있다. Texture를 넘기는 게 가능하기 때문이다.

 

간단하게 높이맵 적용 실습을 해보았다.

 

png는 뭔가 그려주는 파일이란 이미지였는데 그뿐만 아니라 중요한 정보를 기입해서 정보를 꺼내 쓰는 방식이 자주 등장하게 될 것이다.

반응형

'DirectX' 카테고리의 다른 글

41. DirectX11 3D 입문_Mesh  (0) 2024.02.10
40. DirectX11 3D 입문_Normal  (0) 2024.02.09
38. DirectX11 3D 입문_Sampling  (0) 2024.02.07
37. DirectX11 3D 입문_Geometry  (0) 2024.02.06
36. DirectX11 3D 입문_텍스처  (0) 2024.02.06

댓글