[목차]

 

- 라이트 추가

- 세팅

- 업데이트

- 결과

 

 

[라이트 추가]

 

라이트는 그래픽스에서 가장 기본적이지만 중요하고, 그래픽스를 배울 때 처음으로 접하는 부분이 아닐까 생각됩니다. 실제로 구현해보기 전 개념을 먼저 익혔고, 프레임워크에 추가했습니다.

 

https://umtimos.tistory.com/199

 

[Graphics] Lighting

[Lighting의 필요성] 그래픽스에서 라이팅(Lighting)은 화면의 분위기, 깊이감, 사실감을 결정짓는 중요한 요소입니다. 빛 처리가 없으면 아무리 3D 물체라고 해도 평면처럼 보이게 됩니다. 위의 예시

umtimos.tistory.com

 

 

[세팅]

 

본격적으로 픽셀 셰이더를 활용해야하는 단계입니다. 픽셀 셰이더를 위한 상수버퍼와 Light라는 개념을 정의하기 위한 Ext_Light 클래스를 추가했습니다.

////////////////////////////// Ext_Actor.h
#pragma once
#include "Ext_Actor.h"

enum class LightType
{
	Directional,
	Point,
	Spot,
	Unknown
};

struct LightData
{
	float4 LightColor = { 1.0f, 1.0f, 1.0f, 0.25f };// RGB(색), w(강도)
	float4 LightWorldPosition; // 라이트 월드 공간 위치
	float4 LightForwardVector; // 월드 입사 벡터
	float4 CameraWorldPosition; // 시점 월드 공간 위치

	float NearDistance = 1.0f;
	float FarDistance = 100.0f;
	float AttenuationValue = 1.0f; // 거리감쇠값에 사용
	int LightType = 0;
    bool bIsLightSet = false;
};

constexpr unsigned int MAX_LIGHTS = 64;
struct LightDatas
{
	int LightCount = 0;
	LightData Lights[MAX_LIGHTS];
};

class Ext_Light : public Ext_Actor
{
public:
	// constrcuter destructer
	Ext_Light();
	~Ext_Light() {};

	// delete Function
	Ext_Light(const Ext_Light& _Other) = delete;
	Ext_Light(Ext_Light&& _Other) noexcept = delete;
	Ext_Light& operator=(const Ext_Light& _Other) = delete;
	Ext_Light& operator=(Ext_Light&& _Other) noexcept = delete;

	void LightUpdate(std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime);

	void SetLightType(LightType _Type) { LTData->LightType = static_cast<int>(_Type); };
	void SetLightColor(float4 _Color) { LTData->LightColor = _Color; };
	void SetAttenuationValue(float _Value) { LTData->AttenuationValue = _Value; };
	void SetLightRange(float _Range)
	{
		LTData->NearDistance = 1.0f;
		LTData->FarDistance = _Range;
	}

	std::shared_ptr<LightData> GetLightData() { return LTData; }

protected:
	
private:
	std::shared_ptr<LightData> LTData = nullptr;
	LightType Type = LightType::Unknown;

};

////////////////////////////// Ext_Actor.cpp
#include "PrecompileHeader.h"
#include "Ext_Light.h"
#include "Ext_Camera.h"
#include "Ext_Transform.h"

Ext_Light::Ext_Light()
{
	LTData = std::make_shared<LightData>();
    LTData->bIsLightSet = true;
}

// 여기서의 Camera는 바라보는 시선, 위치를 의미함
void Ext_Light::LightUpdate(std::shared_ptr<Ext_Camera> _Camera, float _DeltaTime)
{
	LTData->LightWorldPosition = GetTransform()->GetWorldPosition();
	LTData->LightForwardVector = GetTransform()->GetLocalForwardVector();
	LTData->CameraWorldPosition = _Camera->GetTransform()->GetWorldPosition();
}

 

Light는 Actor를 상속받기 때문에 생성 시 Scene의 Actors 컨테이너에 저장됩니다. 하지만 Light들에 대해서 따로 순회를 돌며 Update를 진행해야 하는 부분이 있기 때문에, 이들에 대해서만 저장할 컨테이너를 하나 추가했습니다.

 

