이번엔 조명 코드를 분석해보자

우선 LightDemo를 실행시켜보면 실시간으로 Spot, Direct, Point Light가 적용돼서 화면이 보이는 것을 볼 수 있다.

또한 전에 배웠던 Diffuse Ambient Specular Light도 모두 적용되어 있는 모습을 볼 수 있다. 

이렇게 다양한 조명을 적용하는 것도 좋지만 조명이 많아질 수록 연산량이 늘어나기 때문에 필요한 조명만 필요한 영역에 사용하는 것이 좋다.

우선 LightDemo 코드를 보면 헤더파일에 LightHelper가 추가되어있는데 이는 각 조명에 대한 구조체 정의가 있는 부분이다. 

우선 DirectionalLight는 일직선으로 가는 조명으로 Ambient Diffuse Specular 3가지 특성을 가지고 있다.

struct DirectionalLight
{
	DirectionalLight() { ZeroMemory(this, sizeof(this)); }

	XMFLOAT4 Ambient;
	XMFLOAT4 Diffuse;
	XMFLOAT4 Specular;
	XMFLOAT3 Direction;
	float Pad; // Pad the last float so we can set an array of lights if we wanted.
};

 

PointLight는 어떤 Point로부터 모든 방향으로 동일한 세기의 빛을 뿜는 광원을 말한. 이 조명에는 Ambient,Diffuse,Specular 특성이 있고 Point의 위치와 영향을 줄 거리, 높이 등으로 이루어져있다. 

struct PointLight
{
	PointLight() { ZeroMemory(this, sizeof(this)); }

	XMFLOAT4 Ambient;
	XMFLOAT4 Diffuse;
	XMFLOAT4 Specular;

	// Packed into 4D vector: (Position, Range)
	XMFLOAT3 Position;
	float Range;

	// Packed into 4D vector: (A0, A1, A2, Pad)
	XMFLOAT3 Att;
	float Pad; // Pad the last float so we can set an array of lights if we wanted.
};

 

SpotLight는 손전등과 무대 조명과 같이 원뿔 모양으로 한곳을 비추는 조명으로 구조체 내부는 Point Light와 같다.

struct SpotLight
{
	SpotLight() { ZeroMemory(this, sizeof(this)); }

	XMFLOAT4 Ambient;
	XMFLOAT4 Diffuse;
	XMFLOAT4 Specular;

	// Packed into 4D vector: (Position, Range)
	XMFLOAT3 Position;
	float Range;

	// Packed into 4D vector: (Direction, Spot)
	XMFLOAT3 Direction;
	float Spot;

	// Packed into 4D vector: (Att, Pad)
	XMFLOAT3 Att;
	float Pad; // Pad the last float so we can set an array of lights if we wanted.
};

 

여기서 조명마다 있는 pad는 16바이트 정렬을 해주기 위한 값이다.

이렇게 정의해준 조명 중에 어떤 조명을 사용할 지 골라준 다음에 쉐이더에서 빛에 따른 색상 연산을 해주어야한다. 쉐이더 코드를 살펴보자면 우선 위의 조명 구조체와 구조를 맞춰준 구조체를 쉐이더쪽에서도 만들어줘야한다.

struct DirectionalLight
{
	float4 Ambient;
	float4 Diffuse;
	float4 Specular;
	float3 Direction;
	float pad;
};

struct PointLight
{ 
	float4 Ambient;
	float4 Diffuse;
	float4 Specular;

	float3 Position;
	float Range;

	float3 Att;
	float pad;
};

struct SpotLight
{
	float4 Ambient;
	float4 Diffuse;
	float4 Specular;

	float3 Position;
	float Range;

	float3 Direction;
	float Spot;

	float3 Att;
	float pad;
};

조명에 대한 값을 매 프레임 받아주기 위해 상수버퍼를 추가해준 것을 볼 수 있다. 

cbuffer cbPerFrame
{
	DirectionalLight gDirLight;
	PointLight gPointLight;
	SpotLight gSpotLight;
	float3 gEyePosW;
};

 

그리고 쉐이더에서 각 조명을 연산해주는 코드를 살펴볼 것인데 대표적으로 DirectionalLight 코드만 분석해보자.

