DirectX11 Tutorial 40: 그림자 매핑

강좌번역/DirectX 11 2019. 1. 7. 23:52 by 빠재

원문: Tutorial 40: Shadow Mapping

이번 예제에서는 DirectX 11에서 C++와 HLSL을 이용하여 그림자 매핑을 구현하는 방법을 다룹니다. 이 튜토리얼을 시작하기 전에 여러분들이 이미 다음 주제에 대해 잘 알고 있어야 합니다: 렌더 투 텍스쳐 (22번 튜토리얼), 투영 텍스쳐(27번 튜토리얼), 깊이 버퍼(35번 튜토리얼).

그림자 매핑은 작거나 중간 크기의 화면에 그림자를 그리는 데 가장 빠르고 CPU/GPU 입장에서 효율적인 방법 중 하나입니다. 또한 매우 사실적인 결과를 내는 간단한 방법 중의 하나이기도 합니다. 그림자 매핑이 어떻게 작동하는지 이해하기 위해 빛 하나가 비춰진 장면 하나를 가지고 시작하겠습니다.

장면을 비추고 있는 빛은 현재 카메라 위치의 후방 오른쪽에 위치하고 있습니다. 다음 단계에서 빛의 시점으로 장면을 그릴 것이기 때문에 빛의 위치가 매우 중요합니다. 빛의 시점에서 장면을 그릴 때는 오직 깊이 버퍼의 정보만 렌더 투 텍스쳐에 그릴 것입니다. 이렇게 렌더 투 텍스쳐에 깊이 버퍼가 그려진 것을 그림자 맵(Shadow map)이라 하고 아래 그림과 같이 생겼습니다:

이제 장면 전체에 모든 그림자를 받을 수 있는 물체의 깊이 정보를 알고 있기 때문에 어느 부분에 그림자가 드리워야 할 지 알아낼 수 있습니다. 장면을 그릴 때 그림자 맵을 장면에 투영시켜 그림자를 드리우는 물체의 깊이를 가져오고 픽셀 셰이더에서 픽셀 단위로 빛의 위치와 비교합니다. 만약 빛이 카메라보다 가깝다면 픽셀을 밝게 합니다. 만약 카메라가 더 가깝다면 픽셀에 그림자를 넣습니다. 이런 작업을 하면 아래와 같은 이미지가 만들어집니다:

이제 HLSL 셰이더를 먼저 확인하면서 그림자 맵 예제의 코드를 보겠습니다.

Shadow.vs

그림자 셰이더는 그림자 그리기를 수행하는 기본적인 점조명 셰이더입니다.

////////////////////////////////////////////////////////////////////////////////
// Filename: shadow.vs
////////////////////////////////////////////////////////////////////////////////


/////////////
// GLOBALS //
/////////////

정점 셰이더의 MatrixBuffer에 조명의 시점에 기초한 두 개의 행렬이 추가로 필요합니다.

cbuffer MatrixBuffer
{
    matrix worldMatrix;
    matrix viewMatrix;
    matrix projectionMatrix;
    matrix lightViewMatrix;
    matrix lightProjectionMatrix;
};


//////////////////////
// CONSTANT BUFFERS //
//////////////////////
cbuffer LightBuffer2
{
    float3 lightPosition;
    float padding;
};


//////////////
// TYPEDEFS //
//////////////
struct VertexInputType
{
    float4 position : POSITION;
    float2 tex : TEXCOORD0;
    float3 normal : NORMAL;
};

PixelInputType에는 조명 시점으로 변환된 정점의 위치를 픽셀 셰이더로 전달하기 위해 lightViewPosition변수가 있습니다.

struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
    float3 normal : NORMAL;
    float4 lightViewPosition : TEXCOORD1;
    float3 lightPos : TEXCOORD2;
};


////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
PixelInputType ShadowVertexShader(VertexInputType input)
{
    PixelInputType output;
    float4 worldPosition;
    
    
    // 행렬 연산을 위해 위치 벡터의 원소 수가 4개가 되게 합니다.
    input.position.w = 1.0f;

    // 정점의 위치를 월드, 뷰, 투영 행렬에 대하여 계산합니다.
    output.position = mul(input.position, worldMatrix);
    output.position = mul(output.position, viewMatrix);
    output.position = mul(output.position, projectionMatrix);

정점을 조명의 시점으로 변환합니다.

    // 정점들의 위치를 광원의 시점에서의 위치로 계산합니다.
    output.lightViewPosition = mul(input.position, worldMatrix);
    output.lightViewPosition = mul(output.lightViewPosition, lightViewMatrix);
    output.lightViewPosition = mul(output.lightViewPosition, lightProjectionMatrix);

    // 픽셀 셰이더의 텍스쳐 좌표를 저장합니다.
    output.tex = input.tex;
    
    // 노말 벡터를 월드 행렬에만 곱합니다.
    output.normal = mul(input.normal, (float3x3)worldMatrix);
    
    // 노말 벡터를 정규화합니다.
    output.normal = normalize(output.normal);

    // 월드에서의 정점의 위치를 계산합니다.
    worldPosition = mul(input.position, worldMatrix);

    // 조명의 위치에서의 정점의 위치를 조명의 위치와 정점의 월드 위치를 이용하여 계산합니다.
    output.lightPos = lightPosition.xyz - worldPosition.xyz;

    // 조명 위치 벡터를 정규화합니다.
    output.lightPos = normalize(output.lightPos);

    return output;
}

Shadow.ps

////////////////////////////////////////////////////////////////////////////////
// Filename: shadow.ps
////////////////////////////////////////////////////////////////////////////////


//////////////
// TEXTURES //
//////////////
Texture2D shaderTexture : register(t0);

depthMapTexture이 그림자 맵입니다. 이 텍스쳐는 조명의 시점에서 그려진 깊이 버퍼입니다.

Texture2D depthMapTexture : register(t1);


///////////////////
// SAMPLE STATES //
///////////////////

깊이 버퍼를 샘플링할 때는 clamp를 사용하는 샘플러를 사용합니다. 그래야 특정 지점에서 래핑되어 잘못된 정보를 샘플링하는 일이 없어집니다.

SamplerState SampleTypeClamp : register(s0);
SamplerState SampleTypeWrap  : register(s1);


//////////////////////
// CONSTANT BUFFERS //
//////////////////////
cbuffer LightBuffer
{
    float4 ambientColor;
    float4 diffuseColor;
};