또한 MeshComponentUnit들에 대해서도 생성 시 Scene의 LightDatas 상수 버퍼를 세팅하도록 설정했습니다.

// 메시 컴포넌트 유닛 생성 시 호출, Mesh, Material, ConstantBuffer 세팅
void Ext_MeshComponentUnit::MeshComponentUnitInitialize(std::string_view _MeshName, MaterialType _SettingValue)
{
	std::string MaterialName = MaterialSettingToString(_SettingValue);

	Mesh = Ext_DirectXMesh::Find(_MeshName); // 메시 설정
	Material = Ext_DirectXMaterial::Find(MaterialName); // 머티리얼 설정

	if (nullptr == Mesh || nullptr == Material)
	{
		MsgAssert("존재하지 않는 메시나 머티리얼을 메시유닛에 넣을 수는 없습니다.")
	}

	InputLayout->CreateInputLayout(Mesh->GetVertexBuffer(), Material->GetVertexShader()); // InputLayout 설정

	// 상수버퍼 세팅
	// [1] 버텍스 셰이더 정보 가져오기
	const Ext_DirectXBufferSetter& VertexShaderBuffers = Material->GetVertexShader()->GetBufferSetter();
	BufferSetter.Copy(VertexShaderBuffers);

	// [2] 픽셀 셰이더 정보 가져오기
	const Ext_DirectXBufferSetter& PixelShaderBuffers = Material->GetPixelShader()->GetBufferSetter();
	BufferSetter.Copy(PixelShaderBuffers);

	// [3] 트랜스폼 상수버퍼 세팅하기
	const TransformData& TFData = *(OwnerMeshComponent.lock()->GetTransform()->GetTransformData().get());
	BufferSetter.SetConstantBufferLink("TransformData", TFData);

	// [4] 빛 상수버퍼 세팅하기(스태틱, 다이나믹은 빛 연산 실시를 위해 추가 세팅)
	if (_SettingValue == MaterialType::Static || _SettingValue == MaterialType::Dynamic)
	{
		const LightDatas& LTDatas = OwnerMeshComponent.lock()->GetOwnerScene().lock()->GetLightDataBuffer();
		BufferSetter.SetConstantBufferLink("LightDatas", LTDatas);
	}
    //...
}

 

 

[업데이트]

 

Redering 단계에서 카메라의 뷰, 프로젝션 행렬을 구한 뒤 바로 Light 들의 업데이트를 진행합니다.

// Camera들의 Rendering 호출
void Ext_Scene::Rendering(float _DeltaTime)
{
	// Rendering 업데이트
	for (auto& CamIter : Cameras)
	{
		std::shared_ptr<Ext_Camera> CurCamera = CamIter.second;

		CurCamera->CameraTransformUpdate(); // 카메라에 대한 뷰, 프로젝션, 뷰포트 행렬 최신화
		LightDataBuffer.LightCount = 0; // 라이트 업데이트 전, 상수버퍼 갯수 초기화(순회하면서 값 넣어줘야하기 때문)

		for (auto& LightIter : Lights) // 라이트들 돌면서 업데이트
		{
			std::shared_ptr<Ext_Light> CurLight = LightIter.second;
			CurLight->LightUpdate(CurCamera, _DeltaTime);

			LightDataBuffer.Lights[LightDataBuffer.LightCount] = *CurLight->GetLightData().get(); // 상수버퍼에 값 넣어주기
			++LightDataBuffer.LightCount; // 다음 순회를 위해 1 올려주는 작업임
		}

		CurCamera->Rendering(_DeltaTime); // Camera의 MeshComponent들 Rendering
	}

	Ext_Imgui::Render(GetSharedFromThis<Ext_Scene>(), _DeltaTime);
}

 

다음으로는 Vertex Shader입니다.

 

cbuffer TransformData : register(b0)
{
    float4 LocalPosition;
    float4 LocalRotation;
    float4 LocalScale;
    float4 LocalQuaternion;
    float4x4 LocalMatrix;

    float4 WorldPosition;
    float4 WorldRotation;
    float4 WorldQuaternion;
    float4 WorldScale;
    float4x4 WorldMatrix;

    float4x4 ViewMatrix;
    float4x4 ProjectionMatrix;
    float4x4 WorldViewMatrix;
    float4x4 WorldViewProjectionMatrix;
}

