[DirectX11] 13. 라이트( Directional Light ) – Constant Buffer 활용하기.



라이트( Directional Lighting )에 대해 구현을 해보고자 한다. 그리고 이 라이트을 구현할때 Constant Buffer를 활용하려 한다. 구현하려는 라이트는 난반사( Diffused Reflection )에 기초한다.



Diffused Reflection


렌더링되는 물체에 빛이 일정한 방향으로 비추고 있을때, 표면이 일정하지 않은 물체는 반사되는 빛의 양이 각 표면마다 다르다. 이 일정하지 않은 표면을 법선( Normal )벡터로 표현한다.


법선 벡터와 빛의 방향을 가지고 표면( 픽셀 )에서의 반사량을 계산하고 빛의 색과 텍스처에서 샘플링한 픽셀 컬러를 적당히 블렌딩하여 라이팅을 구현하게 되는 것이다.



Constant Buffer 추가하기


일단 빛의 방향과 색을 지정해주기 위해 Constant Buffer를 추가한다. 우선 버퍼에 사용될 데이터형을 정의해야 한다.

C++
struct LightProperty
{
    Vector Direction;
    Vector Color;
    float  padding[ 2 ];
}

이렇게 정의하거나

C++
Vector4 LightDirection;
Vector4 LightColor;

이렇게 정의할 수 있다.


우선 Constant Buffer의 중요한 특징중 하나는 내부 데이터를 수정할 때 일부만 갱신할 수 없다는 것이다. struct LightProperty형태로 버퍼를 만들어두고 Direction혹은 Color만 데이터값을 갱신할 수 없다는 것이다. Constant Buffer의 데이터 갱신은 항상 전체로 이루어진다. 따라서 버퍼를 구성하는 데이터의 갱신 주기가 차이가 많이 난다면 그냥 서로 다른 버퍼로 정의하고 생성하는편이 낫다. 물론 동시에 사용가능한 Constant Buffer의 갯수는 제한이 되어 있기 때문에 무작정 세분화해서 하나하나 만들 수는 없다. 이것은 잘 판단해서 그룹화할건 그룹화 하고 서로 분리할건 분리할 수 있어야 한다. 일단은 후자로 정의하기로 한다.

또 하나 중요한 특징은 Constant Buffer의 크기( Byte width )는 반드시 16바이트 배수여야 한다. GPU의 4쌍 레지스터에서 버퍼로부터 데이터를 효율적으로 읽기 위해서이다. 그래서 LightDirection, LightColor모두 Vector4타입을 사용했다. 4번째값을 사용하지 않고 패딩의 역할을 하는 것이다. struct LightProperty의 경우도 16바이트 배수로 데이터형을 맞추기 위해 패딩으로 8바이트를 추가해준 것이다.

C++
ID3D11Buffer* LightDirectionBuffer;
ID3D11Buffer* LightColorBuffer;

D3D11_BUFFER_DESC bd;  
ZeroMemory( &bd, sizeof( D3D11_BUFFER_DESC ) );  
  
bd.Usage          = D3D11_USAGE_DYNAMIC;    
bd.ByteWidth      = sizeof( Vector4 );    
bd.BindFlags      = D3D11_BIND_CONSTANT_BUFFER;    
bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
  
ID3D11Device->CreateBuffer( &bd, nullptr, &LightDirectionBuffer );
ID3D11Device->CreateBuffer( &bd, nullptr, &LightColorBuffer );

이렇게 버퍼를 생성한다.



Rendering Pipeline에 연결하기


여기와 거의 동일하다. 다만 이 2개의 Constant BufferPixel Shader에서 사용할 버퍼이므로 바인딩하는 함수가 다르다.

C++
ID3D11DeviceContext->PSSetConstantBuffers( 0, 1, &LightDirectionBuffer );
ID3D11DeviceContext->PSSetConstantBuffers( 1, 1, &LightColorBuffer );

