DirectX

5. 텍스쳐와 UV

devRiripong 2023. 12. 13.
반응형

이번 시간에 한 것

 

1. IndexBuffer를 이용해 사각형 그리기

 

2. UV 좌표를 이용해 이미지를 띄우기

    1) struct Vertex에서 color 를 uv로 교체
    2) CreateGeometry에서 color를 uv로 교체
    3) CreateInputLayout에서 COLOR를 TEXCOORD로 교체
    4) Shader에서 color : COLOR를 uv : TEXCOORD로 교체
    5) Shader에서 texture0, sampler0 선언하고, PS 구현
    6) CreateSRV의 이미지 로드하는 부분 구현
    7) CreateSRV의 ::CreateShaderResourceView 호출 부 구현
    8) Render의 PS 영역에서 _shaderResourceView 세팅, Init에서 CreateSRV 호출, 실행

 

3. UV좌표 제어해 보기 

 

1. IndexBuffer를 이용해 사각형 그리기

IndexBuffer라는 게 있다. 이게 뭐고, 왜 필요할까? 

 

사각형 만드는데 정점이 몇개 필요할까? 

-> 4개? 

No, 삼각형 단위로 2개 만들어 줘야 하기 때문에 6개 필요하다. 

 

만약 사각형 하나에 삼각형 6개가 필요하게 되면, 몇 십 만개 되었을 때 메모리적인 낭비가 심해지기 때문에 필요한 것이 Index buffer다. 

 

Index buffer나 정점이 더 필요하게 되나 그게 그거 아닌가? 

-> 인덱스는 2바이트나 4바이트 정수에 불과한데, 정점은 포지션, 컬러 이외의 정보 추가 되면 정점 하나가 사이즈가 커질 수 있다. 

 

정점마다 넘버링을 해서 넘버링을 이어서 삼각형을 만들어 주세요 라고 던지는게 인덱스 버퍼의 역할이다. 

 

 

일단 만들어 보자. 

 

Game.cpp의 Game::CreateGeometry()로 가서 

void Game::CreateGeometry()
{
	// VertexData - CPU 메모리에 있는 거 
	// 1 3
	// 0 2
	{
		_vertices.resize(4);

		_vertices[0].position = Vec3(-0.5f, -0.5f, 0.f); 
		_vertices[0].color = Color(1.f, 0.f, 0.f, 1.f); 

		_vertices[1].position = Vec3(-0.5f, 0.5f, 0.f);
		_vertices[1].color = Color(1.f, 0.f, 0.f, 1.f);

		_vertices[2].position = Vec3(0.5f, -0.5f, 0.f);
		_vertices[2].color = Color(1.f, 0.f, 0.f, 1.f);

		_vertices[3].position = Vec3(0.5f, 0.5f, 0.f);
		_vertices[3].color = Color(1.f, 0.f, 0.f, 1.f);
	}

이렇게 vertex를 하나 추가해서 만들어 준다.

 

추가적으로 IndexBuffer라는 걸 만들어 줄 것이다. 
Game.h에 

vector<uint32> _indices; 
// uint16 중 선택사항인데 일단은 4바이트로 가본다.인덱스 목록 CPU에 들고 있음. 
ComPtr<ID3D11Buffer> _indexBuffer = nullptr; 
// GPU에 복사해서 넘겨줘야

이렇게 선언한다.

 

그리고 _indexBuffer를 채우는 코드를 CreateGeometry에 추가한다. 

	// Index
	{
		_indices = { 0, 1, 2, 2, 1, 3 }; 
        // 시계방향 골랐으면 시계방향을 유지해야 한다. 삼각형 2개 만들어 줬다.
	}

	// IndexBuffer
	{
		D3D11_BUFFER_DESC desc;
		ZeroMemory(&desc, sizeof(desc));
		desc.Usage = D3D11_USAGE_IMMUTABLE;
		desc.BindFlags = D3D11_BIND_INDEX_BUFFER; 
		// Input assembler에서 건내 주는 index buffer를 만들고 있는 것이기 때문
		desc.ByteWidth = (uint32)(sizeof(uint32) * _indices.size());

		D3D11_SUBRESOURCE_DATA data;
		ZeroMemory(&data, sizeof(data));
		data.pSysMem = _indices.data(); // &_indices[0];와 같은 의미

		HRESULT hr = _device->CreateBuffer(&desc, &data, _indexBuffer.GetAddressOf());
		CHECK(hr); 
	}
}