//////////////
// TYPEDEFS //
//////////////
struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
    float3 normal : NORMAL;
    float4 lightViewPosition : TEXCOORD1;
    float3 lightPos : TEXCOORD2;
};


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
float4 ShadowPixelShader(PixelInputType input) : SV_TARGET
{
    float bias;
    float4 color;
    float2 projectTexCoord;
    float depthValue;
    float lightDepthValue;
    float lightIntensity;
    float4 textureColor;

깊이 맵의 부동소수점 해상도가 낮기 때문에 그림자 매핑에서 빛의 깊이와 물체의 깊이를 비교할 때 바이어스 보정이 필요합니다.

    // 부동소수점 오차 문제를 해결하기 위한 바이어스 값을 설정합니다.
    bias = 0.001f;

    // 모든 픽셀에 기본 색상으로 주변광 값을 사용합니다.
    color = ambientColor;

그림자 맵(깊이 버퍼 텍스쳐) 샘플링에 쓰일 투영된 텍스쳐 좌표를 조명의 시점에 기초하여 계산합니다.

    // 투영된 텍스쳐 좌표를 계산합니다.
    projectTexCoord.x =  input.lightViewPosition.x / input.lightViewPosition.w / 2.0f + 0.5f;
    projectTexCoord.y = -input.lightViewPosition.y / input.lightViewPosition.w / 2.0f + 0.5f;

투영된 좌표가 조명의 시야에 있는지 확인하고 그렇지 않다면 해당 픽셀이 주변광의 값이 되도록 합니다.

    // 투영된 좌표가 0부터 1 사이의 값인지 확인합니다. 범위 안에 있다면 해당 픽셀은 조명의 시야 안에 있습니다.
    if((saturate(projectTexCoord.x) == projectTexCoord.x) && (saturate(projectTexCoord.y) == projectTexCoord.y))
    {

이제 조명의 시야 안에 있기 때문에 그림자 맵(depthMapTexture)에서 깊이 값을 가져옵니다. 그림자 맵이 흑백 텍스쳐이기 때문에 빨간색 채널만을 샘플링합니다. 이 텍스쳐에서 얻어온 깊이값은 가장 가까운 물체까지의 거리로 볼 수 있습니다. 해당 물체가 그림자를 드리우는 것이기 때문에 이 가정은 중요하며 이 텍스쳐가 그림자 맵이라고 불리는 이유이기도 합니다.

        // 투영된 텍스쳐 좌표 위치의 샘플러를 사용하여 깊이 텍스쳐에서 그림자 맵의 값을 샘플링합니다.
        depthValue = depthMapTexture.Sample(SampleTypeClamp, projectTexCoord).r;

해당 픽셀의 물체의 깊이를 가져왔으므로 이제 조명의 깊이를 가져와 조명이 물체의 앞에 있는지 뒤에 있는지 확인해야 합니다. 이 값은 lightViewPosition에서 가져옵니다. 부동소수점 정확도 문제로 bias값을 추가로 빼야 한다는 점에 유의하시기 바랍니다.

        // 조명의 깊이를 계산합니다.
        lightDepthValue = input.lightViewPosition.z / input.lightViewPosition.w;

        // lightDepthValue에서 bias를 뺍니다.
        lightDepthValue = lightDepthValue - bias;

이제 조명의 깊이와 물체의 깊이값을 비교합니다. 만약 조명이 카메라보다 가깝다면 그림자가 없는 위치입니다. 하지만 그림자 맵에서 조명이 물체 뒤에 있다면 그림자가 드리워질 것입니다. 그림자는 단지 주변광 조명에만 적용되는 것이기 때문에 물체를 그저 까맣게 색칠하거나 하는 일은 하지 않습니다.

        // 그림자 맵의 깊이값과 조명의 싶이값을 비교하여 이 픽셀이 그림자를 드리울지 말지를 정합니다.
        // 조명이 물체 앞에 있다면 픽셀을 밝히고, 그렇지 않다면 물체가 그림자를 드리우는 것이기 때문에 픽셀을 어둡게 합니다.
        if(lightDepthValue < depthValue)
        {

빛이 물체보다 앞에 있다면 그림자가 없는 것이므로 평소대로 조명을 주면 됩니다.

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

            if(lightIntensity > 0.0f)
            {
                // 최종 디퓨즈 색상을 기본 디퓨즈 색상과 빛의 강도를 가지고 계산합니다.
                color += (diffuseColor * lightIntensity);

                // 최종 조명 색상을 너무 큰 값이 되지 않게 합니다.
                color = saturate(color);
            }
        }
    }

    // 현재 텍스쳐 좌표 위치의 샘플러를 이용하여 텍스쳐의 픽셀의 색상을 샘플링합니다.
    textureColor = shaderTexture.Sample(SampleTypeWrap, input.tex);

    // 조명과 텍스쳐 색상을 혼합합니다.
    color = color * textureColor;

    return color;
}

Shadowshaderclass.h

ShadowShaderClassLightShaderClass에 그림자를 위한 기능이 수정되었습니다.

////////////////////////////////////////////////////////////////////////////////
// Filename: shadowshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SHADOWSHADERCLASS_H_
#define _SHADOWSHADERCLASS_H_


//////////////
// INCLUDES //
//////////////
#include <d3d11.h>
#include <d3dx10math.h>
#include <d3dx11async.h>
#include <fstream>
using namespace std;


////////////////////////////////////////////////////////////////////////////////
// Class name: ShadowShaderClass
////////////////////////////////////////////////////////////////////////////////
class ShadowShaderClass
{
private:

MatrixBufferType는 조명의 뷰와 투영 행렬을 추가로 가집니다.

    struct MatrixBufferType
    {
        D3DXMATRIX world;
        D3DXMATRIX view;
        D3DXMATRIX projection;
        D3DXMATRIX lightView;
        D3DXMATRIX lightProjection;
    };

    struct LightBufferType
    {
        D3DXVECTOR4 ambientColor;
        D3DXVECTOR4 diffuseColor;
    };

    struct LightBufferType2
    {
        D3DXVECTOR3 lightPosition;
        float padding;
    };

public:
    ShadowShaderClass();
    ShadowShaderClass(const ShadowShaderClass&);
    ~ShadowShaderClass();

    bool Initialize(ID3D11Device*, HWND);
    void Shutdown();
    bool Render(ID3D11DeviceContext*, int, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, ID3D11ShaderResourceView*, 
            ID3D11ShaderResourceView*, D3DXVECTOR3, D3DXVECTOR4, D3DXVECTOR4);

private:
    bool InitializeShader(ID3D11Device*, HWND, WCHAR*, WCHAR*);
    void ShutdownShader();
    void OutputShaderErrorMessage(ID3D10Blob*, HWND, WCHAR*);

    bool SetShaderParameters(ID3D11DeviceContext*, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, D3DXMATRIX, ID3D11ShaderResourceView*, 
                 ID3D11ShaderResourceView*, D3DXVECTOR3, D3DXVECTOR4, D3DXVECTOR4);
    void RenderShader(ID3D11DeviceContext*, int);

private:
    ID3D11VertexShader* m_vertexShader;
    ID3D11PixelShader* m_pixelShader;
    ID3D11InputLayout* m_layout;
    ID3D11SamplerState* m_sampleStateWrap;

clamp를 사용하는 두번째 샘플러 변수를 선언합니다.

