DirectX11 Tutorial 32: 유리, 얼음

강좌번역/DirectX 11 2017. 9. 23. 17:23 by 빠재

Tutorial 32: Glass and Ice

원문: http://rastertek.com/dx11tut32.html

이번 예제에서는 HLSL과 C++를 사용하여 유리 및 얼음 셰이더를 구현하는 방법을 살펴봅니다. 코드는 이전 튜토리얼에 기초해 있습니다.

유리와 얼음 셰이더는 둘 다 같은 방법으로 구현되어 있습니다. 유리나 얼음에서 빛이 어떻게 꺾이는지에 대해서는 노말맵을 사용했습니다. 노말맵의 각 픽셀들은 유리나 얼음 뒤에 있는 픽셀을 샘플링할 때 오프셋의 역할을 합니다. 이렇게 빛의 굴절처럼 보이는 효과를 만들어 냄으로 빛이 유리나 얼음을 통과하는 모습을 비슷하게 흉내내어 그 뒤에 있는 물체를 비춥니다. 이런 굴절 효과를 섭동이라고 부릅니다.

셰이더를 짤 때 유리와 얼음의 차이는 사소한 정도입니다. 유리와 얼음은 표면의 색상을 표현하기 위해 서로 다른 색상의 색상 텍스쳐를 사용할 것입니다. 노말맵 역시 서로 다른 표면의 재질감 때문에 다른 텍스쳐를 사용하겠지만, 다시 말하지만 셰이더 입장에서는 노말 벡터를 계산하기 위한 룩업 텍스쳐가 다른 것일 뿐입니다. 마지막 차이점은 어느 정도의 섭동이 사용되었는가 하는 것입니다. refractionScale이라는 변수를 이용하여 섭동의 크기를 조절합니다. 이 변수를 통해 유리에는 좀 더 적은 섭동이 발생하고 얼음에는 좀 더 강한 섭동을 시뮬레이션합니다.

여러분이 이전의 물 셰이더 예제를 읽고 이해하셨다면 이번 예제에 사용된 기법도 반사만 없을 뿐이지 물을 그리는 데 사용된 기법의 응용이라는 것을 깨달을 것입니다. 좀 더 실제적인 유리나 얼음 표면을 만들기 위해 매우 약한 정도의 반사를 추가해볼 수 있습니다.

이번 셰이더에서 사용하는 기본적인 알고리즘을 검토해 본 뒤 유리와 얼음 예제를 단계별로 보도록 하겠습니다.

셰이더 알고리즘

  • 1단계: 유리 뒤에 있는 씬을 텍스쳐에 그립니다. 이것을 굴절 텍스쳐라고 부릅니다.
  • 2단계: 유리 표면에 굴절 텍스쳐를 투영시킵니다.
  • 3단계: 유리를 투과하는 빛을 흉내내기 위하여 노말맵을 이용하여 굴절 텍스쳐의 좌표를 흔듭니다.
  • 4단계: 섭동이 끝난 굴절 텍스쳐와 유리의 색상 텍스쳐를 혼합하여 최종 결과물을 만들어 냅니다.

유리와 얼음이 각 단계별로 어떻게 구현되는지 살펴보겠습니다.

유리

우선 유리 뒤에 보이는 장면을 텍스쳐에 그려야 합니다. 그리고 나서 이 텍스쳐를 유리 표면에 투영시켜 단순히 삼각형 2개짜리 도형인 유리가 전체 씬을 투과해 볼 수 있는 것처럼 보일 것입니다. 여기에 사용한 렌더 투 텍스쳐와 텍스쳐 투영은 이전 튜토리얼에서 살펴본 바 있습니다.

예제를 단순화하기 위해 다양한 물체들로 이루어진 복잡한 장면 대신 텍스쳐가 그려진 사각형 하나를 씬으로 하겠습니다. 따라서 장면을 텍스쳐에 그리고 유리 모델에 투영한 것은 아래와 같은 굴절 텍스쳐를 만들어 냅니다:

씬이 좀 더 복잡하다면 유리창이 보이지 않게 되고 모든 것이 똑같이 보일 것입니다. 이렇게 되는 이유는 텍스쳐가 완벽하게 투영되면 단지 3D 장면의 부분을 똑같은 내용의 2D 텍스쳐로 덮은 것이기 때문에 유리가 완벽히 깨끗한 3D 장면을 보여주게 되어 기존 장면과 구분할 수 없게 됩니다. 최소한 무엇이 유리고 무엇이 장면인지 구분지을 수 있게 하려면 디버그 용도로 유리 텍스쳐를 좀 더 어둡거나 밝게 할 수 있습니다.

