Global Shader
이전에 작업하던 방식은 MeshRenderer 컴포넌트에서 .fx 파일의 쉐이더 에서 cubffer GLOBAL에 선언된 World, View, Projection 등의 행렬에 직접 접근해 Const Buffer에서 설정해 주었다. 이는 엄청난 편리함을 가져다 주는 장점이 있었다. 그런데 이 방식으로 고집해서 계속 하게되면 진지한 게임을 제작하는데 있어 문제가 발생한다.
이렇게 하게되면 한가지 변수만 바뀌어도 나머지 변수들도 다 Update를 해주어야 하는 문제가 발생한다.
그렇기 때문에 이를 나눠 관리할 필요성이 있다.
World, View, Projection에서 개개인의 물체마다 다르게 관리해야할 것은 바로 World 이고 나머지 행렬 두개는 묶어서 한번에 관리가 가능하다. 가령 World는 정점마다 다 계산해서 매 프레임 업데이트 해줘야하지만 View나 Projection은 바뀌는 경우가 많이 없으므로 이는 한번만 업데이트하고 가져다 쓰는 형식인 것이다.
그러니 공용으로 뺄 수 있는 쉐이더는 Global.fx 라는 파일로 정리해서 Include 해서 사용하도록 하자.
#ifndef _GLOBAL_FX_ // 헤더가드
#define _GLOBAL_FX_
/////////////////
// ConstBuffer //
/////////////////
cbuffer GlobalBuffer
{
matrix V;
matrix P;
matrix VP;
};
cbuffer TransformBuffer
{
matrix World;
};
////////////////
// VertexData //
////////////////
struct Vertex
{
float4 position : POSITION;
};
struct VertexTexture
{
float4 position : POSITION;
float2 uv : TEXCOORD;
};
struct VertexColor
{
float4 Position : POSITION;
float4 Color : COLOR;
};
struct VertexTextureNormal
{
float4 position : POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
};
//////////////////
// VertexOutput //
//////////////////
struct VertexOutput
{
float4 position : SV_POSITION;
float2 uv : TEXCOORD;
float3 normal : NORMAL;
};
//////////////////
// SamplerState //
//////////////////
SamplerState LinearSampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};
SamplerState PointSampler
{
Filter = MIN_MAG_MIP_POINT;
AddressU = Wrap;
AddressV = Wrap;
};
/////////////////////
// RasterizerState //
/////////////////////
RasterizerState FillModeWireFrame
{
FillMode = WireFrame;
};
///////////
// Macro //
///////////
#define PASS_VP(name, vs, ps) \
pass name \
{ \
SetVertexShader(CompileShader(vs_5_0, vs())); \
SetPixelShader(CompileShader(ps_5_0, ps())); \
}
//////////////
// Function //
//////////////
#endif
이제 원하는 쉐이더에 이것을 집어넣어 사용할 수 있다.
이제 이 ConstBuffer에 값을 넣기 위해 RenderManager를 만들어서 원하는 데이터를 삽입하도록 하자.
#pragma once
#include "ConstantBuffer.h"
class Shader;
struct GlobalDesc
{
Matrix V = Matrix::Identity;
Matrix P = Matrix::Identity;
Matrix VP = Matrix::Identity;
};
struct TransformDesc
{
Matrix W = Matrix::Identity;
};
class RenderManager
{
DECLARE_SINGLE(RenderManager);
public:
void Init(shared_ptr<Shader> shader);
void Update();
void PushGlobalData(const Matrix& view, const Matrix& projection);
void PushTransfomrData(const TransformDesc& desc);
private:
shared_ptr<Shader> _shader;
GlobalDesc _globalDesc;
shared_ptr<ConstantBuffer<GlobalDesc>> _globalBuffer;
ComPtr<ID3DX11EffectConstantBuffer> _globalEffectBuffer;
TransformDesc _transformDesc;
shared_ptr<ConstantBuffer<TransformDesc>> _transformBuffer;
ComPtr<ID3DX11EffectConstantBuffer> _transformEffectBuffer;
};
#include "pch.h"
#include "RenderManager.h"
#include "Camera.h"
void RenderManager::Init(shared_ptr<Shader> shader)
{
_shader = shader;
_globalBuffer = make_shared<ConstantBuffer<GlobalDesc>>();
_globalBuffer->Create();
_globalEffectBuffer = _shader->GetConstantBuffer("GlobalBuffer");
_transformBuffer = make_shared<ConstantBuffer<TransformDesc>>();
_transformBuffer->Create();
_transformEffectBuffer = _shader->GetConstantBuffer("TransformBuffer");
}
void RenderManager::Update()
{
PushGlobalData(Camera::S_MatView, Camera::S_MatProjection);
}
void RenderManager::PushGlobalData(const Matrix& view, const Matrix& projection)
{
_globalDesc.V = view;
_globalDesc.P = projection;
_globalDesc.VP = view * projection;
_globalBuffer->CopyData(_globalDesc);
_globalEffectBuffer->SetConstantBuffer(_globalBuffer->GetComPtr().Get());
}
void RenderManager::PushTransfomrData(const TransformDesc& desc)
{
_transformDesc = desc;
_transformBuffer->CopyData(_transformDesc);
_transformEffectBuffer->SetConstantBuffer(_transformBuffer->GetComPtr().Get());
}
Depth Stencil View
Depth Stencil View 같은 경우는 3D 게임에서 매우 중요한 개념이다. 이는 이해하기 쉽지 않아 실제 코드를 보면서 이해는 것이 훨씬 빠르다.
#include "pch.h"
#include "11. DepthStencilDemo.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
void DepthStencilDemo::Init()
{
_shader = make_shared<Shader>(L"08. GlobalTest.fx");
RESOURCES->Init();
// Camera
_camera = make_shared<GameObject>();
_camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f,0.f,-10.f });
_camera->AddComponent(make_shared<Camera>());
_camera->AddComponent(make_shared<CameraScript>());
// Object
_obj = make_shared<GameObject>();
_obj->GetOrAddTransform();
_obj->AddComponent(make_shared<MeshRenderer>());
{
_obj->GetMeshRenderer()->SetShader(_shader);
}
{
auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
_obj->GetMeshRenderer()->SetMesh(mesh);
}
{
auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
_obj->GetMeshRenderer()->SetTexture(texture);
}
_obj2 = make_shared<GameObject>();
_obj2->GetOrAddTransform()->SetPosition(Vec3{ 0.5f, 0.f, 2.f });
_obj2->AddComponent(make_shared<MeshRenderer>());
{
_obj2->GetMeshRenderer()->SetShader(_shader);
}
{
auto mesh = RESOURCES->Get<Mesh>(L"Cube");
_obj2->GetMeshRenderer()->SetMesh(mesh);
}
{
auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
_obj2->GetMeshRenderer()->SetTexture(texture);
}
RENDER->Init(_shader);
}
void DepthStencilDemo::Update()
{
_camera->Update();
RENDER->Update();
_obj->Update();
_obj2->Update();
}
void DepthStencilDemo::Render()
{
}
이렇게 오브젝트 Sphere와 Cube를 만들고 Cube를 Sphere보다 더 뒤쪽으로 배치를 해 놓았다.
하지만 결과를 보면 묘하게도 Cube가 더 앞에 있는 것 처럼 보인다. 실제로 다가가서 확인해보면 Shpere가 앞쪽에 위치하지만 정면에서 확인했을 때는 Cube가 앞에 있어 보인다.
이러한 문제는 2D 때도 마주칠 수 있었는데, 바로 물체를 그리는 순서 때문이다. 만약 Update의 순서를 바꿔 Sphere를 나중에 그리게 되면 우리가 원래 예상한 결과를 얻을 수 있다.
이전에 쉐이더를 코딩할 때 우리는 한개의 물체만을 고려해서 코드를 작성했다. 두 개 이상의 물체에 대한 고려를 하지 않았기 때문에 당연한 결과이다.
해결방법은 특정 물체를 그릴 때, 그 물체가 그려지기 이전에 특정 픽셀 마다 더 앞에 있는 픽셀이 존재한다면 그리지 않는다. 라는 조건이 필요하다는 것이다.
이제 이걸 엔진단에서 해결하게 된다.
화면 크기와 동일한 빈 텍스처를 만든다. 이 텍스처는 물체들의 깊이 값을 저장하는 역할을 한다.
D3D11_TEXTURE2D_DESC desc = { 0 };
ZeroMemory(&desc, sizeof(desc));
desc.Width = static_cast<uint32>(GAME->GetGameDesc().width);
desc.Height = static_cast<uint32>(GAME->GetGameDesc().height);
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
desc.SampleDesc.Count = 1;
desc.SampleDesc.Quality = 0;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
desc.CPUAccessFlags = 0;
desc.MiscFlags = 0;
HRESULT hr = DEVICE->CreateTexture2D(&desc, nullptr, _depthStencilTexture.GetAddressOf());
CHECK(hr);
이 텍스처가 어떻게 사용되고 분석될 것인지 View를 생성해 정해준다.
D3D11_DEPTH_STENCIL_VIEW_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
desc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
desc.Texture2D.MipSlice = 0;
HRESULT hr = DEVICE->CreateDepthStencilView(_depthStencilTexture.Get(), &desc, _depthStencilView.GetAddressOf());
CHECK(hr);
그리고 이제 OM 단계에서 이 View를 지정하게 되면 render 하는 깊이를 이 View Texture에 전부 저장하게 되고 이를 가지고 판별해 물체를 그릴지 안그릴지를 판단한다.
void Graphics::RenderBegin()
{
_deviceContext->OMSetRenderTargets(1, _renderTargetView.GetAddressOf(), _depthStencilView.Get());
_deviceContext->ClearRenderTargetView(_renderTargetView.Get(), (float*)(&GAME->GetGameDesc().clearColor));
_deviceContext->ClearDepthStencilView(_depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1, 0);
_deviceContext->RSSetViewports(1, &_viewport);
}
이렇게 OMset 함수를 통해 _depthStencilView를 세팅하고 이전 프레임에 설정한 Depth 값을 Clear 함수를 통해 밀어줘야 정상작동 하게 된다. Clear 할 때, 값을 0 이 아닌 1로 밀게 되는데 그 이유는 카메라의 영역에 관계가 있다.
우리가 SRT를 통해 물체를 월드 좌표로 변환하고 그걸 다시 카메라 좌표계ㅡ 투영, 스크린 좌표계로 바꾸게 되는데 이때, 깊이의 값은 0~1 사이로 결정되기 때문이다. 즉 0은 카메라와 딱 붙어 있는 상태라는 것이고 1은 카메라가 찍을 수 있는 가장 먼 거리를 의미한다.
이제 Object의 그리는 순서 없이 깊이 값에 따라 그려지게 된다.
이는 Depth랑만 관련 있는데 그럼 Stencil은 무엇일까?
미술에서 쉽게 Stencil은 구멍을 파서 그 구멍으로 그림을 만드는 기법을 의미하는데 이와같은 방식으로 특정 픽셀들을 구멍내서 물체를 보여주는 고급 기법을 의미한다. 지금 당장에는 사용하진 않을것인데 그 역할을 같이 해주는 녀석이 DepthStencilView이다.
Ambient
조명 4총사 중 먼저 Ambient부터 알아보자.
일단 현실세계에 있는 조명을 게임에서 구현하는 것은 불가능하다고 볼 수 있다. 그래픽스에서는 얼마나 "최소한"의 비용으로 "높은" 퀄리티를 낼 수 있는가? 현실세계와 최대한 비슷하게 연출 해낼 수 있는가 를 연구한다.
Ambient는 한마디로 환경광을 의미한다. 최초 발사된 빛들이 물체에 반사되어 다른 물체를 밝히는 것, 그래서 모든 물체는 빛이 1이라도 존재한다는 것을 의미한다. 즉, 광원이 불분명한 빛을 의미한다.
결론적으로 일정한 빛의 색과 일정한 빛의 양을 지닌다는게 특징이다.
float4 LightAmbient;
float4 MaterialAmbient;
LightAmbient는 광원 자체의 색을 의미하고 MaterialAmbient는 물체마다 흡수하는 색을 의미한다.
PS 단계를 아래와 같이하고
// Ambient (주변광/환경광)
// 수많은 반사를 거쳐서 광원이 불분명한 빛
// 일정한 밝기와 색으로 표현
float4 PS(VertexOutput input) : SV_TARGET
{
float4 color = LightAmbient * MaterialAmbient;
return color;
//return Texture0.Sample(LinearSampler, input.uv);
}
빛의 색상을 은은한 빨간색으로 설정한 후 실행하면 다음과 같다.
// 빛
Vec4 lightAmbient{ 0.5f,0.f,0.f,1.f };
_shader->GetVector("LightAmbient")->SetFloatVector((float*)&lightAmbient);
{
// 모든 빛을 흡수한다.
Vec4 materialAmbient(1.f);
_shader->GetVector("MaterialAmbient")->SetFloatVector((float*)&materialAmbient);
_obj->Update();
}
{
Vec4 materialAmbient(1.f);
_shader->GetVector("MaterialAmbient")->SetFloatVector((float*)&materialAmbient);
_obj2->Update();
}
만약 빛의 색은 그대로 인데 materialAmbient 값을 작게 만들면 어떻게 될까? 물체마다 빛을 어느정도 만큼 받을 것인지를 결정하기 때문에(LightAmbient * MaterialAmbient 이기 때문) 검정색에 가깝게 표현이 될것이다.
물론 이것만 가지고 빛을 표현하진 않고 뒤에 나올 여러가지를 섞는게 일반적이다. 보여주고 싶은 텍스처에 특정 색상을 강조하고 싶다던지 등등을 말이다.
중요한 것은 방향이 없다는 점이다. 모두 균일하게 동작한다는 점이 특징이다.
Diffuse
Diffuse light는 분산광이라고 한다. 이는 물체의 표면에서 분산되어 눈으로 들어오는 빛을 의미한다. 각도에 따라 빛의 세기가 달라진다는 점이 특징이다. 이는 이전 포스팅에도 기재되어 있는 내용이다.
각 정점에 대한 수직인 정점벡터와 빛의 방향 벡터를 내적해서 그 크기를 구하는 것이다.
float3 LightDir;
float4 LightDiffuse;
float4 MaterialDiffuse;
Texture2D DiffuseMap;
// Diffuse (분산광)
// 물체의 표면에서 분산되어 눈으로 바로 들어오는 빛
// 각도에 따라 밝기가 다르다.(Lambert 공식)
float4 PS(VertexOutput input) : SV_TARGET
{
float4 color = DiffuseMap.Sample(LinearSampler, input.uv);
float value = dot(-LightDir, normalize(input.normal));
color = color * value * LightDiffuse * MaterialDiffuse;
return color;
}
// 빛
Vec4 lightDiffuse{ 1.f,1.f,1.f,1.f };
_shader->GetVector("LightDiffuse")->SetFloatVector((float*)&lightDiffuse);
Vec3 lightDir{ 1.f,-1.f,1.f };
lightDir.Normalize();
_shader->GetVector("LightDir")->SetFloatVector((float*)&lightDir);
{
// 물체가 빛을 얼마나 흡수하냐
Vec4 material(1.f);
_shader->GetVector("MaterialDiffuse")->SetFloatVector((float*)&material);
_obj->Update();
}
{
Vec4 material(1.f);
_shader->GetVector("MaterialDiffuse")->SetFloatVector((float*)&material);
_obj2->Update();
}
하지만 이도 조금 어색한데 뒷면이 아예 안보인다는 점이 문제이다. 즉, 위에서 배운 Ambient와 함께 사용해서 빛을 조립해서 사용하면 해결할 수 있다.
Specular
Diffuse랑 비슷하지만 살짝 다르다. 빛이 물체에 반사되어 나가는 빛과 우리의 눈이 물체를 바라보고 있는 그 사이 각에 따라 빛의 세기를 연출하는 것이다.
Diffuse는 빛과 물체의 방향벡터 사이의 각도를 체크하는 것이고 Specular는 반사된 빛이 우리 가 바라보고 있는 시선 사이의 각도를 체크하는 것이다.
물체의 특정 부분이 반짝 거리는 효과를 주는 것이다.
#include "00. Global.fx"
float3 LightDir;
float4 LightSpecular;
float4 MaterialSpecular;
Texture2D DiffuseMap;
MeshOutput VS(VertexTextureNormal input)
{
MeshOutput output;
output.position = mul(input.position, World);
output.worldPosition = input.position;
output.position = mul(output.position, VP);
output.uv = input.uv;
output.normal = mul(input.normal, (float3x3)World);
return output;
}
// Specular (반사광)
// 한방향으로 완전히 반사되는 빛 (Phong)
float4 PS(MeshOutput input) : SV_TARGET
{
// float3 R = reflect(LightDir, input.normal);
float3 R = LightDir - (2 * input.normal * dot(LightDir, input.normal));
R = normalize(R);
float3 cameraPosition = -V._41_42_43;
float3 E = normalize(cameraPosition - input.worldPosition);
float value = saturate(dot(R, E)); // clamp(0, 1)
float specular = pow(value, 10);
float4 color = LightSpecular * MaterialSpecular * specular;
return color;
}
technique11 T0
{
PASS_VP(P0, VS, PS)
};
수식을 이리저리 계산하면 위에 PS 단계가 나온다.
Lighting 통합하기
이제 위에 사용된 위의 빛 3가지를 혼합해서 사용하면 적절한 빛 연출을 할 수 있다.
#ifndef _LIGHT_FX_
#define _LIGHT_FX_
#include "00. Global.fx"
////////////
// Struct //
////////////
struct LightDesc
{
float4 ambient;
float4 diffuse;
float4 specular;
float4 emissive;
float3 direction;
float padding;
};
struct MaterialDesc
{
float4 ambient;
float4 diffuse;
float4 specular;
float4 emissive;
};
/////////////////
// ConstBuffer //
/////////////////
cbuffer LightBuffer
{
LightDesc GlobalLight;
};
cbuffer MaterialBuffer
{
MaterialDesc Material;
};
/////////
// SRV //
/////////
Texture2D DiffuseMap;
Texture2D SpecularMap;
Texture2D NormalMap;
//////////////
// Function //
//////////////
float4 ComputeLight(float3 normal, float2 uv, float3 worldPosition)
{
float4 ambientColor = 0;
float4 diffuseColor = 0;
float4 specularColor = 0;
float4 emissiveColor = 0;
// Ambient
{
float4 color = GlobalLight.ambient * Material.ambient;
ambientColor = DiffuseMap.Sample(LinearSampler, uv) * color;
}
// Diffuse
{
float4 color = DiffuseMap.Sample(LinearSampler, uv);
float value = dot(-GlobalLight.direction, normalize(normal));
diffuseColor = color * value * GlobalLight.diffuse * Material.diffuse;
}
// Specular
{
//float3 R = reflect(GlobalLight.direction, normal);
float3 R = GlobalLight.direction - (2 * normal * dot(GlobalLight.direction, normal));
R = normalize(R);
float3 cameraPosition = CameraPosition();
float3 E = normalize(cameraPosition - worldPosition);
float value = saturate(dot(R, E)); // clamp(0~1)
float specular = pow(value, 10);
specularColor = GlobalLight.specular * Material.specular * specular;
}
// Emissive
{
float3 cameraPosition = CameraPosition();
float3 E = normalize(cameraPosition - worldPosition);
float value = saturate(dot(E, normal));
float emissive = 1.0f - value;
// min, max, x
emissive = smoothstep(0.0f, 1.0f, emissive);
emissive = pow(emissive, 2);
emissiveColor = GlobalLight.emissive * Material.emissive * emissive;
}
return ambientColor + diffuseColor + specularColor + emissiveColor;
}
#endif
ComputeLight 함수를 이용해서 공용적으로 빛을 작업할 수 있다.
은은하게 빛나는 곳은 Ambient로 일반적인 곳은 Diffuse로 강조하고 싶은 곳은 Specular가 필요하다는 것을 확인할 수 있다.
'C++ > DirectX 11' 카테고리의 다른 글
[DirectX] 모델 - Assimp, Material 로딩, Mesh 로딩 (0) | 2024.09.27 |
---|---|
[DirectX] Lighting & Material - Material, Normal Mapping (0) | 2024.09.25 |
[DirectX] DirectX 11 3D 입문 - Height Map, Normal, Mesh (0) | 2024.09.13 |
[DirectX] DirectX 11 3D 입문 - 카메라, 텍스처, Geometry (0) | 2024.09.12 |
[DirectX] DirectX 11 3D 입문 - 프로젝트 설정 및 간단한 실습 (0) | 2024.09.11 |