    ID3D11SamplerState* m_sampleStateClamp;
    ID3D11Buffer* m_matrixBuffer;
    ID3D11Buffer* m_lightBuffer;
    ID3D11Buffer* m_lightBuffer2;
};

#endif

Shadowshaderclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: shadowshaderclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "shadowshaderclass.h"


ShadowShaderClass::ShadowShaderClass()
{
    m_vertexShader = 0;
    m_pixelShader = 0;
    m_layout = 0;
    m_sampleStateWrap = 0;

생성자에서 새로운 샘플러 스테이트 변수를 null로 초기화합니다.

    m_sampleStateClamp = 0;
    m_matrixBuffer = 0;
    m_lightBuffer = 0;
    m_lightBuffer2 = 0;
}


ShadowShaderClass::ShadowShaderClass(const ShadowShaderClass& other)
{
}


ShadowShaderClass::~ShadowShaderClass()
{
}


bool ShadowShaderClass::Initialize(ID3D11Device* device, HWND hwnd)
{
    bool result;

그림자 HLSL 셰이더 파일을 로드합니다.

    // 정점 및 픽셀 셰이더를 초기화합니다.
    result = InitializeShader(device, hwnd, L"../Engine/shadow.vs", L"../Engine/shadow.ps");
    if(!result)
    {
        return false;
    }

    return true;
}


void ShadowShaderClass::Shutdown()
{
    // 정점 및 픽셀 셰이더 그리고 관련된 객체들을 해제합니다.
    ShutdownShader();

    return;
}

그림자 맵 텍스쳐는 Render 함수의 인자로 전달됩니다. 전달된 텍스쳐는 렌더링되기 전에 SetShaderParameters 함수를 통해 셰이더에 설정됩니다.

bool ShadowShaderClass::Render(ID3D11DeviceContext* deviceContext, int indexCount, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, 
                   D3DXMATRIX projectionMatrix, D3DXMATRIX lightViewMatrix, D3DXMATRIX lightProjectionMatrix, 
                   ID3D11ShaderResourceView* texture, ID3D11ShaderResourceView* depthMapTexture, D3DXVECTOR3 lightPosition, 
                   D3DXVECTOR4 ambientColor, D3DXVECTOR4 diffuseColor)
{
    bool result;


    // Set the shader parameters that it will use for rendering.
    result = SetShaderParameters(deviceContext, worldMatrix, viewMatrix, projectionMatrix, lightViewMatrix, lightProjectionMatrix, texture, 
                     depthMapTexture, lightPosition, ambientColor, diffuseColor);
    if(!result)
    {
        return false;
    }

    // Now render the prepared buffers with the shader.
    RenderShader(deviceContext, indexCount);

    return true;
}


bool ShadowShaderClass::InitializeShader(ID3D11Device* device, HWND hwnd, WCHAR* vsFilename, WCHAR* psFilename)
{
    HRESULT result;
    ID3D10Blob* errorMessage;
    ID3D10Blob* vertexShaderBuffer;
    ID3D10Blob* pixelShaderBuffer;
    D3D11_INPUT_ELEMENT_DESC polygonLayout[3];
    unsigned int numElements;
    D3D11_SAMPLER_DESC samplerDesc;
    D3D11_BUFFER_DESC matrixBufferDesc;
    D3D11_BUFFER_DESC lightBufferDesc;

픽셀 셰이더에서 사용하는 두 번째 clamp를 사용하는 샘플러 상태의 디스크립션입니다.

    D3D11_BUFFER_DESC lightBufferDesc2;


    // 이 함수에서 사용할 포인터들을 null로 초기화합니다.
    errorMessage = 0;
    vertexShaderBuffer = 0;
    pixelShaderBuffer = 0;

그림자 정점 셰이더를 불러옵니다.

    // 정점 셰이더를 컴파일합니다.
    result = D3DX11CompileFromFile(vsFilename, NULL, NULL, "ShadowVertexShader", "vs_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, NULL, 
                       &vertexShaderBuffer, &errorMessage, NULL);
    if(FAILED(result))
    {
        // If the shader failed to compile it should have writen something to the error message.
        if(errorMessage)
        {
            OutputShaderErrorMessage(errorMessage, hwnd, vsFilename);
        }
        // If there was nothing in the error message then it simply could not find the shader file itself.
        else
        {
            MessageBox(hwnd, vsFilename, L"Missing Shader File", MB_OK);
        }

        return false;
    }

그림자 픽셀 셰이더를 불러옵니다.

    // 픽셀 셰이더를 컴파일합니다.
    result = D3DX11CompileFromFile(psFilename, NULL, NULL, "ShadowPixelShader", "ps_5_0", D3D10_SHADER_ENABLE_STRICTNESS, 0, NULL, 
                       &pixelShaderBuffer, &errorMessage, NULL);
    if(FAILED(result))
    {
        // If the shader failed to compile it should have writen something to the error message.
        if(errorMessage)
        {
            OutputShaderErrorMessage(errorMessage, hwnd, psFilename);
        }
        // If there was nothing in the error message then it simply could not find the file itself.
        else
        {
            MessageBox(hwnd, psFilename, L"Missing Shader File", MB_OK);
        }

        return false;
    }

    // 버퍼에서 정점 셰이더를 생성합니다.
    result = device->CreateVertexShader(vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), NULL, &m_vertexShader);
    if(FAILED(result))
    {
        return false;
    }

    // 버퍼에서 픽셀 세이더를 생성합니다.
    result = device->CreatePixelShader(pixelShaderBuffer->GetBufferPointer(), pixelShaderBuffer->GetBufferSize(), NULL, &m_pixelShader);
    if(FAILED(result))
    {
        return false;
    }

    // 정점 입력 레이아웃 디스크립션을 작성합니다.
    polygonLayout[0].SemanticName = "POSITION";
    polygonLayout[0].SemanticIndex = 0;
    polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT;
    polygonLayout[0].InputSlot = 0;
    polygonLayout[0].AlignedByteOffset = 0;
    polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
    polygonLayout[0].InstanceDataStepRate = 0;

    polygonLayout[1].SemanticName = "TEXCOORD";
    polygonLayout[1].SemanticIndex = 0;
    polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT;
    polygonLayout[1].InputSlot = 0;
    polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
    polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
    polygonLayout[1].InstanceDataStepRate = 0;

    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;

    // 레이아웃의 개수를 구합니다.
    numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]);

    // 정점 입력 레이아웃을 생성합니다.
    result = device->CreateInputLayout(polygonLayout, numElements, vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), 
                       &m_layout);
    if(FAILED(result))
    {
        return false;
    }

    // 더 이상 사용하지 않는 정점 및 픽셀 셰이더 버퍼를 해제합니다.
    vertexShaderBuffer->Release();
    vertexShaderBuffer = 0;

    pixelShaderBuffer->Release();
    pixelShaderBuffer = 0;

    // 래핑 텍스쳐 샘플러 상태 디스크립션을 작성합니다.
    samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
    samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
    samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
    samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
    samplerDesc.MipLODBias = 0.0f;
    samplerDesc.MaxAnisotropy = 1;
    samplerDesc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
    samplerDesc.BorderColor[0] = 0;
    samplerDesc.BorderColor[1] = 0;
    samplerDesc.BorderColor[2] = 0;
    samplerDesc.BorderColor[3] = 0;
    samplerDesc.MinLOD = 0;
    samplerDesc.MaxLOD = D3D11_FLOAT32_MAX;

    // 텍스쳐 샘플러 상태를 생성합니다.
    result = device->CreateSamplerState(&samplerDesc, &m_sampleStateWrap);
    if(FAILED(result))
    {
        return false;
    }