씬이 텍스쳐에 그려졌다면 노말맵을 이용하여 이 굴절 텍스쳐를 요동시켜 마치 장면이 유리 뒤에 있는 것처럼 보이게 합니다. 아래 노말맵을 사용하여 줄무늬 모양의 유리 효과를 만들 것입니다.

노말맵이 있으니 이것의 각 픽셀을 이용하여 굴절 텍스쳐의 어느 부분이 샘플링될지 수정할 수 있습니다. 이렇게 하여 굴절 텍스쳐의 샘플링 위치를 조금씩 위나 아래, 양옆으로 바꾸어 실제 유리처럼 빛이 직선으로 가지 않고 살짝 휘어지는 것처럼 보이게 합니다. 빛이 얼마나 꺾이는지는 refractionScale 변수로 조절하는데, 이 예제에서는 0.01정도로 매우 낮게 설정되었습니다. 참고로 노말맵의 모양에 따라 크기가 달라지기 때문에 이 변수의 값도 원하는 결과를 얻기 위해 노말맵에 따라 달라집니다.

이렇게 노말맵을 이용하여 0.01 스케일로 굴절 텍스쳐를 샘플링하면 아래와 같은 이미지를 얻을 수 있습니다:

기본적인 효과는 거의 다 나왔습니다. 하지만 대부분의 실제 유리는 살짝 색깔이 물들어 있고 어떤 경우는 문양이 있기도 합니다. 예제에 사용한 유리에는 아래 색상 텍스쳐를 사용할 것입니다:

색상 텍스쳐와 섭동이 적용된 굴절 텍스쳐를 혼합하여 아래와 같은 최종 유리 효과를 만들어 냅니다:


얼음

얼음은 셰이더에 입력만 다를 뿐 유리와 완전히 동일합니다.

시작은 똑같이 얼음 모델의 표면에 투영된 텍스쳐가 있는 사각형에서 출발합니다.

얼음은 최종 결과가 다르게 나오길 원하므로 다른 색상 텍스쳐를 사용할 것입니다:

노말맵 역시 얼음 표면 전체에 걸쳐있는 작은 굴곡들을 흉내내기 위해 다른 것을 사용합니다. 다행히도 색상 텍스쳐가 노말맵으로 쓰기에 적당한 노이즈가 있습니다. 이 텍스쳐를 포토샵에서 5배수로 Nvidia 노말맵 필터를 적용시켜 다음과 같은 노말맵을 얻습니다:

이 노말맵에 refractionScale 변수를 0.1과 같이 큰 값을 주면(유리에 적용한 0.01 대신) 섭동이 강하게 적용된 굴절 이미지를 얻습니다:

마지막으로 섭동이 적용된 굴절 이미지와 얼음 색상 텍스쳐를 혼합하면 아래와 같은 매우 사실적인 이미지를 얻을 수 있습니다:

코드를 살펴보기 전에 마지막으로 한마디 하자면 움직이는 배경 위에 구현한 셰이더를 보게 되면(유리나 얼음 뒤에 회전하는 육면체와 같이) 매우 사실적으로 보입니다. 무슨 뜻인지 확인하기 위해 이 예제를 한번이라도 실행해 보기를 권합니다.

프레임워크

이번 예제의 프레임워크는 이전 튜토리얼과 비슷합니다. 얼음과 유리 셰이딩을 위한 GlassShaderClass가 추가됩니다. 3D 씬을 텍스쳐에 그리기 위해 RenderTextureClass가 예제에서 사용됩니다. 또한 유리 뒤에서 배경을 사용할 회전하는 육면체를 그리기 위해 TextureShaderClass 역시 사용할 것입니다.

우선 유리 셰이더에 사용할 HLSL 코드를 보면서 시작하도록 하겠습니다.

Glass.vs

////////////////////////////////////////////////////////////////////////////////
// Filename: glass.vs
////////////////////////////////////////////////////////////////////////////////


/////////////
// GLOBALS //
/////////////
cbuffer MatrixBuffer
{
    matrix worldMatrix;
    matrix viewMatrix;
    matrix projectionMatrix;
};


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

PixelInputType 구조체에 refractionPosition 변수를 추가하여 픽셀 셰이더에 굴절 정점 정보가 전달되도록 합니다.

struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
    float4 refractionPosition : TEXCOORD1;
};


