[DirectX] 2차원 광원
in Study on WinAPI&DirectX
2차원에서 빛을 관리하는 광원에 대해서 알아볼 것이다.
광원 관련 정보 설계
// struct.h 파일 중에서
struct tLightColor
{
Vec4 vDiffuse; // 빛의 색상
Vec4 vAmbient; // 주변 광(환경 광)
};
// LightInfo
struct tLightInfo
{
tLightColor Color; // 빛의 색상
Vec4 vWorldPos; // 광원의 월드 스페이스 위치
Vec4 vWorldDir; // 빛을 보내는 방향
UINT LightType; // 빛의 타입(방향성, 점, 스포트)
float Radius; // 빛의 반경(사거리)
float Angle; // 빛의 각도
int Padding;
};
위와 같이 구조체를 설계해준뒤 광원을 관장하는 CLight2D 컴포넌트에 추가할 것이다.
크기를 16바이트 단위로 설계해줘야하는 점을 주의하자.
CLight2D 클래스
// 헤더파일
#pragma once
#include "CComponent.h"
class CLight2D :
public CComponent
{
private:
tLightInfo m_LightInfo;
public:
void SetLightType(LIGHT_TYPE _Type) { m_LightInfo.LightType = (UINT)_Type; }
void SetLightDirection(Vec3 _vDir) { m_LightInfo.vWorldDir = _vDir; }
void SetLightDiffuse(Vec3 _vDiffuse) {m_LightInfo.Color.vDiffuse = _vDiffuse; }
void SetLightAmbient(Vec3 _vAmbient) {m_LightInfo.Color.vAmbient = _vAmbient; }
void SetRadius(float _Radius) { m_LightInfo.Radius = _Radius; }
void SetAngle(float _Angle) { m_LightInfo.Angle = _Angle; }
public:
virtual void finaltick() override;
CLONE(CLight2D);
public:
CLight2D();
~CLight2D();
};
// cpp 파일
#include "pch.h"
#include "CLight2D.h"
#include "CRenderMgr.h"
#include "CTransform.h"
CLight2D::CLight2D()
: CComponent(COMPONENT_TYPE::LIGHT2D)
{
}
CLight2D::~CLight2D()
{
}
void CLight2D::finaltick()
{
m_LightInfo.vWorldPos = Transform()->GetWorldPos();
CRenderMgr::GetInst()->RegisterLight2D(m_LightInfo);
}
헤더파일의 Set 함수들에서 빛의 속성을 각각 결정해준다.
그리고 cpp 파일의 finaltick 함수에서 본인의 World좌표를 Transform으로부터 받아서 세팅해준다.
그러고 난다음에 Rendering을 진행해준다.
CRenderMgr 클래스
// 헤더파일 중에서
void RegisterLight2D(const tLightInfo& _Light2D) { m_vecLight2D.push_back(_Light2D); }
// cpp 파일 중에서
void CRenderMgr::Clear()
{
m_vecLight2D.clear();
}
광원을 관장하는 벡터 m_vecLight2D를 추가한다.
그리고 광원은 카메라와 달리 여러개일 수 있고, 중간에 추가 및 삭제 될 수 있으므로 Register함수를 구현한다.
그리고 렌더링이 마무리 되면 Clear함수를 호출한다.
이 Clear함수에서는 Light벡터를 비워줘서 다음 프레임에서 광원의 finaltick함수를 다시 수행할 수 있도록 만들어 준다.
define.h
광원의 종류를 아래와 같이 구현해준다.
enum class LIGHT_TYPE
{
DIRECTIONAL,
POINT,
SPOT
}
광원 생성 및 적용해보기
CLevelMgr 클래스
LevelMgr에서 광원을 추가하는 코드를 아래와 같이 작성한다.
// cpp 파일 init 함수 중에서
// 광원 추가
CGameObject* pLightObj = new CGameObject;
pLightObj->SetName(L"Directional Light");
pLightObj->AddComponent(new CTransform);
pLightObj->AddComponent(new CLight2D);
pLightObj->Light2D()->SetLightType(LIGHT_TYPE::DIRECTIONAL);
pLightObj->Light2D()->SetLightDiffuse(Vec3(1.f, 1.f, 1.f));
m_pCurLevel->AddGameObject(pLightObj, 0, false);
CGameObject클래스
그리고 광원도 게임 오브젝트이므로 헤더파일의 코드에 아래와 같이 광원을 Set 하는 코드를 추가해준다.
// 헤더파일 중에서
CLight2D* Light2D() const { return (CLight2D*)m_arrCom[(UINT)COMPONENT_TYPE::LIGHT2D]; }
광원의 종류
enum class LIGHT_TYPE
{
DIRECTIONAL,
POINT,
SPOT
}
위 코드에서 언급되었듯이 빛의 종류는 3가지가 있다.
- 방향광 (Directional)
- 점광 (Point)
- 스포트라이트 (Spot)
방향광
주로 태양광을 표현할 때 사용된다.
사실 약 10m 떨어진 물체가 받는 빛의 진행 방향은 완벽하게 일치하지 않는다.
하지만 그 오차가 매우 미미하기 때문에 마치 빛이 동일한 방향으로 진행하는 것처럼 보이게 된다.
이 현상을 구현할 때 방향광을 사용한다.
그리고 오픈월드 게임에서 낮과 밤을 표현할 때 시간이 지남에 따라 빛의 방향을 바꾸는 방법을 사용한다.
이럴 때 방향광을 사용하는 것이다.
점광
포인트 라이트라고도 불리는 이 빛은 일정한 점을 기준으로 원형으로 빛이 퍼지는 현상을 표현할 때 사용한다.
예를 들어 횃불, 가로등, 전구 등이 있다.
스포트 라이트
360도 퍼지는 점광에서 일정한 각도 제한을 둔다.
그리고 광원이 바라보는 방향으로 빛을 주는 시스템이다.
손전등 등을 구현할 때 사용할 수 있다.
표면의 명암
빛을 마주하고 있는 쪽은 반짝반짝 빛나고, 그 반대편은 그림자가 드리운다.
이런 원리는 무엇일까?
빛이 향하는 벡터가 있고, 표면의 법선벡터가 있다면 표면의 법선벡터와 빛이 향하는 벡터가 서로 마주볼수록, 즉 사이각이 180도에 가까울수록 빛이 난다.
반대로 표면의 법선벡터와 빛이 향하는 벡터가 같을 수록 즉 사이각이 0도에 가까울 수록 그림자가 드리우는 것이다.
2D 광원 처리 쉐이더
광원처리 쉐이더를 만들어보자.
// ======================================
// Std2DLightShader
// RasterizerState : None
// BlendState : Mask
// DepthStencilState : Less
//
// Parameter
// g_tex_0 : Output Texture
// g_tex_1 : Nomal Texture
// ======================================
struct VS_Light_IN
{
float3 vLocalPos : POSITION;
float2 vUV : TEXCOORD;
};
struct VS_Light_OUT
{
float4 vPosition : SV_Position;
float2 vUV : TEXCOORD;
float3 vWorldPos : POSITION;
};
VS_Light_OUT VS_Std2DLight(VS_Light_IN _in)
{
VS_Light_OUT output = (VS_Light_OUT) 0.f;
output.vPosition = mul(float4(_in.vLocalPos, 1.f), g_matWVP);
output.vUV = _in.vUV;
output.vWorldPos = mul(float4(_in.vLocalPos, 1.f), g_matWorld);
return output;
}
float4 PS_Std2DLight(VS_Light_OUT _in) : SV_Target
{
float4 vOutColor = (float4) 0.f;
if (g_btex_0)
{
vOutColor = g_tex_0.Sample(g_sam_0, _in.vUV);
}
else
{
vOutColor = float4(1.f, 0.f, 1.f, 1.f);
}
if (0.f == vOutColor.a)
discard;
// Lighting 처리
float3 vLightColor = (float3) 0.f;
for (int i = 0; i < iLightCount; ++i)
{
if (arrInfo[i].LightType == 0)
{
vLightColor += arrInfo[i].Color.vDiffuse;
}
else if (arrInfo[i].LightType == 1)
{
float3 vLightWorldPos = float3(arrInfo[i].vWorldPos.xy, 0.f);
float3 vWorldPos = float3(_in.vWorldPos.xy, 0.f);
float fDistance = abs(distance(vWorldPos, vLightWorldPos));
float fPow = saturate(1.f - (fDistance / arrInfo[i].Radius));
vLightColor += arrInfo[i].Color.vDiffuse * fPow;
}
}
vOutColor.rgb *= vLightColor;
return vOutColor;
}
- arrInfo[0].LightType == 0 : 방향광
- arrInfo[0].LightType == 1 : 점광
- arrInfo[0].LightType == 2 : 스포트라이트
원래대로라면 픽셀 별로 색상을 바로 출력했으나, 이제는 빛을 받아서 출력을 해야할 때이다.
원래대로 출력할 때 색상은 빛을 100%받고 있다는 가정하에 출력되는 색상이다.
그리고 받는 빛의 양이 줄어들면 점점 어두워지도록 구현해야할 것이다.
이 때 빛의 정보를 받아올 수 있도록 상수버퍼를 새로 구현해야 할 것이다.
// value.fx 중에서
cbuffer LIGHT : register(b2)
{
tLightInfo info[10];
int iLightCount;
int3 iLightPadding;
}
// struct.fx 파일
#ifndef _STRUCT
#define _STRUCT
struct tLightColor
{
float4 vDiffuse; // 빛의 색상
float4 vAmbient; // 주변 광(환경 광)
};
// LightInfo
struct tLightInfo
{
tLightColor Color; // 빛의 색상
float4 vWorldPos; // 광원의 월드 스페이스 위치
float4 vWorldDir; // 빛을 보내는 방향
uint LightType; // 빛의 타입(방향성, 점, 스포트)
float Radius; // 빛의 반경(사거리)
float Angle; // 빛의 각도
int Padding;
};
#endif
그리고 tLightInfo 구조체를 사용할 수 있도록 struct.fx라는 구조체를 담아놓는 셰이더 코드 파일을 새로 만들어준다.
여기서 광원이 몇개 들어올 수 있는지 파악을 하기 힘들다는 문제가 있다.
그래서 광원을 일단은 10개를 받을 수 있도록 10개를 선언한다.
그리고 광원이 몇개 들어왔는지 세는 iLightCount도 맞춘다.
iLightPadding은 상수레지스터의 크기를 16배수 단위로 맞추도록 하는 용도이다.
광원 정보를 상수버퍼로 받아오기
// define.h 중에서
enum class CB_TYPE
{
TRANSFORM,
MATERIAL,
LIGHT,
END
}
상수버퍼의 타입을 추가하고
// CDevice.cpp파일 중에서
void CDevice::CreateConstBuffer()
{
m_arrConstBuffer[(UINT)CB_TYPE::TRANSFORM] = new CConstBuffer((UINT)CB_TYPE::TRANSFORM);
m_arrConstBuffer[(UINT)CB_TYPE::TRANSFORM]->Create(sizeof(tTransform), 1);
m_arrConstBuffer[(UINT)CB_TYPE::MATERIAL] = new CConstBuffer((UINT)CB_TYPE::MATERIAL);
m_arrConstBuffer[(UINT)CB_TYPE::MATERIAL]->Create(sizeof(tMtrlConst), 1);
// 광원 상수 버퍼 할당
m_arrConstBuffer[(UINT)CB_TYPE::LIGHT] = new CConstBuffer((UINT)CB_TYPE::LIGHT);
m_arrConstBuffer[(UINT)CB_TYPE::LIGHT]->Create(sizeof(tLightInfo) * 10 + 16, 1);
}
여기서 광원을 받을 상수버퍼를 추가해주자.
여기서 광원을 10개 받을 수 있도록 선언했으므로 10개 만큼, 그리고 광원이 몇개 들어갔는지 저장할 정수 만큼 (16바이트)을 추가 할당해준다.
// CRenderMgr.cpp 중에서
void CRenderMgr::UpdateData()
{
struct {
tLightInfo arrInfo[10];
int iLightCount;
int padding[3];
}arrInfo{};
for (size_t i = 0; i < m_vecLight2D.size(); ++i)
{
arrInfo.arrInfo[i] = m_vecLight2D[i];
}
arrInfo.iLightCount = m_vecLight2D.size();
static CConstBuffer* pLightBuffer = CDevice::GetInst()->GetConstBuffer(CB_TYPE::LIGHT);
pLightBuffer->SetData(&arrInfo, sizeof(arrInfo));
pLightBuffer->UpdateData();
}
void CRenderMgr::Clear()
{
m_vecLight2D.clear();
}
그리고 SetData를 통해 상수버퍼로 데이터를 보내놓는다.
그러고 나서 상수버퍼와 바인딩을 진행한다.
점광(PointLight) 생성하기
// CLevelMgr.cpp 파일 중에서
// 광원 추가
CGameObject* pLightObj = new CGameObject;
pLightObj->SetName(L"Point Light");
pLightObj->AddComponent(new CTransform);
pLightObj->AddComponent(new CLight2D);
pLightObj->Transform()->SetRelativePos(Vec3(-200.f, 0.f, 0.f));
pLightObj->Light2D()->SetLightType(LIGHT_TYPE::POINT);
pLightObj->Light2D()->SetLightDiffuse(Vec3(1.f, 1.f, 1.f));
pLightObj->Light2D()->SetRadius(500.f);
m_pCurLevel->AddGameObject(pLightObj, 0, false);
pLightObj = pLightObj->Clone();
pLightObj->Transform()->SetRelativePos(Vec3(200.f, 0.f, 0.f));
m_pCurLevel->AddGameObject(pLightObj, 0, false);
점광 생성 코드를 세팅해준다.
std2d.fx 파일 중에서
// std2d.fx 파일 중에서
float4 PS_Std2DLight(VS_Light_OUT _in) : SV_Target
{
float4 vOutColor = (float4) 0.f;
if (g_btex_0)
{
vOutColor = g_tex_0.Sample(g_sam_0, _in.vUV);
}
else
{
vOutColor = float4(1.f, 0.f, 1.f, 1.f);
}
if (0.f == vOutColor.a)
discard;
// Lighting 처리
float3 vLightColor = (float3) 0.f;
for (int i = 0; i < iLightCount; ++i)
{
if (arrInfo[i].LightType == 0)
{
vLightColor += arrInfo[i].Color.vDiffuse;
}
else if (arrInfo[i].LightType == 1)
{
float3 vLightWorldPos = float3(arrInfo[i].vWorldPos.xy, 0.f);
float3 vWorldPos = float3(_in.vWorldPos.xy, 0.f);
float fDistance = abs(distance(vWorldPos, vLightWorldPos));
float fPow = saturate(1.f - (fDistance / arrInfo[i].Radius));
vLightColor += arrInfo[i].Color.vDiffuse * fPow;
}
}
vOutColor.rgb *= vLightColor;
return vOutColor;
}
LightType 값이 1이었을 경우 픽셀 별로 거리 값이 일정 범위 밖을 벗어나면 rgb값을 조정한다.
saturate함수는 결과 값이 0과 1을 벗어나지 않도록 조정하는 기능을한다.
func.fx 셰이더 코드 파일 생성하기.
Light 처리를 별도의 함수파일로 바꿔보자.
#ifndef _FUNC
#define _FUNC
#include "value.fx"
void CalcLight2D(float3 _vWorldPos, inout tLightColor _Light)
{
for (int i = 0; i < iLightCount; ++i)
{
if (arrInfo[i].LightType == 0)
{
_Light.vDiffuse.rgb += arrInfo[i].Color.vDiffuse.rgb;
_Light.vAmbient.rgb += arrInfo[i].Color.vAmbient.rgb;
}
else if (arrInfo[i].LightType == 1)
{
float3 vLightWorldPos = float3(arrInfo[i].vWorldPos.xy, 0.f);
float3 vWorldPos = float3(_vWorldPos.xy, 0.f);
float fDistance = abs(distance(vWorldPos, vLightWorldPos));
float fPow = saturate(1.f - (fDistance / arrInfo[i].Radius));
_Light.Diffuse.rgb += arrInfo[i].Color.vDiffuse.rgb * fPow;
}
else if (arrInfo[i].LightType == 2)
{
// 스포트라이트 추후 구현
}
}
}
#endif
// std2d.fx 파일 중에서
#include "func.fx"
float4 PS_Std2DLight(VS_Light_OUT _in) : SV_Target
{
float4 vOutColor = (float4) 0.f;
if (g_btex_0)
{
vOutColor = g_tex_0.Sample(g_sam_0, _in.vUV);
}
else
{
vOutColor = float4(1.f, 0.f, 1.f, 1.f);
}
if (0.f == vOutColor.a)
discard;
// Lighting 처리
tLightColor LightColor = (tLightColor) 0.f;
CalcLight2D(_in.vWolrdPos, LightColor);
vOutColor.rgb *= (LightColor.vDiffuse.rgb + LightCOlor.vAmbient.rgb);
return vOutColor;
}
스포트라이트 구현하기
시선이 어느 방향으로 향하고 있는지를 vWorldDir을 통해서 불러왔다.
그리고 더 필요한 정보는 제한 각도와 시선이 향하는 방향과 빛이 향하는 방향 사이의 각도이다.
광원과 물체 사이의 방향을 vLight로 저장하였고, acos(아크코사인)함수를 사용해 빛이 비추는 방향(vLightDir)과 광원에서 물체 사이의 방향(vLight)의 내적 값을 절대값 1이하로 낮춘 뒤에 (saturate) 아크코사인 함수를 이용해서 구하였다.
else if (arrInfo[i].LightType == 2)
{
float3 vLightDir = float3(arrInfo[i].vWorldDir.xy, 0.f);
float3 vWorldPos = float3(_vWorldPos.xy, 0.f);
float3 vLightWorldPos = float3(arrInfo[i].vWorldPos.xy, 0.f);
//광원 중심에서 물체를 향하는 방향
float3 vLight = normalize(vWorldPos - vLightWorldPos);
float vLightAngle = acos(saturate(dot(vLight, vLightDir)));
if (vLightAngle <= arrInfo[i].Angle / 2.f)
{
vLightWorldPos = float3(arrInfo[i].vWorldPos.xy, 0.f);
vWorldPos = float3(_vWorldPos.xy, 0.f);
float fDistance = abs(distance(vWorldPos, vLightWorldPos));
float fPow = saturate(1.f - (fDistance / arrInfo[i].Radius));
_Light.vDiffuse.rgb += arrInfo[i].Color.vDiffuse.rgb * fPow;
}
}
이 때, 원하는 부분만을 비추기 위해서는 두 벡터를 내적해서 나온 값의 아크코사인 값을 구한 vLightAngle의 값은 아래 그림과 같이 빛을 비추는 각도(arrInfo[i].Angle / 2.f) 사이에 있어야 한다. 그래야 자연스럽게 손전등이 비추듯이 빛이 퍼지게 된다.
그리고 실행 결과는 아래와 같다.
else if (arrInfo[i].LightType == 2)
{
float3 vLightWorldPos = float3(arrInfo[i].vWorldPos.xy, 0.f);
float3 vWorldPos = float3(_vWorldPos.xy, 0.f);
// 광원 중심에서 물체를 향하는 방향
float3 vLight = normalize(vWorldPos - vLightWorldPos);
float vSeeingAngle = arrInfo[i].Angle;
float vLightAngle = acos(dot(vLight, _vWorldDir));
if (vLightAngle < vSeeingAngle / 2.f)
{
float fDiffusePow = saturate(dot(-vLight, _vWorldDir));
float fDistance = abs(distance(vWorldPos, vLightWorldPos));
float fDistPow = saturate(1.f - (fDistance / arrInfo[i].Radius));
_Light.vDiffuse.rgb += arrInfo[i].Color.vDiffuse.rgb * fDiffusePow * fDistPow;
}
}
그리고 코드를 위와 같이 바꿔서 빛이 번지는 효과를 줄 수도 있다.