clamp를 사용하는 샘플러 상태를 생성합니다.

    // clamp 텍스쳐 샘플러 상태 디스크립션을 작성합니다.
    samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
    samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
    samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;

    // 텍스쳐 샘플러 상태를 생성합니다.
    result = device->CreateSamplerState(&samplerDesc, &m_sampleStateClamp);
    if(FAILED(result))
    {
        return false;
    }

    // 정점 셰이더에 있는 동적 행렬 상수 버퍼의 디스크립션을 설정합니다.
    matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
    matrixBufferDesc.ByteWidth = sizeof(MatrixBufferType);
    matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    matrixBufferDesc.MiscFlags = 0;
    matrixBufferDesc.StructureByteStride = 0;

    // 이 클래스에서 정점 셰이더 상수 버퍼에 접근할 수 있도록 상수 버퍼 포인터를 생성합니다.
    result = device->CreateBuffer(&matrixBufferDesc, NULL, &m_matrixBuffer);
    if(FAILED(result))
    {
        return false;
    }

    // 픽셀 셰이더의 조명 동적 상수 버퍼의 디스크립션을 설정합니다.
    lightBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
    lightBufferDesc.ByteWidth = sizeof(LightBufferType);
    lightBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    lightBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    lightBufferDesc.MiscFlags = 0;
    lightBufferDesc.StructureByteStride = 0;

    // 이 클래스에서 픽셀 셰이더 상수 버퍼에 접근할 수 있도록 상수 버퍼 포인터를 생성합니다.
    result = device->CreateBuffer(&lightBufferDesc, NULL, &m_lightBuffer);
    if(FAILED(result))
    {
        return false;
    }

    // 정점 셰이더의 조명 동적 상수 버퍼의 디스크립션을 설정합니다.
    lightBufferDesc2.Usage = D3D11_USAGE_DYNAMIC;
    lightBufferDesc2.ByteWidth = sizeof(LightBufferType2);
    lightBufferDesc2.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    lightBufferDesc2.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    lightBufferDesc2.MiscFlags = 0;
    lightBufferDesc2.StructureByteStride = 0;

    // 이 클래스에서 정점 셰이더 상수 버퍼에 접근할 수 있도록 상수 버퍼 포인터를 생성합니다.
    result = device->CreateBuffer(&lightBufferDesc2, NULL, &m_lightBuffer2);
    if(FAILED(result))
    {
        return false;
    }

    return true;
}


void ShadowShaderClass::ShutdownShader()
{
    // 조명 상수 버퍼를 해제합니다.
    if(m_lightBuffer)
    {
        m_lightBuffer->Release();
        m_lightBuffer = 0;
    }

    if(m_lightBuffer2)
    {
        m_lightBuffer2->Release();
        m_lightBuffer2 = 0;
    }

    // 행렬 상수 버퍼를 해제합니다.
    if(m_matrixBuffer)
    {
        m_matrixBuffer->Release();
        m_matrixBuffer = 0;
    }

    // 샘플러 상태들을 해제합니다.
    if(m_sampleStateWrap)
    {
        m_sampleStateWrap->Release();
        m_sampleStateWrap = 0;
    }

clamp 샘플러 상태도 해제합니다.

    if(m_sampleStateClamp)
    {
        m_sampleStateClamp->Release();
        m_sampleStateClamp = 0;
    }

    // 레이아웃을 해제합니다.
    if(m_layout)
    {
        m_layout->Release();
        m_layout = 0;
    }

    // 픽셀 셰이더를 해제합니다.
    if(m_pixelShader)
    {
        m_pixelShader->Release();
        m_pixelShader = 0;
    }

    // 정점 셰이더를 해제합니다.
    if(m_vertexShader)
    {
        m_vertexShader->Release();
        m_vertexShader = 0;
    }

    return;
}


void ShadowShaderClass::OutputShaderErrorMessage(ID3D10Blob* errorMessage, HWND hwnd, WCHAR* shaderFilename)
{
    char* compileErrors;
    unsigned long bufferSize, i;
    ofstream fout;


    // Get a pointer to the error message text buffer.
    compileErrors = (char*)(errorMessage->GetBufferPointer());

    // Get the length of the message.
    bufferSize = errorMessage->GetBufferSize();

    // Open a file to write the error message to.
    fout.open("shader-error.txt");

    // Write out the error message.
    for(i=0; i<bufferSize; i++)
    {
        fout << compileErrors[i];
    }

    // Close the file.
    fout.close();

    // Release the error message.
    errorMessage->Release();
    errorMessage = 0;

    // Pop a message up on the screen to notify the user to check the text file for compile errors.
    MessageBox(hwnd, L"Error compiling shader.  Check shader-error.txt for message.", shaderFilename, MB_OK);

    return;
}