////////////////////////////////////////////////////////////////////////////////
// Vertex Shader
////////////////////////////////////////////////////////////////////////////////
PixelInputType GlassVertexShader(VertexInputType input)
{
    PixelInputType output;
    matrix viewProjectWorld;


    // Change the position vector to be 4 units for proper matrix calculations.
    input.position.w = 1.0f;

    // Calculate the position of the vertex against the world, view, and projection matrices.
    output.position = mul(input.position, worldMatrix);
    output.position = mul(output.position, viewMatrix);
    output.position = mul(output.position, projectionMatrix);
    
    // Store the texture coordinates for the pixel shader.
    output.tex = input.tex;

입력된 정점 좌표를 투영된 좌표로 변환해 줄 행렬을 맨듭니다.

    // Create the view projection world matrix for refraction.
    viewProjectWorld = mul(viewMatrix, projectionMatrix);
    viewProjectWorld = mul(worldMatrix, viewProjectWorld);

임력된 정점 좌표를 투영된 좌표로 변환하고 픽셀 셰이더로 전달합니다.

    // Calculate the input position against the viewProjectWorld matrix.
    output.refractionPosition = mul(input.position, viewProjectWorld);

    return output;
}

Glass.ps

////////////////////////////////////////////////////////////////////////////////
// Filename: glass.ps
////////////////////////////////////////////////////////////////////////////////


/////////////
// GLOBALS //
/////////////
SamplerState SampleType;

유리 셰이더는 세 가지의 텍스쳐를 사용합니다. 유리의 기본 표면 색상으로 colorTexxture을 사용합니다. normalTexture은 모든 법선 정보를 가지고 있는 룩업 테이블로 사용됩니다. 마지막으로 refractionTexture은 유리 뒤에 있는 씬이 2D로 렌더링된 내용이 담겨 있습니다. register를 바로 사용하여 할당하는 것을 눈치채셨을 것입니다. 이를 통해 HLSL파일에 배치된 순서에 따라 어느 레지스터에 할당되는지 자동으로 결정하지 않고 직접 할당할 수 있게 됩니다.

Texture2D colorTexture : register(t0);
Texture2D normalTexture : register(t1);
Texture2D refractionTexture : register(t2);

refactionScale의 값을 설정하기 위해 GlassBuffer를 사용합니다. refractionScale 변수는 섭동을 어느 정도의 크기로 굴절 텍스쳐에 적용할지 조절하는 데 사용됩니다. 일반적으로 유리에는 낮은 값을, 얼음에는 큰 값을 사용합니다.

cbuffer GlassBuffer
{
    float refractionScale;
    float3 padding;
};


//////////////
// TYPEDEFS //
//////////////
struct PixelInputType
{
    float4 position : SV_POSITION;
    float2 tex : TEXCOORD0;
    float4 refractionPosition : TEXCOORD1;
};


////////////////////////////////////////////////////////////////////////////////
// Pixel Shader
////////////////////////////////////////////////////////////////////////////////
float4 GlassPixelShader(PixelInputType input) : SV_TARGET
{
    float2 refractTexCoord;
    float4 normalMap;
    float3 normal;
    float4 refractionColor;
    float4 textureColor;
    float4 color;

우선 (-1, +1)의 대칭 좌표를 (0, 1)의 텍스쳐 좌표로 변환합니다.

    // Calculate the projected refraction texture coordinates.
    refractTexCoord.x = input.refractionPosition.x / input.refractionPosition.w / 2.0f + 0.5f;
    refractTexCoord.y = -input.refractionPosition.y / input.refractionPosition.w / 2.0f + 0.5f;

그 다음 노말맵을 샘플링하여 (0, 1)의 텍스쳐 좌표에서 (-1, +1) 좌표로 변환합니다.

    // Sample the normal from the normal map texture.
    normalMap = normalTexture.Sample(SampleType, input.tex);

    // Expand the range of the normal from (0,1) to (-1,+1).
    normal = (normalMap.xyz * 2.0f) - 1.0f;

방금 계산한 노말 값을 이용하여 텍스쳐를 샘플링할 좌표를 흩뜨려 놓습니다. 또한 굴절 크기 변수를 노말 값에 곱하여 섭동의 크기를 늘리거나 줄일 수 있게 합니다.

    // Re-position the texture coordinate sampling position by the normal map value to simulate light distortion through glass.
    refractTexCoord = refractTexCoord + (normal.xy * refractionScale);

그 다음 섭동이 적용된 좌표를 이용하여 굴절 텍스쳐에서 샘플링하고 노말맵에 사용했던 같은 좌표를 이용하여 색상 텍스쳐를 샘플링합니다.

    // Sample the texture pixel from the refraction texture using the perturbed texture coordinates.
    refractionColor = refractionTexture.Sample(SampleType, refractTexCoord);

    // Sample the texture pixel from the glass color texture.
    textureColor = colorTexture.Sample(SampleType, input.tex);

마지막으로 굴절 텍스쳐와 색상 텍스쳐를 혼합하여 최종 결과물을 냅니다.

    // Evenly combine the glass color and refraction value for the final color.
    color = lerp(refractionColor, textureColor, 0.5f);

    return color;
}