struct VSInput
{
    float4 Position : POSITION;
    float4 TexCoord : TEXCOORD;
    float4 Normal : NORMAL;
};

struct VSOutput
{
    float4 Position : SV_POSITION;
    float4 TexCoord : TEXCOORD;
    float3 WorldPosition : POSITION;
    float3 WorldNormal : NORMAL;
};

VSOutput Grapics_VS(VSInput _Input)
{
    VSOutput Output;
    // _Input.Position.w = 1.0f;
    
    // Position 설정
    float4 WorldPos = mul(_Input.Position, WorldMatrix);
    float4 ViewPos = mul(WorldPos, ViewMatrix);
    Output.Position = mul(ViewPos, ProjectionMatrix);
    
    // UV 좌표 설정
    Output.TexCoord = _Input.TexCoord;
    
    // 월드 공간 기준으로 조명 계산을 진행하기 위해 WorldMatrix만 처리한 Position, Normal을 생성하여 Pixel Shader에 넘겨줌
    Output.WorldPosition = mul(float4(_Input.Position.xyz, 1.0f), WorldMatrix).rgb;
    Output.WorldNormal = mul(float4(_Input.Normal.xyz, 0.0f), WorldMatrix).rgb;
   
    return Output;
}

 

VSOutput 구조체에 WorldPosition과 WorldNormal을 추가했습니다. 픽셀 셰이더에서 빛 처리를 World Space 기준으로 하기 위해서입니다.

 

다음은 Pixel Shader입니다.

struct LightData
{
    float4 LightColor; // RGB(색), w(강도)
    float4 LightWorldPosition; // 라이트 월드 공간 위치
    float4 LightForwardVector; // 월드 입사 벡터
    float4 CameraWorldPosition; // 시점 월드 공간 위치

    float NearDistance;
    float FarDistance;
    float AttenuationValue;
    int LightType;
    bool bIsLightSet;
};

cbuffer LightDatas : register(b1)
{
    int LightCount;
    LightData Lights[64];
};

Texture2D BaseColorTex : register(t0); // 텍스처 자원
SamplerState Sampler : register(s0); // 샘플러

struct PSInput
{
    float4 Position : SV_POSITION;
    float4 TexCoord : TEXCOORD;
    float3 WorldPosition : POSITION;
    float3 WorldNormal : NORMAL;
};