bool ShadowShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix,
                        D3DXMATRIX projectionMatrix, D3DXMATRIX lightViewMatrix, D3DXMATRIX lightProjectionMatrix, 
                        ID3D11ShaderResourceView* texture, ID3D11ShaderResourceView* depthMapTexture, D3DXVECTOR3 lightPosition,
                        D3DXVECTOR4 ambientColor, D3DXVECTOR4 diffuseColor)
{
    HRESULT result;
    D3D11_MAPPED_SUBRESOURCE mappedResource;
    unsigned int bufferNumber;
    MatrixBufferType* dataPtr;
    LightBufferType* dataPtr2;
    LightBufferType2* dataPtr3;


    // 이 셰이더에 사용할 행렬들을 전치합니다.
    D3DXMatrixTranspose(&worldMatrix, &worldMatrix);
    D3DXMatrixTranspose(&viewMatrix, &viewMatrix);
    D3DXMatrixTranspose(&projectionMatrix, &projectionMatrix);

정점 셰이더에 조명 행렬들을 보내기 전에 먼저 전치를 합니다.

    D3DXMatrixTranspose(&lightViewMatrix, &lightViewMatrix);
    D3DXMatrixTranspose(&lightProjectionMatrix, &lightProjectionMatrix);

    // 쓰기 위해 조명 상수 버퍼를 잠급니다.
    result = deviceContext->Map(m_matrixBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
    if(FAILED(result))
    {
        return false;
    }

    // 상수 버퍼의 데이터 포인터를 얻어옵니다.
    dataPtr = (MatrixBufferType*)mappedResource.pData;

    // 상수 버퍼에 행렬을 복사합니다.
    dataPtr->world = worldMatrix;
    dataPtr->view = viewMatrix;
    dataPtr->projection = projectionMatrix;

조명 행렬을 행렬 상수 버퍼에 복사합니다.

    dataPtr->lightView = lightViewMatrix;
    dataPtr->lightProjection = lightProjectionMatrix;

    // 상수 버퍼의 잠금을 해제합니다.
    deviceContext->Unmap(m_matrixBuffer, 0);

    // 정점 셰이더에서의 상수 버퍼 위치입니다.
    bufferNumber = 0;

    // 정점 셰이더의 상수 버퍼를 갱신된 값으로 설정합니다.
    deviceContext->VSSetConstantBuffers(bufferNumber, 1, &m_matrixBuffer);

    // 픽셀 셰이더에 셰이더 텍스쳐 리소스를 설정합니다.
    deviceContext->PSSetShaderResources(0, 1, &texture);

그림자 맵 텍스쳐를 픽셀 셰이더에 설정합니다.

    deviceContext->PSSetShaderResources(1, 1, &depthMapTexture);

    // 쓰기 위하여 조명 상수 버퍼를 잠급니다.
    result = deviceContext->Map(m_lightBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
    if(FAILED(result))
    {
        return false;
    }

    // 상수 버퍼의 데이터 포인터를 얻어옵니다.
    dataPtr2 = (LightBufferType*)mappedResource.pData;

    // 상수 버퍼에 행렬을 복사합니다.
    dataPtr2->ambientColor = ambientColor;
    dataPtr2->diffuseColor = diffuseColor;

    // 상수 버퍼의 잠금을 해제합니다.
    deviceContext->Unmap(m_lightBuffer, 0);

    // 픽셀 셰이더에서의 조명 상수 버퍼 위치입니다.
    bufferNumber = 0;

    // 마지막으로 픽셀 셰이더의 조명 상수 버퍼를 갱신된 값으로 설정합니다.
    deviceContext->PSSetConstantBuffers(bufferNumber, 1, &m_lightBuffer);

    // 쓰기 위하여 두 번째 조명 상수 버퍼를 잠급니다.
    result = deviceContext->Map(m_lightBuffer2, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
    if(FAILED(result))
    {
        return false;
    }

    // 상수 버퍼의 데이터 포인터를 얻어옵니다.
    dataPtr3 = (LightBufferType2*)mappedResource.pData;

    // 상수 버퍼에 조명 변수들을 복사합니다.
    dataPtr3->lightPosition = lightPosition;
    dataPtr3->padding = 0.0f;

    // 상수 버퍼의 잠금을 해제합니다.
    deviceContext->Unmap(m_lightBuffer2, 0);

    // 정점 셰이더에서의 조명 상수 버퍼 위치입니다.
    bufferNumber = 1;

    // 마지막으로 픽셀 셰이더의 조명 상수 버퍼를 갱신된 값으로 설정합니다.
    deviceContext->VSSetConstantBuffers(bufferNumber, 1, &m_lightBuffer2);

    return true;
}


void ShadowShaderClass::RenderShader(ID3D11DeviceContext* deviceContext, int indexCount)
{
    // 정점 입력 레이아웃을 설정합니다.
    deviceContext->IASetInputLayout(m_layout);

    // 이 삼각형을 그리는 데 사용할 정점 및 픽셀 셰이더를 설정합니다.
    deviceContext->VSSetShader(m_vertexShader, NULL, 0);
    deviceContext->PSSetShader(m_pixelShader, NULL, 0);

픽셀 셰이더에 사용할 clamp 샘플러 상태를 설정합니다.

    // 픽셀 셰이더에서 사용할 샘플러 상태를 설정합니다.
    deviceContext->PSSetSamplers(0, 1, &m_sampleStateClamp);
    deviceContext->PSSetSamplers(1, 1, &m_sampleStateWrap);

    // 삼각형을 그립니다.
    deviceContext->DrawIndexed(indexCount, 0, 0);

    return;
}

Lightclass.h

이 예제에서는 LightClass를 수정하여 자기만의 뷰와 투영 행렬을 가지게 합니다.

////////////////////////////////////////////////////////////////////////////////
// Filename: lightclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _LIGHTCLASS_H_
#define _LIGHTCLASS_H_


//////////////
// INCLUDES //
//////////////
#include <d3dx10math.h>


////////////////////////////////////////////////////////////////////////////////
// Class name: LightClass
////////////////////////////////////////////////////////////////////////////////
class LightClass
{
public:
    LightClass();
    LightClass(const LightClass&);
    ~LightClass();

    void SetAmbientColor(float, float, float, float);
    void SetDiffuseColor(float, float, float, float);
    void SetPosition(float, float, float);
    void SetLookAt(float, float, float);

    D3DXVECTOR4 GetAmbientColor();
    D3DXVECTOR4 GetDiffuseColor();
    D3DXVECTOR3 GetPosition();

    void GenerateViewMatrix();
    void GenerateProjectionMatrix(float, float);

    void GetViewMatrix(D3DXMATRIX&);
    void GetProjectionMatrix(D3DXMATRIX&);

private:
    D3DXVECTOR4 m_ambientColor;
    D3DXVECTOR4 m_diffuseColor;
    D3DXVECTOR3 m_position;
    D3DXVECTOR3 m_lookAt;
    D3DXMATRIX m_viewMatrix;
    D3DXMATRIX m_projectionMatrix;
};

#endif

Lightclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: lightclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "lightclass.h"


LightClass::LightClass()
{
}


LightClass::LightClass(const LightClass& other)
{
}


LightClass::~LightClass()
{
}


void LightClass::SetAmbientColor(float red, float green, float blue, float alpha)
{
    m_ambientColor = D3DXVECTOR4(red, green, blue, alpha);
    return;
}


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


void LightClass::SetPosition(float x, float y, float z)
{
    m_position = D3DXVECTOR3(x, y, z);
    return;
}

SetLookAt 함수는 조명이 바라보는 방향으로 m_lookAt 벡터를 설정합니다. 이 벡터는 조명의 뷰 행렬을 만드는 데 사용됩니다.

void LightClass::SetLookAt(float x, float y, float z)
{
    m_lookAt.x = x;
    m_lookAt.y = y;
    m_lookAt.z = z;
    return;
}


D3DXVECTOR4 LightClass::GetAmbientColor()
{
    return m_ambientColor;
}


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


D3DXVECTOR3 LightClass::GetPosition()
{
    return m_position;
}

이 조명의 뷰 행렬은 위방향 벡터, lookAt벡터 그리고 조명의 위치를 이용하여 만듭니다.

void LightClass::GenerateViewMatrix()
{
    D3DXVECTOR3 up;


    // 위쪽을 가리키는 벡터를 설정합니다.
    up.x = 0.0f;
    up.y = 1.0f;
    up.z = 0.0f;

    // 세 벡터를 이용하여 뷰 행렬을 생성합니다.
    D3DXMatrixLookAtLH(&m_viewMatrix, &m_position, &m_lookAt, &up);
    
    return;
}

조명의 투영 행렬은 필드 오브 뷰, 뷰의 종횡비, 조명 범위의 near 및 far 평면을 이용하여 만들어집니다. 투영에 사용하는 조명의 종류는 일반적인 점조명이 아닌 사각형의 스폿라이트이지만 샘플링을 사각형 그림자 맵 텍스쳐에서 하기 때문에 이렇게 해야 합니다. 그런 이유로 사각 투영을 위하여 필드 오브 뷰와 종횡비가 필요합니다.

void LightClass::GenerateProjectionMatrix(float screenDepth, float screenNear)
{
    float fieldOfView, screenAspect;


    // 사각 조명에 쓰이는 필드 오브 뷰와 화면 종횡비를 설정합니다.
    fieldOfView = (float)D3DX_PI / 2.0f;
    screenAspect = 1.0f;

    // 조명의 투영 행렬을 생성합니다.
    D3DXMatrixPerspectiveFovLH(&m_projectionMatrix, fieldOfView, screenAspect, screenNear, screenDepth);

    return;
}

또한 뷰 및 투영 행렬을 반환하는 함수가 있습니다.

void LightClass::GetViewMatrix(D3DXMATRIX& viewMatrix)
{
    viewMatrix = m_viewMatrix;
    return;
}


void LightClass::GetProjectionMatrix(D3DXMATRIX& projectionMatrix)
{
    projectionMatrix = m_projectionMatrix;
    return;
}

Graphicsclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_


///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "d3dclass.h"
#include "cameraclass.h"
#include "modelclass.h"
#include "lightclass.h"

렌더 투 텍스쳐, 깊이 셰이더, 그림자 셰이더 클래스의 헤더들을 추가합니다.

#include "rendertextureclass.h"
#include "depthshaderclass.h"
#include "shadowshaderclass.h"


/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = true;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 100.0f;
const float SCREEN_NEAR = 1.0f;

그림자 맵을 손쉽게 수정하기 위한 define 구문을 추가합니다.

const int SHADOWMAP_WIDTH = 1024;
const int SHADOWMAP_HEIGHT = 1024;


////////////////////////////////////////////////////////////////////////////////
// Class name: GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
    GraphicsClass();
    GraphicsClass(const GraphicsClass&);
    ~GraphicsClass();

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

private:
    bool RenderSceneToTexture();
    bool Render();

private:
    D3DClass* m_D3D;
    CameraClass* m_Camera;

이 장면에는 육각형, 구, 바닥 모델을 사용할 것입니다.

    ModelClass *m_CubeModel, *m_GroundModel, *m_SphereModel;
    LightClass* m_Light;

렌더 투 텍스쳐, 깊이 셰이더, 그림자 셰이더 객체를 선언합니다.

    RenderTextureClass* m_RenderTexture;
    DepthShaderClass* m_DepthShader;
    ShadowShaderClass* m_ShadowShader;
};

#endif

Graphicsclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "graphicsclass.h"


GraphicsClass::GraphicsClass()
{
    m_D3D = 0;
    m_Camera = 0;
    m_CubeModel = 0;
    m_GroundModel = 0;
    m_SphereModel = 0;
    m_Light = 0;
    m_RenderTexture = 0;
    m_DepthShader = 0;  
    m_ShadowShader = 0;
}


GraphicsClass::GraphicsClass(const GraphicsClass& other)
{
}


GraphicsClass::~GraphicsClass()
{
}


bool GraphicsClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
    bool result;


    // Direct3D 객체를 생성합니다.
    m_D3D = new D3DClass;
    if(!m_D3D)
    {
        return false;
    }

    // Direct3D 객체를 초기화합니다.
    result = m_D3D->Initialize(screenWidth, screenHeight, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize Direct3D.", L"Error", MB_OK);
        return false;
    }

    // 카매라를 생성합니다.
    m_Camera = new CameraClass;
    if(!m_Camera)
    {
        return false;
    }

    // 카메라의 초기 위치를 설정합니다.
    m_Camera->SetPosition(0.0f, 0.0f, -10.0f);

육각형 모델을 설정합니다.

    // 육각형 모델을 생성합니다.
    m_CubeModel = new ModelClass;
    if(!m_CubeModel)
    {
        return false;
    }

    // 육각형 모델을 초기화합니다.
    result = m_CubeModel->Initialize(m_D3D->GetDevice(), "../Engine/data/cube.txt", L"../Engine/data/wall01.dds");
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize the cube model object.", L"Error", MB_OK);
        return false;
    }

    // 육각형 모델의 위치를 설정합니다.
    m_CubeModel->SetPosition(-2.0f, 2.0f, 0.0f);

구 모델을 설정합니다.

    // 구 모델을 생성합니다.
    m_SphereModel = new ModelClass;
    if(!m_SphereModel)
    {
        return false;
    }

    // 구 모델을 초기화합니다.
    result = m_SphereModel->Initialize(m_D3D->GetDevice(), "../Engine/data/sphere.txt", L"../Engine/data/ice.dds");
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize the sphere model object.", L"Error", MB_OK);
        return false;
    }

    // 구 모델의 위치를 설정합니다.
    m_SphereModel->SetPosition(2.0f, 2.0f, 0.0f);