Glassshaderclass.h

GlassShaderClass의 코드는 TextureShaderClass에 기반해 있고 유리 셰이딩을 위한 수정이 조금 있습니다.

////////////////////////////////////////////////////////////////////////////////
// Filename: glassshaderclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GLASSSHADERCLASS_H_
#define _GLASSSHADERCLASS_H_


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


////////////////////////////////////////////////////////////////////////////////
// Class name: GlassShaderClass
////////////////////////////////////////////////////////////////////////////////
class GlassShaderClass
{
private:
    struct MatrixBufferType
    {
        D3DXMATRIX world;
        D3DXMATRIX view;
        D3DXMATRIX projection;
    };

픽셀 셰이더에 있는 굴절 크기를 위한 상수 버퍼 구조체를 선언합니다.

    struct GlassBufferType
    {
        float refractionScale;
        D3DXVECTOR3 padding;
    };


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

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

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

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

private:
    ID3D11VertexShader* m_vertexShader;
    ID3D11PixelShader* m_pixelShader;
    ID3D11InputLayout* m_layout;
    ID3D11SamplerState* m_sampleState;
    ID3D11Buffer* m_matrixBuffer;

m_glassBuffer 변수로 유리 셰이더에 필요한 굴절 크기 값을 전달할 수 있게 합니다.

    ID3D11Buffer* m_glassBuffer;
};

#endif

Glassshaderclass.cpp

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


GlassShaderClass::GlassShaderClass()
{
    m_vertexShader = 0;
    m_pixelShader = 0;
    m_layout = 0;
    m_sampleState = 0;
    m_matrixBuffer = 0;

생성자에서 유리 버퍼 변수를 null로 초기화합니다.

    m_glassBuffer = 0;
}


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


GlassShaderClass::~GlassShaderClass()
{
}


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

glass.vsglass.ps HLSL 파일로 유리 셰이더를 초기화합니다.

    // Initialize the vertex and pixel shaders.
    result = InitializeShader(device, hwnd, L"../Engine/glass.vs", L"../Engine/glass.ps");
    if(!result)
    {
        return false;
    }

    return true;
}


void GlassShaderClass::Shutdown()
{
    // Shutdown the vertex and pixel shaders as well as the related objects.
    ShutdownShader();

    return;
}

Render 함수는 색상 텍스쳐, 노말맵 텍스쳐, 굴절 텍스쳐, 굴절 크기 값을 인자로 받습니다. 이 값들은 RenderShader함수로 실제 렌더링이 일어나기 전에 SetShaderParameters 함수를 이용하여 세팅됩니다.

bool GlassShaderClass::Render(ID3D11DeviceContext* deviceContext, int indexCount, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, 
                  D3DXMATRIX projectionMatrix, ID3D11ShaderResourceView* colorTexture, 
                  ID3D11ShaderResourceView* normalTexture, ID3D11ShaderResourceView* refractionTexture, 
                  float refractionScale)
{
    bool result;


    // Set the shader parameters that it will use for rendering.
    result = SetShaderParameters(deviceContext, worldMatrix, viewMatrix, projectionMatrix, colorTexture, 
                     normalTexture, refractionTexture, refractionScale);
    if(!result)
    {
        return false;
    }

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

    return true;
}


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


    // Initialize the pointers this function will use to null.
    errorMessage = 0;
    vertexShaderBuffer = 0;
    pixelShaderBuffer = 0;

유리 정점 셰이더를 불러옵니다.

    // Compile the vertex shader code.
    result = D3DX11CompileFromFile(vsFilename, NULL, NULL, "GlassVertexShader", "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;
    }