중단점을 찍고 실행해 보면 _indexBuffer에 뭔가 들어 가 있는 것을 볼 수 있다. 
이렇게 인덱스 버퍼가 만들어졌다. 

 

Index Buffer도 렌더링 파이프라인 그림에서 보면 Input Assembler 단계에서 끼워 넣어준다. 
SetVertexBuffer랑 똑같이 해주면 된다. 
void Game::Render()로 가서 보면 InputAssembler 단계에서 

_deviceContext->IASetIndexBuffer(_indexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);

이 코드를 끼워 넣으면 렌더링 파이프 라인에 묶여서 얘를 사용할 준비가 끝난다. 

 

기존엔 Render의 끝에서 Draw를 해줬었는데 이제 이 방식이 아니라 

// _deviceContext->Draw(_vertices.size(), 0); 
_deviceContext->DrawIndexed(_indices.size(), 0, 0);

이렇게 해서 6개의 인덱스를 건내 주면 그걸 이용해서 그려주게 된다고 볼 수 있다. 

 

실행을 해 보면 

이렇게 잘 뜨는 것을 볼 수 있다.

 

결국 이 인덱스 버퍼의 장점은 무엇인가? (가끔 면접에 나오는 질문이다. )
-> 정점의 개수를 줄일 수 있기 때문에 메모리적으로 효율성 있게 작업을 할 수 있게 해준다. 


정점에다가 넘버링을 해서 넘겨주는 방식이다. 

 

2. UV 좌표를 이용해 이미지를 띄우기

이미지를 붙여서 이미지를 띄우는 것을 하고 싶은데 UV좌표라는 걸 알아야 작업을 할 수 있다. 

 

그리고 Default.hlsl 쉐이더를 수정 해서 일반 색상이 아니라 이미지 파일에서의 좌표를 꺼내서 그 좌표의 색상을 PS에서 return 하게 유도를 해줘야 한다. 

 

알파값이 있는 .png 파일 2개를 준비한다. GameCoding 프로젝트의 폴더(.cpp, .h가 들어 있는 폴더)에 넣는다.

 

1) struct Vertex에서 color 를 uv로 교체

struct Vertex
{
	Vec3 position; 
	// Color color; 
	Vec2 uv; 
};

일단 Struct.h의 struct Vertex에서 Color color;를 제거한다.
Vec2 uv; 를 추가해서 UV 좌표를 등장시킨다.

UV는 0에서 1사이의 값을 가지고 있을 것이다.
좌 상단이 0, 0, 우 하단이 1, 1 이다. 
이미지가 스트레칭 되어서 사각형 안에 들어가게 된다. 
텍스쳐의 퍼센티지를 설정하는 느낌. 어느 부분을 오려 붙일지 입력하는 것이다.

 

2) CreateGeometry에서 color를 uv로 교체

지금 빌드하면 에러가 발생하는데 그 부분들을 하나씩 고쳐준다.

void Game::CreateGeometry()
{
	// VertexData - CPU 메모리에 있는 거 
	// 1 3
	// 0 2
	{
		_vertices.resize(4);

		_vertices[0].position = Vec3(-0.5f, -0.5f, 0.f); 
		_vertices[0].uv = Vec2(0.f, 1.f); 
		// _vertices[0].color = Color(1.f, 0.f, 0.f, 1.f); 

		_vertices[1].position = Vec3(-0.5f, 0.5f, 0.f);
		_vertices[1].uv = Vec2(0.f, 0.f);
		// _vertices[1].color = Color(1.f, 0.f, 0.f, 1.f);

		_vertices[2].position = Vec3(0.5f, -0.5f, 0.f);
		_vertices[2].uv = Vec2(1.f, 1.f);
		// _vertices[2].color = Color(1.f, 0.f, 0.f, 1.f);

		_vertices[3].position = Vec3(0.5f, 0.5f, 0.f);
		_vertices[3].uv = Vec2(1.f, 0.f);
		// _vertices[3].color = Color(1.f, 0.f, 0.f, 1.f);
	}

color를 주석처리 하고 각 점의 uv 좌표를 설정해 주면 빌드가 된다. 

 

3) CreateInputLayout에서 COLOR를 TEXCOORD로 교체

