일단, 조명을 추가하기 전 RenderBase의 구성을 살짝 바꿔주었다.
기존에는 init()이 순수가상함수여서, 하위 클래스에서 구현해주도록 했지만 조명에 대한 상수버퍼는 모든 렌더러가 보유하고 있도록 하기 위해 부모함수의 Init()에 조명에 대한 상수버퍼를 생성하는 코드를 작성하고 하위 클래스의 Init에선 Renderbase::Init()을 호출해주도록 하였다.
하지만, 이렇게 수동으로 부모 클래스의 함수를 호출하는 경우, 실수로 빼먹을 수도 있다.
이를 방지하기 위해 bool값을 하나 선언하였다.
렌더러를 생성하는 함수에서 렌더러의 init함수를 호출한 뒤, 렌더러의 bool값이 false라면 콘솔창에 메세지를 출력해주도록 설정하여, Init을 호출하지 않은 경우 이를 바로 알아볼 수 있도록 해주었다.
void RenderBase::Init()
{
isCallInit = true;
}
RenderBase의 init에 일단 isCallInit을 true로 만드는 코드를 추가해주었다.
template <typename T>
static std::shared_ptr<T> CreateRenderer()
{
std::shared_ptr<class RenderBase> NewRenderer = std::make_shared<T>();
NewRenderer->Init();
if (NewRenderer->isCallInitFunction() == false)
{
std::cout << NewRenderer->Name << " : RenderBase::Init() is not called. " << std::endl;
}
EngineBase::GetInstance().AddRenderer(NewRenderer);
return std::dynamic_pointer_cast<T>(NewRenderer);
}
렌더러를 생성하는 함수에서, 해당 렌더러의 isCallInit이 false라면, 콘솔창에 메세지를 출력해주도록 하였다.
bool isCallInitFunction()
{
return isCallInit;
}
isCallInitFunction은 그냥 bool값을 반환하는 함수이다. 아무래도, 해당 bool값은 init함수 이외의 다른 곳에서 변경되면 안되기 때문에 private로 안전하게 숨겨두고 함수를 통해 값을 확인할 수 있도록 하였다.
이제 조명을 구성해보자.
먼저 조명 정보를 담을 구조체를 하나 선언하였다.
struct LightData
{
DirectX::SimpleMath::Vector3 Strength = { 1.0f, 1.0f, 1.0f };
float FallOffStart = 0.0f;
DirectX::SimpleMath::Vector3 Direction = { 0.0f, 0.0f, 1.0f };
float FallOffEnd = 10.0f;
DirectX::SimpleMath::Vector3 Position = { 0.0f, 0.0f, -2.0f };
float SpotPower = 1.0f;
};
Strength는 빛의 강도이다.
FallOffStart와 FallOffEnd는 빛의 감쇠 거리이다.
빛과의 거리가 FallOfStart 이하 일 때 빛은 최대 강도이고, 거리가 멀어질수록 감쇠하다 FallOfEnd에 도달하면 빛은 사라지게 된다.
Direction은 빛의 방향이며, Positon은 광원의 위치이다.
#define LIGHT_NUM 3
struct LightCBuffer
{
DirectX::SimpleMath::Vector3 EyeWorld;
float Ambient = 0.1f;
Light Lights[LIGHT_NUM];
};
빛의 개수를 중간에 계속 바꿔가며 상수버퍼를 업데이트 하는 것은 매우 비효율적이기 때문에 컴파일 타임에 전역 조명의 개수를 미리 정해놓고, 고정된 개수의 조명을 사용하도록 하기 위해 위와 같이 LIGHT_NUM을 define한 뒤, 해당 개수에 맞는 조명을 가지고 있는 구조체를 만들어주었다.
Specular 를 계산하기 위해선 눈의 위치 정보가 필요하기 때문에 Eye의 WorldPositon도 함께 보내주었고, 저수준의 조명모델에선 ambient를 상수로 계산하는 것이 일반적이기 때문에 Ambient는 0.1f의 상수로 설정하였다.
LightCBuffer& GetWorldLight()
{
return WorldLight;
}
LightCBuffer WorldLight;
EnigineBase엔 위처럼 WorldLight변수를 선언하여 주었고, 이를 반환해주는 함수도 만들어주었다.
void RenderBase::Init()
{
isCallInit = true;
CreateConstantBuffer(EngineBase::GetInstance().GetWorldLight());
}
RenderBase의 init에서 조명에 대한 상수버퍼를 만들어주었다.
#define LIGHT_NUM 3
struct Light
{
float3 Strength;
float FallOffStart;
float3 Direction;
float FallOffEnd;
float3 Position;
float SpotPower;
};
cbuffer WorldLights : register(b0)
{
Light Lights[LIGHT_NUM];
};
쉐이더에서도 조명에 대한 정보를 추가해주었다.
이제, 쉐이더 코드를 작성할 차례이다.
버텍스 쉐이더에선 조명 계산을 하지 않고, 픽셀 쉐이더에서 할 것이다.
그렇기 때문에, 일부 트랜스폼 정보만 버텍스 쉐이더에서 계산한 뒤 픽셀쉐이더로 보내줄 것이다.
#include "LightHeader.hlsli"
cbuffer WorldLights : register(b0)
{
float3 EyeWorld;
float Ambient;
Light Lights[LIGHT_NUM];
};
cbuffer WorldViewProjection : register(b1)
{
matrix World;
matrix View;
matrix Projection;
};
struct VertexShaderInput
{
float3 pos : POSITION;
float3 Normal : NORMAL;
float2 TexCoord : TEXCOORD;
};
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float2 TexCoord : TEXCOORD0;
float3 WorldPos : POSITION1;
float3 WorldNormal : NORMAL;
};
PixelShaderInput main(VertexShaderInput _Input)
{
PixelShaderInput Output;
float4 Pos = float4(_Input.pos, 1.0f);
Pos = mul(Pos, World);
Pos = mul(Pos, View);
Pos = mul(Pos, Projection);
Output.pos = Pos;
Output.TexCoord = _Input.TexCoord;
Output.WorldPos = mul(float4(_Input.pos, 1.0f), World).rgb;
Output.WorldNormal = mul(float4(_Input.Normal, 0.0f), World).rgb;
return Output;
}
보면, WorldPos와 WorldNormal을 계산한 뒤, 픽셀 쉐이더로 전달해주고 있다.
조명 계산을 world공간에서 진행할 것이기 때문이다.
#include "LightHeader.hlsli"
cbuffer WorldLights : register(b0)
{
float3 EyeWorld;
float Ambient;
Light Lights[LIGHT_NUM];
};
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float2 TexCoord : TEXCOORD;
float3 WorldPos : POSITION1;
float3 WorldNormal : NORMAL;
};
Texture2D DiffuseTexture : register(t0);
SamplerState Sampler : register(s0);
float4 main(PixelShaderInput _Input) : SV_TARGET
{
float4 Color = DiffuseTexture.Sample(Sampler, _Input.TexCoord);
float LightSum = 0.0f;
float DiffuseLight = CalDiffuseLight(Lights[0], _Input.WorldNormal);
float SpecularLight = CalSpecular_Phong(Lights[0], _Input.WorldNormal, EyeWorld - _Input.WorldPos);
float AmbientLight = Ambient;
LightSum = DiffuseLight + SpecularLight + AmbientLight;
Color.rgb *= LightSum;
return Color;
}
픽셀 쉐이더에선 이렇게 WorldPos와 worldNormal을 받아서 DiffuseLight와 SpecularLight를 계산하는 함수로 보내주었다.
계산된 총 합을 현재 색상에 곱함으로써 결과 색상을 도출하였다.
조명 계산 함수는 아래와 같다.
float CalDiffuseLight(Light _Light, float3 _Normal)
{
float3 LightDir = -_Light.Direction;
LightDir = normalize(LightDir);
float3 Normal = normalize(_Normal);
float CosTheta = dot(LightDir, Normal);
return CosTheta;
}
디퓨즈 라이트 계산이다. 간단하게 빛의 방향과 노멀벡터를 내적하여 값을 구해주었다.
float CalSpecular_Phong(Light _Light, float3 _Normal, float3 _EyeDir)
{
float3 LightDir = _Light.Direction;
LightDir = normalize(LightDir);
float3 Normal = _Normal;
Normal = normalize(Normal);
float3 ReflectedLightDir = reflect(LightDir, Normal);
float3 EyeDir = _EyeDir;
EyeDir = normalize(EyeDir);
float Specular = max(0.0f, dot(ReflectedLightDir, EyeDir));
Specular = pow(Specular, 32.0f);
return max(Specular, 0.0f);
}
위 코드는 스페큘러 라이트 계산식이다.
원래는 FallOffStart와 FallOffEnd도 적용해야 하고 이것저것 추가해야할게 많지만, 테스트를 하면서 천천히 추가하기 위해 기초적인 계산식만 작성하였다.
조명을 계산하기 위해, 렌더러의 트랜스폼 구조도 살짝 바꿔주었다.
시점과 관련된 뷰행렬은 총 1개만 존재한다고 가정하여, EngineBase에 선언해주었다.
그리고, 모든 렌더러는 EngineBase의 뷰행렬을 사용하여 트랜스폼을 계산하도록 하였다.
WorldLight.EyeWorld = DirectX::SimpleMath::Vector3::Transform(DirectX::SimpleMath::Vector3(0.0f), ViewMat.Invert());
EyeWorld는 (0.0f, 0.0f, 0.0f)에 뷰행렬의 역행렬을 곱해서 계산하였다.
ViewMat = DirectX::XMMatrixLookAtLH(EyePos, FocusPos, UpDir);
ViewMat *= DirectX::SimpleMath::Matrix::CreateRotationX(ViewRot.x) *
DirectX::SimpleMath::Matrix::CreateRotationY(ViewRot.y) *
DirectX::SimpleMath::Matrix::CreateRotationZ(ViewRot.z);
IMGUI를 이용해 뷰행렬의 회전을 조작하기 위해 ViewRot이라는 vector3를 선언하였고, 이를 이용해 뷰행렬을 프레임마다 갱신해줄 것이다.
일단 여기까지 적용한 뒤 테스트를 해보자.
얼추 잘 실행되는 듯 하다.
스페큘러도 잘 적용되는 듯 하고, 뷰행렬의 회전도 잘 적용된다.
다음엔 Specular의 강도를 조절하기 위한 shiness와 전체 빛의 강도를 조절할 Strengh, 그리고 빛의 감쇠까지 적용해 볼 예정이다.
'프로젝트 > Direct X 그래픽스' 카테고리의 다른 글
프로젝트 : DirectX를 활용한 그래픽스 (14 - 구조 수정) (0) | 2024.05.12 |
---|---|
프로젝트 : DirectX를 활용한 그래픽스 (13 - 조명 추가 (2) ) (0) | 2024.05.11 |
프로젝트 : DirectX를 활용한 그래픽스 (11 - IMGUI, 델타타임 추가) (0) | 2024.05.07 |
프로젝트 : DirectX를 활용한 그래픽스 (10 - 텍스쳐링) (0) | 2024.05.07 |
프로젝트 : DirectX를 활용한 그래픽스 (9 - 상수버퍼 구조 변경) (0) | 2024.05.06 |