유리 픽셀 셰이더를 불러옵니다.

    // Compile the pixel shader code.
    result = D3DX11CompileFromFile(psFilename, NULL, NULL, "GlassPixelShader", "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;
    }

    // Create the vertex shader from the buffer.
    result = device->CreateVertexShader(vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), NULL, 
                        &m_vertexShader);
    if(FAILED(result))
    {
        return false;
    }

    // Create the vertex shader from the buffer.
    result = device->CreatePixelShader(pixelShaderBuffer->GetBufferPointer(), pixelShaderBuffer->GetBufferSize(), NULL, 
                       &m_pixelShader);
    if(FAILED(result))
    {
        return false;
    }

    // Create the vertex input layout description.
    // This setup needs to match the VertexType stucture in the ModelClass and in the shader.
    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;

    // Get a count of the elements in the layout.
    numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]);

    // Create the vertex input layout.
    result = device->CreateInputLayout(polygonLayout, numElements, vertexShaderBuffer->GetBufferPointer(), 
                       vertexShaderBuffer->GetBufferSize(), &m_layout);
    if(FAILED(result))
    {
        return false;
    }

    // Release the vertex shader buffer and pixel shader buffer since they are no longer needed.
    vertexShaderBuffer->Release();
    vertexShaderBuffer = 0;

    pixelShaderBuffer->Release();
    pixelShaderBuffer = 0;

    // Create a texture sampler state description.
    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;

    // Create the texture sampler state.
    result = device->CreateSamplerState(&samplerDesc, &m_sampleState);
    if(FAILED(result))
    {
        return false;
    }

    // Setup the description of the matrix dynamic constant buffer that is in the vertex shader.
    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;

    // Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class.
    result = device->CreateBuffer(&matrixBufferDesc, NULL, &m_matrixBuffer);
    if(FAILED(result))
    {
        return false;
    }

픽셀 셰이더에 굴절 크기를 설정하기 위한 디스크립션 객체를 설정하고 유리 상수 버퍼를 생성합니다.

    // Setup the description of the glass dynamic constant buffer that is in the pixel shader.
    glassBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
    glassBufferDesc.ByteWidth = sizeof(GlassBufferType);
    glassBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    glassBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
    glassBufferDesc.MiscFlags = 0;
    glassBufferDesc.StructureByteStride = 0;

    // Create the constant buffer pointer so we can access the pixel shader constant buffer from within this class.
    result = device->CreateBuffer(&glassBufferDesc, NULL, &m_glassBuffer);
    if(FAILED(result))
    {
        return false;
    }

    return true;
}


void GlassShaderClass::ShutdownShader()
{

ShutdownShader 함수에서 유리 버퍼를 해제합니다.

    // Release the glass constant buffer.
    if(m_glassBuffer)
    {
        m_glassBuffer->Release();
        m_glassBuffer = 0;
    }

    // Release the matrix constant buffer.
    if(m_matrixBuffer)
    {
        m_matrixBuffer->Release();
        m_matrixBuffer = 0;
    }

    // Release the sampler state.
    if(m_sampleState)
    {
        m_sampleState->Release();
        m_sampleState = 0;
    }

    // Release the layout.
    if(m_layout)
    {
        m_layout->Release();
        m_layout = 0;
    }

    // Release the pixel shader.
    if(m_pixelShader)
    {
        m_pixelShader->Release();
        m_pixelShader = 0;
    }

    // Release the vertex shader.
    if(m_vertexShader)
    {
        m_vertexShader->Release();
        m_vertexShader = 0;
    }

    return;
}