oid ComputeDirectionalLight(Material mat, DirectionalLight L, 
                             float3 normal, float3 toEye,
					         out float4 ambient,
						     out float4 diffuse,
						     out float4 spec)
{
	// Initialize outputs.
	ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
	diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
	spec    = float4(0.0f, 0.0f, 0.0f, 0.0f);

	// The light vector aims opposite the direction the light rays travel.
	float3 lightVec = -L.Direction;

	// Add ambient term.
	ambient = mat.Ambient * L.Ambient;	

	// Add diffuse and specular term, provided the surface is in 
	// the line of site of the light.
	
	float diffuseFactor = dot(lightVec, normal);

	// Flatten to avoid dynamic branching.
	[flatten]
	if( diffuseFactor > 0.0f )
	{
		float3 v         = reflect(-lightVec, normal);
		float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);
					
		diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
		spec    = specFactor * mat.Specular * L.Specular;
	}
}

 

코드를 보면 조명의 특성 3가지를 동시에 연산한 다음에 최종적인 색상을 반환해주고 있다. 뱉어준 색상을 통해 PS단계에서 다른 조명의 색상이 있다면 이것까지 연산에 포함해서 최종적으로 화면에 보이는 색을 반환해준다. 

float4 PS(VertexOut pin) : SV_Target
{
	// Interpolating normal can unnormalize it, so normalize it.
	pin.NormalW = normalize(pin.NormalW);

	float3 toEyeW = normalize(gEyePosW - pin.PosW);

	// Start with a sum of zero. 
	float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
	float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
	float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);

	// Sum the light contribution from each light source.
	float4 A, D, S;

	ComputeDirectionalLight(gMaterial, gDirLight, pin.NormalW, toEyeW, A, D, S);
	ambient += A;
	diffuse += D;
	spec += S;

	ComputePointLight(gMaterial, gPointLight, pin.PosW, pin.NormalW, toEyeW, A, D, S);
	ambient += A;
	diffuse += D;
	spec += S;

	ComputeSpotLight(gMaterial, gSpotLight, pin.PosW, pin.NormalW, toEyeW, A, D, S);
	ambient += A;
	diffuse += D;
	spec += S;

	float4 litColor = ambient + diffuse + spec;

	// Common to take alpha from diffuse material.
	litColor.a = gMaterial.Diffuse.a;

	return litColor;
}

 

그리고 조명 코드 밑에 보면 Material이라는 구조체가 있는데 이는 물체의 특징으로 어떤 재질도 되어 있는지에 관한 정이다. 이 재질에 따라 빛을 반사하는 정도가 다를 것이다. 결국은 재질 과 빛의 특징 연산을 통해 최종적인 빛 연산이 이루어지게 된다.

 

연산의 이론적인 부분을 한번 복습 해보자.

우선 Ambient(주변광)는 빛의 반사를 생각해서 최소한의 빛을 보장하기 위해 빛의 색상을 일정 수준이라도 더 해준다. 

코드에서 보면 단순하게 Ambient값을 곱해주는 것을 볼 수 있다.

ambient = mat.Ambient * L.Ambient;

 

Diffuse(확산광)는 물체에 빛을 쏜다고 했을 때 물체 포면에 닿아서 여러 방향으로 확산되는 특징을 가지고 있으며 표면과 물체의 노멀 벡터 각도가 0도일 때 가장 강하며 90도일 때 가장 약하다. 이 빛은 물체의 재질에 영향을 많이 받는다.

이 값은 -1~ 1까지 의 범위를 가지고 있다. 계산은 -L과 N의 내적을 구해주면 cos값을 구할 수 있는데

이를 물체 재질값과 Diffuse 값을 곱해주면 된다.

코드 상에서 공식은 다음과 같다. 

float3 lightVec = -L.Direction;
float diffuseFactor = dot(lightVec, normal);
diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;

 

Specular(경사)은 물체의 표면에서 특정 각도로 반사되어서 우리 눈에 보이는 조명이다. 반짝이는 효과라고 생각해도 된다. 물체의 재질에 따라 반짝임 정도가 달라진다.

계산은 L과 N의 반사각을 구한 다음 눈까지의 각도와 내적해준 다음 Specular값을 배수값으로 곱해주면 된다.

float3 v         = reflect(-lightVec, normal);
float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);

spec    = specFactor * mat.Specular * L.Specular;

 

이렇게 해준 다음에 최종적으로  PS단계에서는 이 3가지 특성의 합으로 조명의 값을 연산해준다.

float4 litColor = ambient + diffuse + spec;



이제 본격적으로 Directx 11에서 그리기 연산이 어떻게 이루어지는 지 살펴보자.

우선 위처럼 하나의 정육면체 큐브를 만든다고 했을 때 이 큐브가 어떻게 만들어지는 지 생각해보자.

이 큐브도 렌더링파이프라인을 모두 거쳐서 나오게 될 것이다.