Geometry 바꿨을 때 먼저 작업한게 InputLayout이었다. 
이게 하는 역할은 방금 만들어준 Vertex라는 애가 어떻게 생겼는지 묘사하는 단계다.
근데 이제는 COLOR가 빠졌어. COLOR 대신 UV좌표가 들어가는데, 보통 이걸 Shader랑 맞춰주는데 이 부분을 Texture의 좌표다라고 TEXCOORD라는 이름을 사용한다. 

void Game::CreateInputLayout()
{
	D3D11_INPUT_ELEMENT_DESC layout[] =
	{
		{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
		{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}, 
        // 12바이트 부터 UV가 시작한다. offset
	};

 

4) Shader에서 color : COLOR를 uv : TEXCOORD로 교체

그리고 CreateInputLayout에서 _vsVlob을 받아서 사용하는데, _vsVlob은 CreateVS에서 LoadShaderFromFile함수를 호출해 Default.hlsl의 VS를 로딩해서 만들어지는 것이기 때문에, VS와 연관이 있고, VS의 매개 변수 형태인 VS_INPUT과 연관이 있다. 
그래서 Default.hlsl Shader를 CreateInputLayout의 POSITION, TEXCOORD와 맞춰줘야 한다. 

struct VS_INPUT
{ 
    float4 position : POSITION; 
    // float4 color : COLOR;
    float2 uv : TEXCOORD; 
};

struct VS_OUTPUT
{
    float4 position : SV_POSITION; 
    // float4 color : COLOR;
    float2 uv : TEXCOORD;
};

// IA-VS-RS-PS-OM
// 위치와 관련된 변화
VS_OUTPUT VS(VS_INPUT input)
{ 
    VS_OUTPUT output; 
    output.position = input.position; 
    output.uv = input.uv; 
    
    return output; 
}

 

5) Shader에서 texture0, sampler0 선언하고, PS 구현

이렇게 하고, PS에서 uv좌표의 비율에 맞는 이미지의 픽셀을 그려줘야 한다. 이미지 파일이라는 리소스 자체를 GPU한테 건내줘서 GPU가 이미지 파일을 알고 있어야 하고, 거기서 점 하나를 꺼내와야 한다. 이 부분을 만들어 줘야 한다.

 

 Default.hlsl에 

Texture2D texture0 : register(t0); 
// t가 Texture의 약자이고, t0 register에다가 teture0이라는 아이를 등록을 할 것이다 라고 예고를 하는 거. 

SamplerState sampler0 : register(s0); 

// 이렇게 texture와 sampler가 정해 지면 color를 정할 때 바로 input을 하는게 아니라 
// texture0에다가 Sample이란 함수 이용 	
// smapler0는 어떻게 갖고 올지에 대한 규약

float4 PS(VS_OUTPUT input) : SV_Target 
{      
    float4 color = texture0.Sample(sampler0, input.uv);
    // texture0에서 uv좌표를 이용하여 해당하는 색상을 빼온다.
    
    return color; 
}
// : SV_Target은 픽셀 셰이더(Pixel Shader)의 출력이 렌더링 타겟(Render Target), 
// 즉 프레임 버퍼에 저장될 색상 데이터임을 나타냅니다.

texture0을 사용하기로 한 것 까지는 오케이인데, 
어떤 이미지 파일 하나를 로드 해서 CPU 메모리에 들고 있다가, 그걸 리소스로 만들어서 GPU 쪽에다가 만들어준 다음에, 렌더링 파이프라인에 연결을 해줘야 한다. 

 