바닥 모델을 설정합니다.

    // 바닥 모델을 생성합니다.
    m_GroundModel = new ModelClass;
    if(!m_GroundModel)
    {
        return false;
    }

    // 바닥 모델을 초기화합니다.
    result = m_GroundModel->Initialize(m_D3D->GetDevice(), "../Engine/data/plane01.txt", L"../Engine/data/metal001.dds");
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize the ground model object.", L"Error", MB_OK);
        return false;
    }

    // 바닥 모델의 위치를 설정합니다.
    m_GroundModel->SetPosition(0.0f, 1.0f, 0.0f);

    // 조명을 생성합니다.
    m_Light = new LightClass;
    if(!m_Light)
    {
        return false;
    }

    // 조명을 초기화합니다.
    m_Light->SetAmbientColor(0.15f, 0.15f, 0.15f, 1.0f);
    m_Light->SetDiffuseColor(1.0f, 1.0f, 1.0f, 1.0f);

조명의 lookAt을 설정하고 나면 조명에서 투영 행렬을 생성할 수 있습니다.

    m_Light->SetLookAt(0.0f, 0.0f, 0.0f);
    m_Light->GenerateProjectionMatrix(SCREEN_DEPTH, SCREEN_NEAR);

그림자 맵으로 사용할 렌더 투 텍스쳐를 생성합니다. 조명의 시점에서 장면의 깊이 버퍼가 이 렌더 투 텍스쳐 객체에 렌더링될 것입니다.

    // 렌더 투 텍스쳐를 생성합니다.
    m_RenderTexture = new RenderTextureClass;
    if(!m_RenderTexture)
    {
        return false;
    }

    // 렌더 투 텍스쳐를 초기화합니다.
    result = m_RenderTexture->Initialize(m_D3D->GetDevice(), SHADOWMAP_WIDTH, SHADOWMAP_HEIGHT, SCREEN_DEPTH, SCREEN_NEAR);
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize the render to texture object.", L"Error", MB_OK);
        return false;
    }

