DirectX 11/DX11 Tutorial

[06] 난반사 조명(Diffuse light)

김띠띵 2024. 5. 24. 22:51

 

 

Tutorial 6: Diffuse Lighting

Tutorial 6: Diffuse Lighting In this tutorial I will cover how to light 3D objects using diffuse lighting and DirectX 11. We will start with the code from the previous tutorial and modify it. The type of diffuse lighting we will be implementing is called d

www.rastertek.com

DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.


들어가며

 

이번 튜토리얼에서는 Dx11을 사용하여 물체에 난반사 조명을 적용시키는 방법을 다룰 것입니다.

난반사 광이란 빛을 받아 여러 방향으로 고르게 반사되는 빛을 뜻합니다.

 

우리가 구현할 난반사 조명의 유형을 방향성 조명(Directional lighting)이라고 합니다.

방향성 조명은 태양이 지구를 비추는 방식과 유사합니다. 이 조명은 매우 멀리 떨어진 광원으로부터 오는 빛의 방향을 기반으로 물체에 비치는 빛의 양(밝기)을 결정할 수 있습니다. 그러나 직접 닿지 않는 표면은 조명되지 않습니다.(첫 번째로 빛이 닿는 부분만 적용되며 빛의 반사까지는 계산하지 않는다는 뜻인듯?)

 

난반사 조명의 구현은 VS와 PS에서 이루어집니다. 난반사 조명에는 방향과 우리가 조명하고자 하는 폴리곤의 법선 벡터가 필요합니다. 폴리곤을 구성하는 세 개의 정점을 이용하여 폴리곤의 법선을 계산할 수 있습니다. 이 튜토리얼에서는 조명 방적식에 난반사 조명의 색상을 구현할 것입니다.

 

프레임워크

 

(NEW) LightClass
장면에서 광원을 나타냅니다, 빛의 방향과 색상을 보유하는 것 외에는 다른 역할을 하지 않습니다.

 

(NEW) LightShaderClass

기존 TextureShaderClass를 제거하고 모델의 조명 셰이딩을 처리하는 셰이더 클래스입니다.


Light.vs

 

이전 셰이더와 특별하게 다른점은 법선 벡터가 float3형태로 정점데이터에 생기고, 월드 공간에서의  법선 벡터를구합니다.

PixelInputType LightVertexShader(VertexInputType input)
{
  	// ....
    
    // 법선벡터를 월드공간으로 공간변환 합니다.
    output.normal = mul(input.normal, (float3x3) worldMatrix);
	
    // 계산된 법선벡터를 정규화 합니다.
    output.normal = normalize(output.normal);

    return output;
}

 

Light.ps

 

정점 데이터에 법선벡터의 float3이 추가되었고 상수버퍼도 추가 되었습니다.

또한 난반사광의 양을 이곳에서 계산합니다.

Texture2D shaderTexture : register(t0);
SamplerState SampleType : register(s0);

cbuffer LightBuffer
{
    float4 diffuseColor;
    float3 lightDirection;
    float padding;
};

struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
    float3 normal : NORMAL;
};


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
float4 LightPixelShader(PixelInputType input) : SV_TARGET
{
    float4 textureColor;
    float3 lightDir;
    float lightIntensity;
    float4 color;

    textureColor = shaderTexture.Sample(SampleType, input.tex);

    // 빛의 방향을 반전시킵니다.
    lightDir = -lightDirection;

    // 이 픽셀의 빛의 양을 계산합니다.
    lightIntensity = saturate(dot(input.normal, lightDir));

    // 빛의 양과 픽셀의 최종 색상을 곱하여 최최종 색상을 만들어 냅니다.
    color = saturate(diffuseColor * lightIntensity);

    // 최최종 색상과 텍스쳐의 색상을 곱하여 텍스쳐를 적용시킵니다.
    color = color * textureColor;

    return color;
}

 

LightShaderClass.h

 

Light 셰이더를 만들고 적용하기 위한 클래스