VSSetConstantBuffers대신 PSSetConstantBuffers를 호출하였다. 슬롯은 각각 빛의 방향이 0번 슬롯, 빛의 색이 1번 슬롯에 할당되었다.



Buffer의 내용 갱신하기


갱신도 동일하다. 방향과 빛의 데이터값을 갱신해준다.

C++
Vector4 direction = Vector4( 0.f, 0.f, -1.f, 1.f ) );

D3D11_MAPPED_SUBRESOURCE mappedResource;  
ID3D11DeviceContext->Map( LightDirectionBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource );  
{  
    memcpy_s( mappedResource.pData, sizeof( Vector4 ), &direction, sizeof( Vector4 ) );  
}  
ID3D11DeviceContext->Unmap( LightDirectionBuffer, 0 );

Vector4 color = Vector4( 0.f, 0.f, 1.f, 1.f ) );

ID3D11DeviceContext->Map( LightColorBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource );  
{  
    memcpy_s( mappedResource.pData, sizeof( Vector4 ), &color, sizeof( Vector4 ) );  
}  
ID3D11DeviceContext->Unmap( LightColorBuffer, 0 );


Shader 수정


라이팅 계산을 할 수 있도록 쉐이더 코드의 수정도 필요하다.




정점 쉐이더의 경우 정점 데이터에서 법선 벡터를 포함할 수 있도록 해야 한다.

HLSL
struct PixelIn  
{  
    float4 position : SV_POSITION;  
    float2 texCoord : TEXCOORD;  
    float3 normal   : NORMAL;
};  
  
PixelIn VS( float4 position : POSITION, float2 texCoord : TEXCOORD, float3 normal : NORMAL )  
{  
    PixelIn output;  
    output.position = position;  
    output.texCoord = texCoord;  
    output.normal   = normal;
  
    return output;  
}



픽셀 쉐이더에서는 전달 받은 픽셀의 법선 벡터를 통해 반사량을 계산하고 빛의 컬러와 함께 빛의 색을 결정한 다음 텍스처 샘플링 컬러값과 블렌딩하여 최종 컬러값을 결정한다.

HLSL
cbuffer LightDirectionBuffer : register( b0 )  
{  
    float4 lightDirection;  
};  
  
cbuffer LightColorBuffer : register( b1 )  
{  
    float4 lightColor;  
};  
  
  
float4 PS( PixelIn input ) : SV_TARGET  
{  
    float4 textureColor = float4( 1.f, 1.f, 1.f, 1.f );  
    textureColor = psTexture.Sample( SampleType, input.texCoord );  
  
    float3 lightDir = normalize( -lightDirection );  
    float lightIntensity = saturate( dot( lightDir, input.normal ) );
      
    return textureColor * saturate( lightColor * lightIntensity );  
}

그리고 빛의 방향과 색은 Constant Buffer에서 가져온다. Rendering Pipeline에 연결하기에서 연결한 슬롯에 맞추어 레지스터를 지정한다. cbuffer LightDirectionBuffer : register( b0 )cbuffer TransformBuffer : register( b0 )와 레지스터가 중복되어 문제가 생길 수 있으나 연결된 렌더링 파이프라인의 스테이지가 다르므로 이슈가 없다.



슬롯 관련해서 몇가지 테스트해본 사항들을 기록한다.


HLSL
cbuffer LightDirectionBuffer : register( b0 )  
{  
    float4 lightDirection;  
};  
  
cbuffer LightColorBuffer : register( b1 )  
{  
    float4 lightColor;  
};  

cbuffer TestDummyBuffer : register( b1 )  
{  
    float4 testDummy;  
};  
  
  
float4 PS( PixelIn input ) : SV_TARGET  
{  
    float4 textureColor = float4( 1.f, 1.f, 1.f, 1.f );  
    textureColor = psTexture.Sample( SampleType, input.texCoord );  
  
    float3 lightDir = normalize( -lightDirection );  
    float lightIntensity = saturate( dot( lightDir, input.normal ) );
      
    return textureColor * saturate( lightColor * lightIntensity );  
}