깊이 셰이더를 생성하고 초기화합니다.

    // 깊이 셰이더를 생성합니다.
    m_DepthShader = new DepthShaderClass;
    if(!m_DepthShader)
    {
        return false;
    }

    // 깊이 셰이더를 초기화합니다.
    result = m_DepthShader->Initialize(m_D3D->GetDevice(), hwnd);
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize the depth shader object.", L"Error", MB_OK);
        return false;
    }

그림자 셰이더를 생성하고 초기화합니다.

    // 그림자 셰이더를 생성합니다.
    m_ShadowShader = new ShadowShaderClass;
    if(!m_ShadowShader)
    {
        return false;
    }

    // 그림자 셰이더를 초기화합니다.
    result = m_ShadowShader->Initialize(m_D3D->GetDevice(), hwnd);
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize the shadow shader object.", L"Error", MB_OK);
        return false;
    }

    return true;
}


void GraphicsClass::Shutdown()
{
    // 그림자 셰이더를 해제합니다.
    if(m_ShadowShader)
    {
        m_ShadowShader->Shutdown();
        delete m_ShadowShader;
        m_ShadowShader = 0;
    }

    // 깊이 셰이더를 해제합니다.
    if(m_DepthShader)
    {
        m_DepthShader->Shutdown();
        delete m_DepthShader;
        m_DepthShader = 0;
    }

    // 렌더 투 텍스쳐를 해제합니다.
    if(m_RenderTexture)
    {
        m_RenderTexture->Shutdown();
        delete m_RenderTexture;
        m_RenderTexture = 0;
    }

    // 조명을 해제합니다.
    if(m_Light)
    {
        delete m_Light;
        m_Light = 0;
    }

    // 바닥 모델을 해제합니다.
    if(m_GroundModel)
    {
        m_GroundModel->Shutdown();
        delete m_GroundModel;
        m_GroundModel = 0;
    }

    // 구 모델을 해제합니다.
    if(m_SphereModel)
    {
        m_SphereModel->Shutdown();
        delete m_SphereModel;
        m_SphereModel = 0;
    }

    // 육각형 모델을 해제합니다.
    if(m_CubeModel)
    {
        m_CubeModel->Shutdown();
        delete m_CubeModel;
        m_CubeModel = 0;
    }

    // 카메라를 해제합니다.
    if(m_Camera)
    {
        delete m_Camera;
        m_Camera = 0;
    }

    // D3D 객체를 해제합니다.
    if(m_D3D)
    {
        m_D3D->Shutdown();
        delete m_D3D;
        m_D3D = 0;
    }

    return;
}


bool GraphicsClass::Frame(float posX, float posY, float posZ, float rotX, float rotY, float rotZ)
{
    bool result;
    static float lightPositionX = -5.0f;


    // 카메라의 위치를 설정합니다.
    m_Camera->SetPosition(posX, posY, posZ);
    m_Camera->SetRotation(rotX, rotY, rotZ);

프레임 함수 안에서 조명을 왼쪽에서 오른쪽으로 이동하여 조명의 위치에 따라 그림자가 움직이는 것을 볼 수 있도록 합니다.

    // 매 프레임마다 조명의 위치를 갱신합니다.
    lightPositionX += 0.05f;
    if(lightPositionX > 5.0f)
    {
        lightPositionX = -5.0f;
    }

    // 조명의 위치를 설정합니다.
    m_Light->SetPosition(lightPositionX, 8.0f, -5.0f);

    // 그래픽스 장면을 그립니다.
    result = Render();
    if(!result)
    {
        return false;
    }

    return true;
}

RenderSceneToTexture 함수는 프레임 렌더링의 시작부분에 불립니다. 조명의 시점에서 장면의 깊이 버퍼를 렌더 투 텍스쳐에 그리는데 이것이 바로 그림자 맵이 됩니다.

bool GraphicsClass::RenderSceneToTexture()
{
    D3DXMATRIX worldMatrix, lightViewMatrix, lightProjectionMatrix, translateMatrix;
    float posX, posY, posZ;
    bool result;

렌더링 타겟으로 렌더 투 텍스쳐를 설정합니다.

    // 렌더 투 텍스쳐를 렌더링 타겟이 되게 합니다.
    m_RenderTexture->SetRenderTarget(m_D3D->GetDeviceContext());

    // 렌더 투 텍스쳐의 내용을 지웁니다.
    m_RenderTexture->ClearRenderTarget(m_D3D->GetDeviceContext(), 0.0f, 0.0f, 0.0f, 1.0f);

조명의 뷰 행렬을 생성합니다.

    // 조명의 위치에서 조명의 뷰 행렬을 생성합니다.
    m_Light->GenerateViewMatrix();

    // d3d 객체에서 월드 행렬을 구합니다.
    m_D3D->GetWorldMatrix(worldMatrix);

    // 조명에서 뷰 및 직교 행렬을 구합니다.
    m_Light->GetViewMatrix(lightViewMatrix);
    m_Light->GetProjectionMatrix(lightProjectionMatrix);

깊이 셰이더와 조명의 뷰 및 투영 행렬을 이용하여 장면의 모든 물체들을 그립니다.

    // 육각형 모델의 이동 행렬을 설정합니다.
    m_CubeModel->GetPosition(posX, posY, posZ);
    D3DXMatrixTranslation(&worldMatrix, posX, posY, posZ);

    // 깊이 셰이더로 육각형 모델을 그립니다.
    m_CubeModel->Render(m_D3D->GetDeviceContext());
    result = m_DepthShader->Render(m_D3D->GetDeviceContext(), m_CubeModel->GetIndexCount(), worldMatrix, lightViewMatrix, lightProjectionMatrix);
    if(!result)
    {
        return false;
    }

    // 월드 행렬을 초기화합니다.
    m_D3D->GetWorldMatrix(worldMatrix);

    // 구 모델의 이동 행렬을 설정합니다.
    m_SphereModel->GetPosition(posX, posY, posZ);
    D3DXMatrixTranslation(&worldMatrix, posX, posY, posZ);

    // 깊이 셰이더로 구 모델을 그립니다.
    m_SphereModel->Render(m_D3D->GetDeviceContext());
    result = m_DepthShader->Render(m_D3D->GetDeviceContext(), m_SphereModel->GetIndexCount(), worldMatrix, lightViewMatrix, lightProjectionMatrix);
    if(!result)
    {
        return false;
    }

    // 월드 행렬을 초기화합니다.
    m_D3D->GetWorldMatrix(worldMatrix);

    // 바닥 모델의 이동 행렬을 설정합니다.
    m_GroundModel->GetPosition(posX, posY, posZ);
    D3DXMatrixTranslation(&worldMatrix, posX, posY, posZ);

    // 깊이 셰이더로 모델을 그립니다.
    m_GroundModel->Render(m_D3D->GetDeviceContext());
    result = m_DepthShader->Render(m_D3D->GetDeviceContext(), m_GroundModel->GetIndexCount(), worldMatrix, lightViewMatrix, lightProjectionMatrix);
    if(!result)
    {
        return false;
    }

렌더링 타겟을 원래대로 돌립니다.

    // 렌더링 타겟을 렌더 투 텍스쳐가 아닌 원래의 백버퍼로 돌립니다.
    m_D3D->SetBackBufferRenderTarget();

    // 뷰포트를 원래대로 돌립니다.
    m_D3D->ResetViewport();

    return true;
}