특별하게 다른건 PS의 상수버퍼에 들어가기 위한 구조체가 추가 되었다. 이 상수버퍼 구조체는 조명 정보를 저장한다.

	struct LightBufferType
	{
	    XMFLOAT4 diffuseColor;
	    XMFLOAT3 lightDirection;
	    float padding;//구조체의 메모리 크기가 16(바이트) 배수가 되도록 쓰레기 값을 넣었습니다.
	};
    
private:
	// PS의 상수버퍼
	ID3D11Buffer* m_lightBuffer;

 

 

LightShaderClass.cpp

 

1. InitializeShader

법선 벡터의 시멘틱은 대체적으로 NORMAL 이다.

bool LightShaderClass::InitializeShader(ID3D11Device* device, HWND hwnd, WCHAR* vsFilename, WCHAR* psFilename)
{

	// Vertex input layout description 생성
	// polygonLayout[0] (POSITION) 생성 ....
	// polygonLayout[1] (TEXCOORD) 생성 ....
	
	polygonLayout[2].SemanticName = "NORMAL";
	polygonLayout[2].SemanticIndex = 0;
	polygonLayout[2].Format = DXGI_FORMAT_R32G32B32_FLOAT;
	polygonLayout[2].InputSlot = 0;
	polygonLayout[2].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
	polygonLayout[2].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
	polygonLayout[2].InstanceDataStepRate = 0;
    
    // ....

PS에 들어갈 상수버퍼를 생성해준다.

    // VS에 들어갈 상수버퍼 생성 ....
    
    // 픽셀 셰이더에 들어갈 상수버퍼의 Buffer description을 설정합니다.    
    lightBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
    // ByteWidth가 16바이트의 배수가 아니면 실패합니다.
    lightBufferDesc.ByteWidth = sizeof(LightBufferType);
    lightBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    lightBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    lightBufferDesc.MiscFlags = 0;
    lightBufferDesc.StructureByteStride = 0;

    // Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class.
    result = device->CreateBuffer(&lightBufferDesc, NULL, &m_lightBuffer);

    return true;
}

 

2. SetShaderParameters.cpp

PS들어갈 상수버퍼의 값을 Map/Unmap으로 수정하고 PS단계에 설정한다.

bool LightShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, XMMATRIX worldMatrix, XMMATRIX viewMatrix, XMMATRIX projectionMatrix,
    ID3D11ShaderResourceView* texture, XMFLOAT3 lightDirection, XMFLOAT4 diffuseColor)
{
    // VS에 들어갈 상수버퍼 업데이트 후 VS에 배치 ....
    deviceContext->PSSetShaderResources(0, 1, &texture);

    // PS에 들어갈 상수버퍼 업데이트
    result = deviceContext->Map(m_lightBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);

    // Get a pointer to the data in the constant buffer.
    dataPtr2 = (LightBufferType*)mappedResource.pData;

    // Copy the lighting variables into the constant buffer.
    dataPtr2->diffuseColor = diffuseColor;
    dataPtr2->lightDirection = lightDirection;
    dataPtr2->padding = 0.0f;

    // Unlock the constant buffer.
    deviceContext->Unmap(m_lightBuffer, 0);

    // Set the position of the light constant buffer in the pixel shader.
    bufferNumber = 0;

    // 상수버퍼 PS단계에 배치
    deviceContext->PSSetConstantBuffers(bufferNumber, 1, &m_lightBuffer);

    return true;
}

 

ModelClass.h

 

모델에서 사용하는 정점데이터를 수정합니다.