void GlassShaderClass::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 GlassShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, 
                       D3DXMATRIX projectionMatrix, ID3D11ShaderResourceView* colorTexture, 
                       ID3D11ShaderResourceView* normalTexture, ID3D11ShaderResourceView* refractionTexture,
                       float refractionScale)
{
    HRESULT result;
    D3D11_MAPPED_SUBRESOURCE mappedResource;
    MatrixBufferType* dataPtr;
    GlassBufferType* dataPtr2;
    unsigned int bufferNumber;


    // Transpose the matrices to prepare them for the shader.
    D3DXMatrixTranspose(&worldMatrix, &worldMatrix);
    D3DXMatrixTranspose(&viewMatrix, &viewMatrix);
    D3DXMatrixTranspose(&projectionMatrix, &projectionMatrix);

    // Lock the matrix constant buffer so it can be written to.
    result = deviceContext->Map(m_matrixBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
    if(FAILED(result))
    {
        return false;
    }

    // Get a pointer to the data in the matrix constant buffer.
    dataPtr = (MatrixBufferType*)mappedResource.pData;

    // Copy the matrices into the matrix constant buffer.
    dataPtr->world = worldMatrix;
    dataPtr->view = viewMatrix;
    dataPtr->projection = projectionMatrix;

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

    // Set the position of the matrix constant buffer in the vertex shader.
    bufferNumber = 0;

    // Now set the matrix constant buffer in the vertex shader with the updated values.
    deviceContext->VSSetConstantBuffers(bufferNumber, 1, &m_matrixBuffer);

픽셀 셰이더에서 사용하는 색상, 노말맵, 굴절 텍스쳐가 여기서 설정됩니다.

    // Set the three shader texture resources in the pixel shader.
    deviceContext->PSSetShaderResources(0, 1, &colorTexture);
    deviceContext->PSSetShaderResources(1, 1, &normalTexture);
    deviceContext->PSSetShaderResources(2, 1, &refractionTexture);

유리 버퍼에 락을 걸고 refractionScale 값을 버퍼에 복사한 뒤 픽셀 셰이더에 설정합니다.

    // Lock the glass constant buffer so it can be written to.
    result = deviceContext->Map(m_glassBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
    if(FAILED(result))
    {
        return false;
    }

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

    // Copy the variables into the glass constant buffer.
    dataPtr2->refractionScale = refractionScale;
    dataPtr2->padding = D3DXVECTOR3(0.0f, 0.0f, 0.0f);

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

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

    // Now set the glass constant buffer in the pixel shader with the updated values.
    deviceContext->PSSetConstantBuffers(bufferNumber, 1, &m_glassBuffer);

    return true;
}


void GlassShaderClass::RenderShader(ID3D11DeviceContext* deviceContext, int indexCount)
{
    // Set the vertex input layout.
    deviceContext->IASetInputLayout(m_layout);

    // Set the vertex and pixel shaders that will be used to render this triangle.
    deviceContext->VSSetShader(m_vertexShader, NULL, 0);
    deviceContext->PSSetShader(m_pixelShader, NULL, 0);

    // Set the sampler state in the pixel shader.
    deviceContext->PSSetSamplers(0, 1, &m_sampleState);

    // Render the triangle.
    deviceContext->DrawIndexed(indexCount, 0, 0);

    return;
}

Graphicsclass.h

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


/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = true;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;


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

GlassShaderClass를 include합니다.

#include "glassshaderclass.h"


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

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

private:
    bool RenderToTexture(float);
    bool Render(float);

private:
    D3DClass* m_D3D;
    CameraClass* m_Camera;

회전하는 육면체와 유리창 모델을 선언합니다.

    ModelClass* m_Model;
    ModelClass* m_WindowModel;

회전하는 육면체 부분을 그려낼 렌더 투 텍스쳐 객체를 선언합니다.

    RenderTextureClass* m_RenderTexture;

m_TextureShader은 배경을 그리는 데 사용합니다. m_GlassShader은 유리창 모델을 그리는 데 사용됩니다.

    TextureShaderClass* m_TextureShader;
    GlassShaderClass* m_GlassShader;
};

#endif

Graphicsclass.cpp

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


GraphicsClass::GraphicsClass()
{
    m_D3D = 0;
    m_Camera = 0;
    m_Model = 0;
    m_WindowModel = 0;
    m_RenderTexture = 0;
    m_TextureShader = 0;

생성자에서 GlassShaderClass 객체를 null로 초기화합니다.

    m_GlassShader = 0;
}


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


GraphicsClass::~GraphicsClass()
{
}


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

        
    // Create the Direct3D object.
    m_D3D = new D3DClass;
    if(!m_D3D)
    {
        return false;
    }

    // Initialize the Direct3D object.
    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;
    }

    // Create the camera object.
    m_Camera = new CameraClass;
    if(!m_Camera)
    {
        return false;
    }

유리창 뒤에서 빙글빙글 도는 육면체 모델을 생성합니다. Initialize함수 호출시 노말맵도 지정할 수 있지만 사용하지 않을 것이기 때문에 마지막 인자는 무시해도 됩니다. 노말맵 인자가 있는 것은 함수를 일반화한 결과일 뿐입니다.

    // Create the model object.
    m_Model = new ModelClass;
    if(!m_Model)
    {
        return false;
    }

    // Initialize the model object.
    result = m_Model->Initialize(m_D3D->GetDevice(), "../Engine/data/cube.txt", L"../Engine/data/seafloor.dds", L"../Engine/data/bump03.dds");
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize the model object.", L"Error", MB_OK);
        return false;
    }

유리창 모델을 생성합니다. 사각형을 구성하는 데에는 두개의 삼각형만으로 충분하기 때문에 사각형 .obj 파일을 사용합니다. 또한 glass01.dds 텍스쳐를 유리 색상으로 사용하고 bump03.dds 파일을 노말맵으로 사용하여 유리 굴절 효과를 만듭니다.

    // Create the window model object.
    m_WindowModel = new ModelClass;
    if(!m_WindowModel)
    {
        return false;
    }

    // Initialize the window model object.
    result = m_WindowModel->Initialize(m_D3D->GetDevice(), "../Engine/data/square.txt", L"../Engine/data/glass01.dds", L"../Engine/data/bump03.dds");
    if(!result)
    {
        MessageBox(hwnd, L"Could not initialize the window model object.", L"Error", MB_OK);
        return false;
    }