bool GraphicsClass::Render()
{
    D3DXMATRIX worldMatrix, viewMatrix, projectionMatrix, translateMatrix;
    D3DXMATRIX lightViewMatrix, lightProjectionMatrix;
    bool result;
    float posX, posY, posZ;

깊이 정보를 렌더 투 텍스쳐에 그려서 그림자 맵을 얻습니다.

    // 우선 텍스쳐에 장면을 그립니다.
    result = RenderSceneToTexture();
    if(!result)
    {
        return false;
    }

    // 버퍼의 내용을 지워 장면을 시작합니다.
    m_D3D->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

    // 카메라의 위치에서 뷰 행렬을 생성합니다.
    m_Camera->Render();

조명 뷰 행렬을 생성합니다.

    // 조명의 위치에서 조명 뷰 행렬을 생성합니다.
    m_Light->GenerateViewMatrix();

    // 카메라와 d3d 객체에서 월드, 뷰, 투영 행렬을 구합니다.
    m_Camera->GetViewMatrix(viewMatrix);
    m_D3D->GetWorldMatrix(worldMatrix);
    m_D3D->GetProjectionMatrix(projectionMatrix);

조명에 필요한 두 행렬을 구합니다.

    // 조명 객체에서 조명의 뷰 및 투영 행렬을 구합니다.
    m_Light->GetViewMatrix(lightViewMatrix);
    m_Light->GetProjectionMatrix(lightProjectionMatrix);

그림자 맵 셰이더와 조명 행렬, 그림자 맵 텍스쳐를 이용하여 각 모델들을 그립니다.

    // 육각형 모델의 이동 행렬을 설정합니다.
    m_CubeModel->GetPosition(posX, posY, posZ);
    D3DXMatrixTranslation(&worldMatrix, posX, posY, posZ);
    
    // 육각형 모델의 정점 및 인덱스 버퍼를 그래픽스 파이프라인에 넣어 그려질 수 있게 합니다.
    m_CubeModel->Render(m_D3D->GetDeviceContext());

    // 그림자 셰이더로 모델을 그립니다.
    result = m_ShadowShader->Render(m_D3D->GetDeviceContext(), m_CubeModel->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, lightViewMatrix, 
                    lightProjectionMatrix, m_CubeModel->GetTexture(), m_RenderTexture->GetShaderResourceView(), m_Light->GetPosition(),
                    m_Light->GetAmbientColor(), m_Light->GetDiffuseColor());
    if(!result)
    {
        return false;
    }

    // 월드 행렬을 초기화합니다.
    m_D3D->GetWorldMatrix(worldMatrix);

    // 구 모델의 이동 행렬을 설정합니다.
    m_SphereModel->GetPosition(posX, posY, posZ);
    D3DXMatrixTranslation(&worldMatrix, posX, posY, posZ);

    // 모델의 정점 및 인덱스 버퍼를 그래픽스 파이프라인에 넣어 그려질 수 있게 합니다.
    m_SphereModel->Render(m_D3D->GetDeviceContext());
    result = m_ShadowShader->Render(m_D3D->GetDeviceContext(), m_SphereModel->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, lightViewMatrix, 
                    lightProjectionMatrix, m_SphereModel->GetTexture(), m_RenderTexture->GetShaderResourceView(), m_Light->GetPosition(), 
                    m_Light->GetAmbientColor(), m_Light->GetDiffuseColor());
    if(!result)
    {
        return false;
    }

    // 월드 행렬을 초기화합니다.
    m_D3D->GetWorldMatrix(worldMatrix);

    // 바닥 모델의 이동 행렬을 설정합니다.
    m_GroundModel->GetPosition(posX, posY, posZ);
    D3DXMatrixTranslation(&worldMatrix, posX, posY, posZ);

    // 그림자 셰이더로 바닥 모델을 그립니다.
    m_GroundModel->Render(m_D3D->GetDeviceContext());
    result = m_ShadowShader->Render(m_D3D->GetDeviceContext(), m_GroundModel->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, lightViewMatrix,
                    lightProjectionMatrix, m_GroundModel->GetTexture(), m_RenderTexture->GetShaderResourceView(), m_Light->GetPosition(), 
                    m_Light->GetAmbientColor(), m_Light->GetDiffuseColor());
    if(!result)
    {
        return false;
    }

    // 그려진 장면을 화면에 표시합니다.
    m_D3D->EndScene();

    return true;
}

마치면서

이제 조명의 시점에서의 깊이 정보가 그려진 렌더 투 텍스쳐를 이용하여 그림자를 장면에 더할 수 있습니다. 다양한 입력에도 같은 방법을 사용하여 다른 셰이더 프로그램에도 그림자를 추가할 수 있습니다.

연습문제

  1. 코드를 컴파일하고 실행해 보십니오. 방향키, A, Z, Pageup, Pagedown 키를 사용하여 화면을 이동하고 그림자를 확인해 보십시오. ESC키로 종료합니다.

  2. 픽셀 셰이더의 bias를 0.0으로 하여 bias가 없을 때 어떤 모습인지 확인해 보십시오.

  3. bias를 0.3과 같은 큰 값으로 하여 그림자가 예측된 위치보다 얼마나 떨어져 있는지 확인해 보십시오.

  4. SHADOWMAP_WIDTHSHADOWMAP_HEIGHT 값을 256과 같은 작은 값으로 하여 낮은 해상도의 그림자 맵이 어떻게 보이는지 확인해 보십시오.

  5. 구 모델을 육각형에 더 가까이 두어 서로 그림자가 지는지 확인해 보십시오.

소스 코드

소스 코드 및 데이터 파일: dx11src40.zip

실행파일만: dx11exe40.zip

Nav