우선 IA 단계에서 정점과 인덱스 정보를 묘사하는 아이인 Input Layout을 통해 넘겨주고 이를 VS단계에서 카메라에 어떻게 보이게 되는지 행렬곱 연산을 적용시켜 줄 것이다. 그리고 RS단계에서 색상을 보간해줘서 위와 같이 보이게 해야할 것 이다. 그렇게 해주려면 정점과 색상에 대한 정보가 있어야 할 것이다.

 

코드를 살펴보자면 우선  vertexBuffer와 indexBuffer에 정보를 채워서 GPU쪽에 보내줘야한다.

ComPtr<ID3D11Buffer> _vertexBuffer;
ComPtr<ID3D11Buffer> _indexBuffer;

 

버퍼로 전달하고 싶은 정보를 구조체로 정의하고 정보를 채워준다음 버퍼 정보 정의해서 하나의 버퍼를 만들어 주면 된다.

이때 큐브모양을 만들어 줄 것이기 때문에 정점을 8개로 만들어주었다.

struct Vertex
{
	XMFLOAT3 Pos;
	XMFLOAT4 Color;
};

// Create vertex buffer
Vertex vertices[] =
{
	{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4((const float*)&Colors::White)   },
	{ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4((const float*)&Colors::Black)   },
	{ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4((const float*)&Colors::Red)     },
	{ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4((const float*)&Colors::Green)   },
	{ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Blue)    },
	{ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Yellow)  },
	{ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Cyan)    },
	{ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Magenta) }
};

D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * 8;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = vertices;
HR(_device->CreateBuffer(&vbd, &vinitData, _vertexBuffer.GetAddressOf()));

 

이렇게 정점만 넘겨줘도 그려줄 수는 있지만 삼각형으로 그리다보면 겹치는 부분이 많이 생기기 때문에 인덱스 정보를 

같이 넘겨준다. 인덱스 버퍼도 버텍스 버퍼와 동일한 방식으로 만들어주면 된다.

// Create the index buffer

uint32 indices[] = {
	// front face
	0, 1, 2,
	0, 2, 3,

	// back face
	4, 6, 5,
	4, 7, 6,

	// left face
	4, 5, 1,
	4, 1, 0,

	// right face
	3, 2, 6,
	3, 6, 7,

	// top face
	1, 5, 6,
	1, 6, 2,

	// bottom face
	4, 0, 3,
	4, 3, 7
};

D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(uint32) * 36;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
ibd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = indices;
HR(_device->CreateBuffer(&ibd, &iinitData, _indexBuffer.GetAddressOf()));

 

그리고 shader를 사용할 때 Com객체처럼 사용하는데 이때 이름을 맞춰주면 가져오고 세팅할 때 연동이 돼서 편하게 사용

할 수 있다.

_fxWorldViewProj = _fx->GetVariableByName("gWorldViewProj")->AsMatrix();

_fxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));

 

VS단계를 보면 이렇게 정점이 월드 뷰 프로젝션 변환 행렬이 곱채져서 반환된다는 것을 볼 수 있다.

cbuffer cbPerObject
{
	float4x4 gWorldViewProj;
};

struct VertexIn
{
	float3 PosL  : POSITION;
    float4 Color : COLOR;
};

struct VertexOut
{
	float4 PosH  : SV_POSITION;
    float4 Color : COLOR;
};

VertexOut VS(VertexIn vin)
{
	VertexOut vout;
	
	// Transform to homogeneous clip space.
	vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
	
	// Just pass vertex color into the pixel shader.
    vout.Color = vin.Color;
    
    return vout;
}

 

그 다음 물체가 어떻게 생겼는지 묘사해주는 Input Layout 단계이다. 이때 쉐이더의 구조체 변수와 클래스 변수와 이름과 구조를 동일하게 맞춰주어야 한다.

void BoxDemo::BuildVertexLayout()
{
	// Create the vertex input layout.
	D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
	{
		{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
		{"COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}
	};

	// Create the input layout
	D3DX11_PASS_DESC passDesc;
	_tech->GetPassByIndex(0)->GetDesc(&passDesc);

	HR(_device->CreateInputLayout(vertexDesc, 2, passDesc.pIAInputSignature, passDesc.IAInputSignatureSize, _inputLayout.GetAddressOf()));
}

 

그 다음에는 렌더타켓뷰와 깊이 스텐실뷰를 클리어해주는 것으로 그림 그릴 도화지를 밀어주고 IA단계의 인풋레이아웃을 묘사한 부분을 실제 DeviceContext와 연결시켜주고 물체를 어떤 기본 물체로 그려줄지 세팅하는 토폴로지 단계를 하는 것을 볼 수 있다. 정점버퍼와 인덱스버퍼도 연결시켜준다.

_deviceContext->IASetInputLayout(_inputLayout.Get());
_deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

uint32 stride = sizeof(Vertex);
uint32 offset = 0;
_deviceContext->IASetVertexBuffers(0, 1, _vertexBuffer.GetAddressOf(), &stride, &offset);
_deviceContext->IASetIndexBuffer(_indexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);

 

이렇게 해준다음 월드,뷰,프로젝션 변환행렬을 쉐이더쪽에 정보를 넘겨주고 드려주면 된다. 인덱스 버퍼를 통해 그려주는 DrawIndexed를 사용하는 것을 볼 수 있다.

그려준 도화지를 Present를 통해 제출한다.

// Set constants
XMMATRIX world = ::XMLoadFloat4x4(&_world);
XMMATRIX view = ::XMLoadFloat4x4(&_view);
XMMATRIX proj = ::XMLoadFloat4x4(&_proj);
XMMATRIX worldViewProj = world * view * proj;

_fxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));