6) CreateSRV의 이미지 로드하는 부분 구현

Game클래스에 가서 새로운 리소스를 사용할 준비를 할 것이다. 

SRV(Shader Resource View)라는 픽셀 셰이더 단계에서 넣어줄 수 있는 아이가 하나 있다. 

void CreateSRV();  라는 함수를 선언하고, SRV를 만들어 볼거다. 

Shader의 리소스로 사용할 수 있는 뷰다 라고 볼 수 있다. 

ShaderResourceView의 "View"는 GPU에 저장된 데이터 리소스를 셰이더가 특정 방식으로 해석하고 사용할 수 있도록 하는 인터페이스나 관점을 의미한다. SRV는 리소스를 셰이더에서 읽을 수 있는 형태로 만들어 주는 중요한 구성 요소다.

 

SRV를 만드려면 일단 어떤 이미지 파일 하나를 가져 와야 한다. 

사용할 라이브러리는  DirectXTex라는 라이브러리다. MS 문서를 보면 이걸 사용하라고 권장하는 글이 있다. 

CreateSRV를 구현한다. 

void Game::CreateSRV()
{
	DirectX::TexMetadata md; 
	DirectX::ScratchImage img; 
	HRESULT hr = ::LoadFromWICFile(L"chiikawa.png", WIC_FLAGS_NONE, &md, img); 
	CHECK(hr); 
}

 

여기까지 했으면 일단은 이미지를 로드 한 것이다.
나중엔 ResourceManager에 배치해서 하겠지만 지금 단계에서는 CPU 메모리 상에 들고 있는 것이고, 
이어서 ShaderResourceView라는 걸 만들어 본다.

 

7) CreateSRV의 ::CreateShaderResourceView 호출 부 구현

Game.h에  

	// SRV
	ComPtr<ID3D11ShaderResourceView> _shaderResourceView = nullptr;

을 선언해 준 다음에 다시 CreateSRV로 가서

void Game::CreateSRV()
{
	DirectX::TexMetadata md; 
	DirectX::ScratchImage img; 
	HRESULT hr = ::LoadFromWICFile(L"chiikawa.png", WIC_FLAGS_NONE, &md, img); 
	CHECK(hr); 

	hr = ::CreateShaderResourceView(_device.Get(), img.GetImages(), img.GetImageCount(), 
    					md, _shaderResourceView.GetAddressOf()); 
	CHECK(hr); 
}

 

여기까지 ShaderResourceView를 만들어 줬다. 
이걸 이용해서 연결을 해준다.

 

8) Render의 PS 영역에서 _shaderResourceView 세팅, Init에서 CreateSRV 호출, 실행

Render로 가서 PS영역에서 

	_deviceContext->PSSetShaderResources(0, 1, _shaderResourceView.GetAddressOf());

이렇게 ShaderResourceView를 세팅하는 코드를 추가한다. 

 

실행을 해보면 이미지가 안보인다. 뭔가를 놓친 것이다. 

Game::Init에서 CreateSRV를 호출하는 걸 누락했었다. 왠만하면 함수 만들자마자 바로 호출을 해주자.

 

호출을 해 주고 실행을 하면 

그림이 잘 뜬다.

 

3. UV좌표 제어해 보기 

CreateGeometry에서 UV좌표를 수정해 보자.