class ModelClass
{
private:
	struct VertexType
	{
		XMFLOAT3 position;
		XMFLOAT2 texture;
		XMFLOAT3 normal; //추가
	};

 

 

ModelClass.cpp

 

1. InitializeBuffers

정점 데이터에 normal의 값을 작성하여 저장합니다.

ID3D11ShaderResourceView* ModelClass::GetTexture()
{
	return m_Texture->GetTexture();
}

bool ModelClass::InitializeBuffers(ID3D11Device* device)
{
	// .... 
	
	vertices[0].position = XMFLOAT3(-1.0f, 1.0f, 0.0f);  // Top left.
	vertices[0].texture = XMFLOAT2(0.0f, 0.0f);
	vertices[0].normal = XMFLOAT3(0.0f, 0.0f,-1.0f); // 법선 벡터 추가

	vertices[1].position = XMFLOAT3(1.0f, 1.0f, 0.0f);  // Top right
	vertices[1].texture = XMFLOAT2(1.0f, 0.0f);
	vertices[1].normal = XMFLOAT3(0.0f, 0.0f, -1.0f);

	vertices[2].position = XMFLOAT3(-1.0f, -1.0f, 0.0f);  // Bottom left.
	vertices[2].texture = XMFLOAT2(0.0f, 1.0f);
	vertices[2].normal = XMFLOAT3(0.0f, 0.0f, -1.0f);


	vertices[3].position = XMFLOAT3(1.0f, -1.0f, 0.0f);  // Bottom right.
	vertices[3].texture = XMFLOAT2(1.0f, 1.0f);
	vertices[3].normal = XMFLOAT3(0.0f, 0.0f, -1.0f);

 

 

LightClass.h

 

이 클래스의 목적은 빛의 방향과 색상을 유지하는 것뿐입니다.

#pragma once
#include <directxmath.h>
using namespace DirectX;

class LightClass
{
public:
    LightClass();
    LightClass(const LightClass&) = default;
    ~LightClass() = default;

    void SetDiffuseColor(float, float, float, float);
    void SetDirection(float, float, float);

    XMFLOAT4 GetDiffuseColor();
    XMFLOAT3 GetDirection();

private:
    XMFLOAT4 m_diffuseColor;
    XMFLOAT3 m_direction;
};

 

LightClass.cpp

 

#include "LightClass.h"

void LightClass::SetDiffuseColor(float red, float green, float blue, float alpha)
{
    m_diffuseColor = XMFLOAT4(red, green, blue, alpha);
    return;
}


void LightClass::SetDirection(float x, float y, float z)
{
    m_direction = XMFLOAT3(x, y, z);
    return;
}


XMFLOAT4 LightClass::GetDiffuseColor()
{
    return m_diffuseColor;
}


XMFLOAT3 LightClass::GetDirection()
{
    return m_direction;
}

 

ApplicationClass.h

 

생성한 Light관련 클래스를 include하고 개체의 포인터를 생성합니다.

Render함수도 이제는 매개변수로 float을 받게됩니다.

#pragma once

#include <windows.h>
#include "D3DClass.h"
#include "CameraClass.h"
#include "ModelClass.h"
#include "LightClass.h"
#include "LightShaderClass.h"

const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.3f;

class ApplicationClass
{
public:
	ApplicationClass();
	ApplicationClass(const ApplicationClass&) = default;
	~ApplicationClass() = default;

	bool Initialize(int, int, HWND);
	void Shutdown();
	bool Frame();

private:
	bool Render(float);

private:

	D3DClass* m_Direct3D;
	CameraClass* m_Camera;
	ModelClass* m_Model;
	LightShaderClass* m_LightShader;
	LightClass* m_Light;
};

 

Application.cpp

 

1. Initialize

추가된 포인터의 개체를 생성하고 초기화 합니다.

bool ApplicationClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{

	// ....
    
	// Light shader 개체를 생성하고 초기화 합니다.
	m_LightShader = new LightShaderClass;

	result = m_LightShader->Initialize(m_Direct3D->GetDevice(), hwnd);
	if (!result)
	{
		MessageBox(hwnd, L"Could not initialize the light shader object.", L"Error", MB_OK);
		return false;
	}

	// Light 개체를 생성하고 초기화 합니다.
	m_Light = new LightClass;

	m_Light->SetDiffuseColor(1.0f, 1.0f, 1.0f, 1.0f);
	m_Light->SetDirection(0.0f, 0.0f, 1.0f);

	return true;
}

 

2. Frame

각 프레임마다 계속 회전을 업데이트한 정적 변수를 추가하여 Render함수에 전달합니다.

bool ApplicationClass::Frame()
{
	static float rotation = 0.0f;
	bool result;

	// 회전 값을 프레임마다 업데이트 합니다.
	rotation -= 0.0174532925f * 0.1f;
	if (rotation < 0.0f)
	{
		rotation += 360.0f;
	}

	// 그래픽 장면을 렌더링합니다. 
	result = Render(rotation);

	return true;
}

 

3. Render

bool ApplicationClass::Render(float rotation)
{
	// ....

	// camera 와 d3d objects에서 world, view, projection 행렬을 가져옵니다.
	m_Direct3D->GetWorldMatrix(worldMatrix);
	m_Camera->GetViewMatrix(viewMatrix);
	m_Direct3D->GetProjectionMatrix(projectionMatrix);

	//(추가코드) 삼각형에 rotation값 만큼 회전을 시킵니다. 
	worldMatrix = XMMatrixRotationY(rotation);

	// 렌더링 파이프라인에 모델의 정점, 인덱스 버퍼를 배치하여 그릴 준비를 합니다.
	m_Model->Render(m_Direct3D->GetDeviceContext());

	//(수정코드) 라이트 셰이더를 사용하여 모델을 렌더링합니다.
	result = m_LightShader->Render(m_Direct3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, m_Model->GetTexture(),m_Light->GetDirection(), m_Light->GetDiffuseColor());

	// 렌더링된 장면을 화면에 표시한다.
	m_Direct3D->EndScene();

	return true;
}

 


요약 

 

다음은 코드에 몇 가지 변경을 가하여 기본적인 난방향 조명을 구현한 것입니다. 법선 벡터가 어떻게 작동하는지, 그리고 왜 폴리곤 면에 조명을 계산하는 데 중요한지 이해하는 것이 중요합니다. 우리의 D3DClass에서 후면 컬링(back face culling)이 활성화되어 있기 때문에 회전하는 삼각형의 뒷면은 조명이 비추지 않을 것입니다.

사각형이 뒷면이 보이면 다시 앞면이 바로 나오게 회전 값을 조정했다.

 

연습

 

1. 조명 색상을 녹색으로 변경합니다.

DiffuseColor를 녹색으로 변경해준다.

 

내 생각에 중요했던 것

 

1. 기존 물체의 공간변환시에는 float4를 사용한 공간변환이지만, 광원의 위치가 월드로 공간변환시 사용한건 float3이다.

이유는 투영 변환에서 원근감을 표현하기위해 4차원 벡터가 필요했던것.

광원은 카메라의 위치와도 무관하고 월드 공간까지만 있어도 충분하기 때문에 flaot3로 공간변환이 되었다.

 

2. 상수 버퍼 

상수 버퍼를 구성하는 구조체는 16바이트의 배수로 정의되어야 한다.

이유는 GPU에서 SIMD를 활용하기 위해서다.

 

SIMD는 "Single Instruction, Multiple Data"의 약자로 단일 명령으로 여러 데이터를 동시에 처리 한다는 뜻이다.

즉, 장치가 한 번의 명령으로 여러 데이터를 병렬로 처리할 수 있게 해주는 기술이다.

이 SIMD기술이 주로 128bit(16byte)의 단위로 데이터를 처리하기 때문이다!

 

예시를 들어보자면 숫자 배열의 덧셈이있다.

A = [1, 2, 3, 4]
B = [5, 6, 7, 8]

두 배열의 각 인덱스를 더해 새로운 배열 C를 만든다고 할때,

일반적인 방식으로는 밑의 과정을 거친다.

1. C[0] = 1 + 5 = 6

2. C[1] = 2 + 6 = 8

3. C[2] = 3 + 7 = 10

4. C[3] = 4 + 8 = 12

 

하지만 SIMD를 사용하는 경우

1. [1+5, 2+6, 3+7, 4+8] = [6, 8, 10, 12]

이렇게 한번에 C를 구할 수 있다.

 

 


용어 정리

 

Diffuse light : 난반사, 광원에 의해 여러 방향으로 고르게 반사되는 빛.

물체를 여러 방향에서 바라보아도 물체의 색이 크게 변하지 않는 이유다. (빛이 고르게 반사되는 난반사가 있기때문에)

 

Diffuse lighting : 물체에 광원에 의한 난반사를 적용시키는 것

 

법선 벡터 : 평면 벡터의 수직인 벡터

 

SIMD : 하나의 명령으로 여러 데이터 처리를 하는 기술