D3DX11_TECHNIQUE_DESC techDesc;
_tech->GetDesc(&techDesc);
for (uint32 p = 0; p < techDesc.Passes; ++p)
{
	_tech->GetPassByIndex(p)->Apply(0, _deviceContext.Get());

	// 36 indices for the box.
	_deviceContext->DrawIndexed(36, 0, 0);
}

HR(_swapChain->Present(0, 0));

 

이렇게 해주면 위의 큐브가 보이게 되는 것이다.

강의 링크

https://inf.run/1HYWy

 

학습 페이지

 

www.inflearn.com

 

파이프라인을 설명할 때는 처음부터 각 단계에 집중하기보다는 이 파이프라인으로 무엇을 해주는 지를 포괄적으로 

설명 하는 것이 좋다.

3D 게임을 만든다고 했을 때 3D 게임 세상을 2D화면으로 표현해주기 위해 변환을 해줘야하는 데 변환해주는 일련의 규칙적인 단계들을 모아서 렌더링 파이프라인이라고 한다. 파이프라인의 단계는 밑의 그림과 같다 이런 작업을 GPU를 통해 이루어지게 된다.

https://learn.microsoft.com/ko-kr/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline

 

● IA(Input Asssembler)단계

입력을 조립하는 단계로 정점을 건네주고 그 정점이 삼각형이나 선, 점인지 어떤식으로 구성되어 있는 지에 관한 정보와 

정점만을 건네주면 리소스가 너무 커지기 때문에 색인정보, 즉 인덱스 정보를 같이 넘겨준다. 이때는 좌표가 아직까지 로컬

좌표이다.

이때 로컬좌표는 자기 자신의 좌표계를 기준으로 하는 좌표로 자신의 원점을 중심으로 기하학적으로 어떻게 생겼는지에 관한 좌표정보이다.

 

● VS(Vertex Shader)단계

이 단계에서는 물체를 이루는 기본 도형인 삼각형의 정점별로 실행되는 단계로 World, View, Projection변환 행렬을 곱해주는 것으로 로컬좌표의 물체를 실제 월드에 배치할 수 있다.

 

● RS(Rasterizer State)단계

이 단계는 카메라의 값에 따라 그려주지 않아도 되는 오브젝트를 걸러주는 클래핑과 같은 작업과 후면으로 된 오브젝트는 그려주지 않는 등의 작업을 해주고 마지막으로 보간 작업을 해줘서 정점을 픽셀 단위로 만들어 준다.

● PS(Pixel Shader)단계

픽셀 단위로 넘어온 것의 색상을 정해주는 단계로 일반적인 물체의 색상 뿐만 아니라 여러가지 조명 연산이 적용된 색상까지 반영해주게 된다.

 

● OM(Ouput Merge)단계

색상까지 정해진 후에 텍스처나 렌더타켓을 그려주거나 렌더타켓 뷰를 설정해서 Swap Chain을 연결해준 다음 후면 버퍼 

전면 버퍼에 최종적으로 그려주게 되는 단계이다. 그려주고 Present를 해주면 화면에 출력 된다.

 

● 행렬

 

SRT연산에서 행렬연산을 많이 해주게 되는데 이때 x y z 에서 1을 붙여주는 이유는 선형 변환을 위해서 필요하다.

만약 이 부분이 0이 된다면 선형 이동을 하지 않는다. 방향벡터를 이용해서 방향만 적용할 때 이렇게 사용하면 회전

만 적용된다. 

1을 붙여주면 TransformCode 이고 0을 붙여주면 TransformNormal 이라고 보면 된다.

 

행렬 연산에서 우선 순위는 Scale Rotation(자전) Translation Roation(공전) Parent(계층) 이다.

 

+ Recent posts