렌더 투 텍스쳐 객체에 굴절시킬 전체 장면을 그리고 유리 셰이더에 입력으로 전달합니다.

    // Create the render to texture object.
    m_RenderTexture = new RenderTextureClass;
    if(!m_RenderTexture)
    {
        return false;
    }

    // Initialize the render to texture object.
    result = m_RenderTexture->Initialize(m_D3D->GetDevice(), screenWidth, screenHeight);
    if(!result)
    {
        return false;
    }

회전하는 육면체를 그릴 텍스쳐 셰이더를 생성합니다.

    // Create the texture shader object.
    m_TextureShader = new TextureShaderClass;
    if(!m_TextureShader)
    {
        return false;
    }

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

여기서 유리 셰이더를 생성하고 초기화합니다.

    // Create the glass shader object.
    m_GlassShader = new GlassShaderClass;
    if(!m_GlassShader)
    {
        return false;
    }

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

    return true;
}


void GraphicsClass::Shutdown()
{

Shutdown 함수에서 유리 셰이더 객체를 해제합니다.

    // Release the glass shader object.
    if(m_GlassShader)
    {
        m_GlassShader->Shutdown();
        delete m_GlassShader;
        m_GlassShader = 0;
    }

    // Release the texture shader object.
    if(m_TextureShader)
    {
        m_TextureShader->Shutdown();
        delete m_TextureShader;
        m_TextureShader = 0;
    }

    // Release the render to texture object.
    if(m_RenderTexture)
    {
        m_RenderTexture->Shutdown();
        delete m_RenderTexture;
        m_RenderTexture = 0;
    }

    // Release the window model object.
    if(m_WindowModel)
    {
        m_WindowModel->Shutdown();
        delete m_WindowModel;
        m_WindowModel = 0;
    }

    // Release the model object.
    if(m_Model)
    {
        m_Model->Shutdown();
        delete m_Model;
        m_Model = 0;
    }

    // Release the camera object.
    if(m_Camera)
    {
        delete m_Camera;
        m_Camera = 0;
    }

    // Release the Direct3D object.
    if(m_D3D)
    {
        m_D3D->Shutdown();
        delete m_D3D;
        m_D3D = 0;
    }

    return;
}


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

매 프레임마다 육면체의 회전을 갱신하고 그 값을 RenderToTexture함수와 Render 함수에 전달하여 그 값을 동기화합니다.

    // Update the rotation variable each frame.
    rotation += (float)D3DX_PI * 0.005f;
    if(rotation > 360.0f)
    {
        rotation -= 360.0f;
    }

카메라의 위치 역시 설정합니다.

    // Set the position of the camera.
    m_Camera->SetPosition(0.0f, 0.0f, -10.0f);

우선 유리 셰이더에 굴절 텍스쳐로 전달할 3D 씬을 텍스쳐에 렌더링합니다.

    // Render the scene to texture first.
    result = RenderToTexture(rotation);
    if(!result)
    {
        return false;
    }

그리고 나서 정상적으로 화면을 다시 그립니다. 유리 셰이더로 섭동이 적용되고 색상이 입혀진 유리를 맨 위에 그립니다.

    // Render the scene.
    result = Render(rotation);
    if(!result)
    {
        return false;
    }

    return true;
}

RenderToTexture 함수는 회전하는 육면체를 텍스쳐에 그립니다.

bool GraphicsClass::RenderToTexture(float rotation)
{
    D3DXMATRIX worldMatrix, viewMatrix, projectionMatrix;
    bool result;


    // Set the render target to be the render to texture.
    m_RenderTexture->SetRenderTarget(m_D3D->GetDeviceContext(), m_D3D->GetDepthStencilView());

    // Clear the render to texture.
    m_RenderTexture->ClearRenderTarget(m_D3D->GetDeviceContext(), m_D3D->GetDepthStencilView(), 0.0f, 0.0f, 0.0f, 1.0f);

    // Generate the view matrix based on the camera's position.
    m_Camera->Render();

    // Get the world, view, and projection matrices from the camera and d3d objects.
    m_D3D->GetWorldMatrix(worldMatrix);
    m_Camera->GetViewMatrix(viewMatrix);
    m_D3D->GetProjectionMatrix(projectionMatrix);

    // Multiply the world matrix by the rotation.
    D3DXMatrixRotationY(&worldMatrix, rotation);

    // Put the cube model vertex and index buffers on the graphics pipeline to prepare them for drawing.
    m_Model->Render(m_D3D->GetDeviceContext());

    // Render the cube model using the texture shader.
    result = m_TextureShader->Render(m_D3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, m_Model->GetTexture());
    if(!result)
    {
        return false;
    }

    // Reset the render target back to the original back buffer and not the render to texture anymore.
    m_D3D->SetBackBufferRenderTarget();

    return true;
}