// 각 벡터에 normalize를 해주는 이유는, 명시적으로 normalize된 벡터를 넣어줬다 하더라도 
// 임의의 값이 어떻게 들어올지 모르기 때문에 그냥 해주는것(안정성을 위한 처리라고 보면 됨)
float4 Grapics_PS(PSInput _Input) : SV_TARGET
{
    // 텍스처 색상
    float4 Albedo = BaseColorTex.Sample(Sampler, _Input.TexCoord);
    
    // 월드공간 기준 픽셀(표면) 위치와 법선 단위 벡터
    float3 PixelPosition = _Input.WorldPosition;
    float3 PixelNormal = normalize(_Input.WorldNormal);
    
    // 누적 조명 컬러 (RGB)
    float3 AccumLightColor = float3(0, 0, 0);
    
    for (int i = 0; i < LightCount; ++i)
    {
        LightData LTData = Lights[i];
        
        if (LTData.bIsLightSet == false)
        {
            continue;
        }
        
        // 공통분모
        float3 FinalLight = float3(0.0f, 0.0f, 0.0f);
        float3 LightDirection = normalize(-LTData.LightForwardVector.xyz); // 빛 계산에 사용하는 LightDirection은 "표면에서 봤을 때 빛이 표면으로 들어오는 방향"을 사용하므로 반대로 뒤집음
        
        float Shininess = 32.0f; // Shininess는 임의로 32 고정
        float AmbientIntensity = LTData.LightColor.w;
        
        // 월드공간 기준 시선 위치와 시선의 방향 단위 벡터
        float3 EyePosition = LTData.CameraWorldPosition.xyz;
        float3 EyeDirection = normalize(EyePosition - _Input.WorldPosition);
        
        if (LTData.LightType == 0) // Directional Light
        {
            // Diffuse Light 계산
            float DiffuseLight = saturate(dot(LightDirection, PixelNormal));
        
            // Specular Light 계산, Phong 모델을 사용하기 때문에 R = reflect(L, N)
            float3 ReflectionVector = normalize(2.0f * PixelNormal * dot(LightDirection, PixelNormal) - LightDirection); // 반사벡터
            float RDotV = max(0.0f, dot(ReflectionVector, EyeDirection));
            float3 SpecularLight = pow(RDotV, Shininess);
        
            // Ambient Light 계산, 강도는 LightColor의 w값 사용
            float3 AmbientLight = AmbientIntensity;
        
            AccumLightColor += LTData.LightColor.xyz * (DiffuseLight + SpecularLight + AmbientLight);
        }
        else if (LTData.LightType == 1) // 포인트 라이트
        {
            float3 LightPosition = LTData.LightWorldPosition.xyz;
        
            // 픽셀과 위치와 광원 위치 사이 거리 및 방향 구하기
            float3 PixelToLight = LightPosition - PixelPosition;
            float Distance = length(PixelToLight); // Distance로 Near, Far 컬링하면 재밌는거 볼 수 있음, 밑에 주석을 사용
            //if (Distance >= LTData.NearDistance && Distance <= LTData.FarDistance)
            
            // 픽셀에서 광원으로 향하는 단위 벡터
            float3 Vector = normalize(PixelToLight);
            
            // 감쇠(Attenuation) 공식 = 1/(c0 + c1·d + c2·d²)
            float C0 = 1.0f;
            float C1 = 0.0f;
            float C2 = LTData.AttenuationValue / (LTData.FarDistance * LTData.FarDistance); // 거리에 따른 제곱항 계수, 여기를 조정하면 감쇠가 쌔짐
            float Attenuation = 1.0f / (C0 + C1 * Distance + C2 * Distance * Distance);

            // Diffuse Light 계산
            float DiffuseLight = saturate(dot(PixelNormal, Vector));

            // Specular Light 계산, Phong 모델을 사용하기 때문에 R = reflect(L, N)
            float3 ReflectionVector = normalize(2.0f * PixelNormal * dot(Vector, PixelNormal) - Vector);
            float RDotV = max(0.0f, dot(ReflectionVector, EyeDirection));
            float3 SpecularLight = pow(RDotV, Shininess);

            // Ambient Light 계산, 강도는 LightColor의 w값 사용
            float3 AmbientLight = AmbientIntensity;

            // 누적
            AccumLightColor += LTData.LightColor.xyz * (DiffuseLight + SpecularLight + AmbientLight) * Attenuation;
        }
    }
    
    Albedo.rgb *= AccumLightColor;
    return Albedo;
}

 

 

Pixel Shader에서 실시하는 것은 다음과 같습니다.

 

1. 기존 색상 사용

: Texture가 있다면 그 값이 기본 Color

 

2. 월드 공간 위치, 월드 공간 노말 값 확인

: 노말은 normalize() 실시(혹시 몰라서 그냥 해줌)

 

3. LightData 반복문

: 갯수는 최대 64개로 해뒀기 때문에, 64번 순회를 돌 것입니다(실제로는 2개밖에 없지만). 

 

4. Directional Light 연산 실시

: Diffuse, Specular, Ambient 연산 실시

 

5. Color에 적용

: Diffuse, Specular, Ambient 세 개를 Light Color에 모두 더하고, 기본 Color와 곱해주면서 누적

 

+) 포인트 라이트는 감쇠를 적용해줍니다.

 

 

[결과]

 

디렉셔널 라이트는 기본으로 사용하고, 메인 카메라에 PointLight를 붙여서 Scene 내에 총 2개의 라이트를 사용해봤습니다. 포인트 라이트는 감쇠값을 크게 설정해서, 가까이 가야 잘보입니다.

 

+ Recent posts