위의 경우 추가한 testDummylightColor와 동일한 버퍼를 가리킨다. 즉, 둘의 값은 동일하기 때문에 어느것을 이용해도 동일한 결과값을 가진다.


HLSL
cbuffer TestDummyBuffer : register( b1 )  
{  
    float4 testDummy;  
};  

PixelIn VS( float4 position : POSITION, float2 texCoord : TEXCOORD, float3 normal : NORMAL )  
{  
    PixelIn output;  
    output.position = position;  
    output.texCoord = texCoord;  
    output.normal   = testDummy;
  
    return output;  
}

위의 경우 testDummy는 올바른 Constant Buffer를 가리키지 않는다. Vertex Shader에는 연결된 1번 슬롯의 Constant Buffer가 없기 때문이다. 즉 프로그램 단계에서 연결한 렌더링 파이프라인 스테이지와 슬롯, 그리고 데이터형만 hlsl에서 올바르게 매치해서 사용하면 별도의 구분에 신경쓸 필요는 없다.



InputLayout 수정


당연하게도 정점 쉐이더의 내용, 그 중에서 입력받는 시그니처가 변경되었으므로 InputLayout또한 수정이 되어야 한다.

C++
D3D11_INPUT_ELEMENT_DESC elements[] =  
{  
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,                            D3D11_INPUT_PER_VERTEX_DATA, 0 },  
    { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT,    0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },  
    { "NORMAL",   0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },  
};


Vertex Data 수정


정점 데이터에 법선 벡터를 지정해야 한다.

C++
CRVertex GCRVRect[ 4 ] =  
{  
    { .Position = CRVector( -0.5f,  0.5f, 0.0f ), .TexCoord = CRVector2D( 0.0f, 0.0f ), .Normal = CRVector( 0.f, 0.f, 1.f ) },  
    { .Position = CRVector(  0.5f,  0.5f, 0.0f ), .TexCoord = CRVector2D( 1.0f, 0.0f ), .Normal = CRVector( 0.f, 0.f, 1.f ) },  
    { .Position = CRVector( -0.5f, -0.5f, 0.0f ), .TexCoord = CRVector2D( 0.0f, 1.0f ), .Normal = CRVector( 0.f, 0.f, 1.f ) },  
    { .Position = CRVector(  0.5f, -0.5f, 0.0f ), .TexCoord = CRVector2D( 1.0f, 1.0f ), .Normal = CRVector( 0.f, 0.f, 1.f ) },  
};

결과물은 아래와 같이 얻을 수 있다.

Pasted image 20241226001058.png


yunei0313/CRY at Directional-light에서 코드 확인이 가능하다.

Constant Buffer의 특성상 코드 재사용률이 매우 높아 CRD11BindingConstantBuffer로 템플릿 클래스를 작성하여 사용하였다. 다음은 사용 예시 코드이다.

C++
CRD11BindingConstantBuffer< CRMatrix   > TransformBuffer;  
CRD11BindingConstantBuffer< CRVector4D > LightDirectionBuffer;  
CRD11BindingConstantBuffer< CRVector4D > LightColorBuffer;

TransformBuffer.Create( "Transform", 0, ED11RenderingPipelineStage::VS );  
TransformBuffer.SetInRenderingPipeline();  
  
LightDirectionBuffer.Create( "LightDirection", 0, ED11RenderingPipelineStage::PS );  
LightDirectionBuffer.SetInRenderingPipeline();  
  
LightColorBuffer.Create( "LightColor", 1, ED11RenderingPipelineStage::PS );  
LightColorBuffer.SetInRenderingPipeline();


이전글 : [DirectX11] 12. 변환 – Constant Buffer에 대해 알아보자.
다음글 : [DirectX11] 14. ImGUI 설치하기



Leave a Comment