bool GraphicsClass::Render(float rotation)
{
    D3DXMATRIX worldMatrix, viewMatrix, projectionMatrix;
    float refractionScale;
    bool result;

우선 굴절 크기 변수를 설정하여 섭동이 어느 정도나 일어날 지 수정합니다.

    // Set the refraction scale for the glass shader.
    refractionScale = 0.01f;

    // Clear the buffers to begin the scene.
    m_D3D->BeginScene(0.0f, 0.0f, 0.0f, 1.0f);

    // Generate the view matrix based on the camera's position.
    m_Camera->Render();

    // Get the world, view, and projection matrices from the camera and d3d objects.
    m_D3D->GetWorldMatrix(worldMatrix);
    m_Camera->GetViewMatrix(viewMatrix);
    m_D3D->GetProjectionMatrix(projectionMatrix);

회전하는 육면체를 평소대로 그립니다.

    // Multiply the world matrix by the rotation.
    D3DXMatrixRotationY(&worldMatrix, rotation);

    // Put the cube model vertex and index buffers on the graphics pipeline to prepare them for drawing.
    m_Model->Render(m_D3D->GetDeviceContext());

    // Render the cube model using the texture shader.
    result = m_TextureShader->Render(m_D3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, 
                     m_Model->GetTexture());
    if(!result)
    {
        return false;
    }

    // Reset the world matrix.
    m_D3D->GetWorldMatrix(worldMatrix);

색상 텍스쳐, 노말맵, 굴절 텍스쳐, 굴절 크기값을 인자로 받은 유리 셰이더를 이용하여 유리창 모델을 그립니다.

    // Translate to back where the window model will be rendered.
    D3DXMatrixTranslation(&worldMatrix, 0.0f, 0.0f, -1.5f);

    // Put the window model vertex and index buffers on the graphics pipeline to prepare them for drawing.
    m_WindowModel->Render(m_D3D->GetDeviceContext());

    // Render the window model using the glass shader.
    result = m_GlassShader->Render(m_D3D->GetDeviceContext(), m_WindowModel->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, 
                       m_WindowModel->GetTexture(), m_WindowModel->GetNormalMap(), m_RenderTexture->GetShaderResourceView(), 
                       refractionScale);
    if(!result)
    {
        return false;
    }

    // Present the rendered scene to the screen.
    m_D3D->EndScene();

    return true;
}

정리하며

굴절 및 노말맵으로 섭동을 적용하여 유리와 얼음 효과를 렌더링할 수 있습니다.

연습 문제

  1. 프로그램을 다시 컴파일하여 실행해 보십시오. 녹색 유리 뒤로 회전하는 육면체가 보일 것입니다. ESC키로 종료합니다.

  2. 얼음 효과로 바꾸기 위해 GraphicsClass::Initialize 함수의 아래 부분을 바꾸어 보십시오.

    result = m_WindowModel->Initialize(m_D3D->GetDevice(), "../Engine/data/square.txt", L"../Engine/data/glass01.dds", L"../Engine/data/bump03.dds");
    

    에서

    result = m_WindowModel->Initialize(m_D3D->GetDevice(), "../Engine/data/square.txt", L"../Engine/data/ice01.dds", L"../Engine/data/icebump01.dds");
    

    그리고 refractionScale 값을 0.1로 하고 카메라를 더 가까이 합니다.

    m_Camera->SetPosition(0.0f, 0.0f, -5.0f);
    refractionScale = 0.1f;
    
  3. refractionScale 값을 바꾸어 섭동에 어떤 영향을 주는지 살펴보십시오.

  4. 픽셀 셰이더에서 색상 텍스쳐와 섭동이 적용된 굴절 텍스쳐의 혼합을 바꾸어 다른 결과를 만들어 보십시오.

  5. 여러분만의 유리 색상 텍스쳐와 노말맵을 이용하여 여러분만의 유리 효과를 만들어 보십시오(또한 refractionScale 값을 조절하여 노말맵에 적절하게 보이도록 해 보시기 바랍니다).

소스 코드

Visual Studio 2008 프로젝트: dx11tut32.zip

소스코드만: dx11src32.zip

실행파일만: dx11exe32.zip

'강좌번역 > DirectX 11' 카테고리의 다른 글

DirectX11 Tutorial 34: 빌보드  (3) 2017.11.30
DirectX11 Tutorial 33: 불꽃  (0) 2017.10.25
DirectX11 Tutorial 31: 3차원 음향  (0) 2017.09.08
DirectX11 Tutorial 30: 다중 점조명  (0) 2017.09.03
DirectX11 Tutorial 29: 물  (2) 2017.09.01
Nav