void Game::CreateGeometry()
{
	// VertexData - CPU 메모리에 있는 거 
	// 1 3
	// 0 2
	{
		_vertices.resize(4);

		_vertices[0].position = Vec3(-0.5f, -0.5f, 0.f); 
		_vertices[0].uv = Vec2(0.f, 0.5f); 
		// _vertices[0].color = Color(1.f, 0.f, 0.f, 1.f); 

		_vertices[1].position = Vec3(-0.5f, 0.5f, 0.f);
		_vertices[1].uv = Vec2(0.f, 0.f);
		// _vertices[1].color = Color(1.f, 0.f, 0.f, 1.f);

		_vertices[2].position = Vec3(0.5f, -0.5f, 0.f);
		_vertices[2].uv = Vec2(0.5f, 0.5f);
		// _vertices[2].color = Color(1.f, 0.f, 0.f, 1.f);

		_vertices[3].position = Vec3(0.5f, 0.5f, 0.f);
		_vertices[3].uv = Vec2(0.5f, 0.f);
		// _vertices[3].color = Color(1.f, 0.f, 0.f, 1.f);
	}

이렇게 uv 좌표 1이었던 걸 0.5로 해준다면 어떻게 될까? 

 

 

이미지의 좌측 상단만 확대되어 뜬다. 

만약 초과해서 2로 한다면 어떻게 될까? 

축소되어 뜬다.

 

이런 부분들을 SamplerState같은거로 옵션에 따라 건드릴 수 있다. 
옵션 중에서 rasterize 단계에 넘어 간 다음에 samplerState라는 걸 설정할 수가 있는데, 그걸 다다음 시간에 건드려 보면서, UV좌표를 초과하거나 잘 못 설정했을 때 어떤 식으로 동작할지를 보는 연습을 하게 될 것이다. 

 

결론적으로 
이번 시간에 UV 매핑을 해 보았다. 
겸사겸사 Index buffer로 넣어 보는 것도 해 보았다.
UV 매핑으로 어떠한 텍스쳐를 붙일 때 픽셀 셰이더 단계에서 동작하는 것이고, 픽셀 셰이더 단계에서 텍스쳐를 넣어준 다음에, 해당하는 uv 좌표를 꺼내서 그것에 대한 색상을 갖고 오세요를 Default.hlsl의 PS에서 해 주었다. 
나중엔 텍스쳐 하나만이 아니라

Texture2D texture0 : register(t0); 
Texture2D texture1 : register(t1);


이렇게 texture 1번을 추가해서 

float4 PS(VS_OUTPUT input) : SV_Target
{      
    float4 color = texture1.Sample(sampler0, input.uv);// texture1에 uv좌표를 이용하여 해당하는 색상을 빼온다.
    
    return color; 
}


이렇게 texture1로 한다거나, 
0번과 1번의 색상을 꺼내 적절히 섞는 다거나, 얼마든지 자유롭게 응용을 할 수 있다.


CreateSRV에서 

void Game::CreateSRV()
{
	DirectX::TexMetadata md; 
	DirectX::ScratchImage img; 
	HRESULT hr = ::LoadFromWICFile(L"chiikawa.png", WIC_FLAGS_NONE, &md, img); 
	CHECK(hr); 

	hr = ::CreateShaderResourceView(_device.Get(), img.GetImages(), img.GetImageCount(), md, _shaderResourceView.GetAddressOf()); 
	CHECK(hr); 

	hr = ::LoadFromWICFile(L"hachiware.png", WIC_FLAGS_NONE, &md, img);
	CHECK(hr);

	hr = ::CreateShaderResourceView(_device.Get(), img.GetImages(), img.GetImageCount(), md, _shaderResourceView2.GetAddressOf());
	CHECK(hr);
}

 

이렇게 _shaderResourceView2를 만들어 줘서, 

	ComPtr<ID3D11ShaderResourceView> _shaderResourceView2 = nullptr;

 

CreateSRV를 호출하면 SRV를 하나 더 만들어 주게 해 보았다. 

 

연결을 할 떄는 Render에 가서 

	// PS
	_deviceContext->PSSetShader(_pixelShader.Get(), nullptr, 0); 
	_deviceContext->PSSetShaderResources(0, 1, _shaderResourceView.GetAddressOf()); 
	_deviceContext->PSSetShaderResources(1, 1, _shaderResourceView2.GetAddressOf());

이런식으로 리소스를 연결해줄 수 있다.

 

두 개를 연결을 해줬다면 실행을 해보면, 

이제는 하치와레가 뜨는 것을 볼 수 있다.
Default.hlsl의 PS에서 texture를 조절하면서 띄우고 싶은 걸 왔다 갔다 띄울 수 있다.

float4 PS(VS_OUTPUT input) : SV_Target
{      
    float4 color = texture0.Sample(sampler0, input.uv);// texture0에 uv좌표를 이용하여 해당하는 색상을 빼온다.
    
    return color; 
}

이렇게 texture0으로 다시 바꾸면 치이카와가 뜬다. 

 

4. 맺음말

처음부터 모든 걸 작업하지 않았지만 vertex shader랑 pixel shade만 만들어 놓고 시작한 다음 계속 인자들을 끼워 넣어서 작업하는 실습을 하고 있는데 이 중간에 꽂아 넣어 인자를 넣는 작업이 생각보다 단순하지 않다는 걸 볼 수 있다. 
셰이더랑 통신을 해서 뭔가 넣을 때 이런 식으로 texture1 을 만들어서 준다거나 다양한 방식을 응용할 수 있다. 
vertex shader도 마찬가지다. 추가적인 값을 넣어주고 싶을 때 무작적 넣어주는게 아니라 constant buffer를 만들어서 똑같이 register 등록을 한 다음에  SetConstantBuffer 등을 이용해서 연결해서 작업하는 식으로 동작을 하게 될 것이다.
모든 게 생각보다 간단하지 않다. 

복습할 때 다시 한번 쭉 따라 치면서 만들어 보고, 다음 시간에는 게임 수학을 공부할 것이다. 
행렬과 관련된 부분, DX 함수를 막 사용하는게 아니라 하나씩 구현을 해보면서 행렬에 대한 연습을 해 볼 것이다. 그 다음에 렌더링 파이프라인에 따라 클래스 배치를 옮겨 보면서 코드를 이해 하기 쉽게 할 건데 지금 단계에서는 모든 기능들을 Game 한 클래스 안에다 넣었다.

그림을 그리면서 뭐가 공용적인 부분이고, 뭐가 특정한 오브젝트의 종속적인 부분인지를 생각을 해보는 게 좋다. 
예를 들어 Geometry 같은 경우는 모두가 공유하는 걸까, 특정 오브젝트에 종속적인 개념일까?

vertices, vertexBuffer, indices, indexBuffer, inputLayout 같은 걸 언리얼이나, 유니티에서는 뭐라고 부를까? 
이게 메쉬의 개념이다. 
메쉬 라는 건 하나의 리소스인 거고, 이 리소스로 오크를 그려주건 작업을 할 것이다.

 

셰이더는 모든 애들이 공용으로 사용하는 것일까? 특정 오브젝트 대상으로만 있는 걸까? 전체 구조 보다는 이런걸 생각해야 한다. 
유니티에서 기본적인 셰이더가 있을 건데 그건 특이한 애 제외하고는 공통적으로 사용하는 것이다. 

ShaderResourceView에서 texture, sampler 같은게 함수의 인자같은 존재들이다. 이런 애들 같은 건, 오브젝트 마다 달라질 수 있다고 예상할 수 있다. 

PS가 하나의 함수라고 치면, texture0, texture1는 CPU에서 넣어주는 인자값이라고도 볼 수 있다.  

나중에 구조로 쪼개면서 보면 감이 올 확률이 높다. 
지금은 한방에 넣어서 관리 하다 보니 헷갈릴 수 있다.

Render할 때 일부분은 묶이고, 일부분은 분리 되어 따로 가는 식으로 구조가 잡힐 것이다. 

PixelShader에 ShaderResourceView를 꽂아 넣는 거 까지 해 보았는데, VertexShader에서도 Constant buffer라는 걸 만들어서 꽂아 넣는 작업을 할 수 있다. 이걸  이어서 해볼거야. 

반응형

'DirectX' 카테고리의 다른 글

7. RasterizerState, SampleState, BlendState  (0) 2023.12.14
6. ConstantBuffer  (0) 2023.12.13
4. 삼각형 띄우기  (0) 2023.12.09
3. 장치 초기화  (0) 2023.12.06
2. 기본 프레임워크  (0) 2023.12.02

댓글