ShadowMapping 다이렉트X

다이렉트X 프로그램에서 실행된 화면이다.

렌더몽키에서 할일

다이렉트에서 코딩하기 전에 렌더몽키에서 할 작업이 있다.
모델 저장, 셰이더 파일 수정이다.

모델 저장

원환체 모델 torus.x와 평면 모델 disc.x을 저장한다.

 

2개의 fx 파일 저장

ShadowMapping.rfx 파일을 CreateShadow.rfx와 ApplyShadowDisc.rfx로 두 번 저장한다.

CreateShadow 수정

ApplyShadowTorus, ApplyShadowDisc 두 개의 패스를 삭제한다.

CreateShadow의 정점 셰이더를 오픈한다.

정점 셰이더의 광원-뷰행렬을 삭제한다.
다이렉트 X 프레임워크에서 광원-뷰행렬을 직접 넘길 것이다.

gWorldLightPos, gViewProjMat 전역 변수는 CreateShadow 패스에서 사용되지 않는다.
렌더몽키에서 삭제해준다.

//삭제 되는 광원-뷰행렬
float3 dirZ = -normalize( gWorldLightPos);
 float3 up = float3( 0, 1, 0);
 float3 dirX = cross( up, dirZ);
 float3 dirY = cross( dirZ, dirX);

 lightViewMat = float4x4(
    float4( dirX, -dot( gWorldLightPos.xyz, dirX)),
    float4( dirY, -dot( gWorldLightPos.xyz, dirY)),
    float4( dirZ, -dot( gWorldLightPos.xyz, dirZ)),
    float4( 0, 0, 0, 1));   
 
 lightViewMat = transpose( lightViewMat);

 

삭제된 후의 정점 셰이더 코드이다.

VS_OUTPUT vs_main( VS_INPUT input)
{
   VS_OUTPUT output;
   
   float4x4 lightViewMat = gLightViewMat;  
      
   output.pos = mul( input.pos, gWorldMat);
   output.pos = mul( output.pos, lightViewMat);
   output.pos = mul( output.pos, gLightProjMat);
   
   output.clipPos = output.pos;
      
   return output;
}

ApplyShadow 수정

CreateShadow, ApplyShadowDisc 두 개의 패스를 삭제한다.
ApplyShadowTorus 패스의 정점 셰이더를 오픈한다.

여기에서도 정점 셰이더의 광원-뷰행렬을 삭제한다.
다이렉트 X 프레임워크에서 광원-뷰행렬을 직접 넘길 것이다.

VS_OUTPUT vs_main( VS_INPUT input)
{
   VS_OUTPUT output;
   
   float4x4 lightViewMat = gLightViewMat;  
   
   //render world-view-projection position
   float4 worldPos = mul( input.pos, gWorldMat);
   output.pos = mul( worldPos, gViewProjMat);
   
   //light world-view-projection position
   output.clipPos = mul( worldPos, lightViewMat);
   output.clipPos = mul( output.clipPos, gLightProjMat);
   
   float3 lightDir = normalize( worldPos.xyz - gWorldLightPos.xyz);
   float3 worldNormal = normalize( mul( input.normal, (float3x3)gWorldMat));
   
   output.diffuse = dot(-lightDir, worldNormal);
   
   return output;
}

프레임워크

렌더링을 크게 그림자 만들기와 그림자 입히기로 나뉜다.

전역변수

원형 평면 메시 gBackgroundMesh와 그림자 만들기 셰이더 gCreateShadowShader를 변수를 추가한다.
토러스용 메시는 기존에 있던 gMesh를 사용한다.
그림자 입히기는 gShader를 사용한다.

LPD3DXMESH gBackgroundMesh = NULL;
LPD3DXEFFECT gCreateShadowShader = NULL;

셰이더의 메시용 색상을 위한 변수도 추가한다.
토러스는 gMeshColor, 원형 평면은 gBackgroundColor이다.

//메시 색상
D3DXVECTOR4 gMeshColor(1, 1, 0, 1);
D3DXVECTOR4 gBackgroundColor(1, 1, 0, 1);

그림자 만들기용 렌더 타겟을 추가한다.

//그림자 렌더타겟
LPDIRECT3DTEXTURE9 gShadowRenderTarget = NULL;
LPDIRECT3DSURFACE9 gShadowDepthStencil = NULL;

LPDIRECT3DSURFACE9는 그림자 맵을 그릴때 사용할 깊이 버퍼이다.
그림자맵의 크기가 화면의 크기보다 훨씬 크기 때문에 하드웨어 깊이 버퍼를 사용할 수 없다.

DestroyDirectX( ) 함수

추가했던 변수들을 해제한다.

_RELEASE_(gBackgroundMesh);
_RELEASE_(gCreateShadowShader);

_RELEASE_(gShadowRenderTarget);
_RELEASE_(gShadowDepthStencil);

LoadAsset( ) 함수

셰이더와 모델을 로딩한다.

// 셰이더 로딩
gShader = LoadShader("ApplyShadow.fx");
if(gShader == NULL)
    return false;

// 모델 로딩
gMesh = LoadModel("torus.x");
if(gMesh == NULL)
    return false;

//CreateShadow 셰이더 로딩
gCreateShadowShader = LoadShader("CreateShadow.fx");
if(gCreateShadowShader == NULL)
    return false;

//원형 평면 메시 로딩
gBackgroundMesh = LoadModel("disc.x");
if(gBackgroundMesh == NULL)
    return false;

InitShadowRenderTarget(  ) 함수

그림자맵의 크기를 2048로 한다.

CreateTexture( )에서 D3DUSAGE_RENDERTARGET,  D3DFMT_R32F 포맷으로 렌더타겟 텍스쳐를 만든다.

CreateDepthStencilSurface( )에서 그림자맵과 동일한 크기로 깊이 버퍼를 만든다.

//렌더타겟 생성
const int shadowMapSize = 2048;
if( FAILED(gD3DDevice->CreateTexture(
        shadowMapSize, shadowMapSize,
        1, //밉맵의 수, 렌더타겟은 하나만 만든다.
        D3DUSAGE_RENDERTARGET, //렌더타겟용 텍스쳐
        D3DFMT_R32F, //텍스처 포맷, 렌더몽키의 R32F 포맷
        D3DPOOL_DEFAULT,
        &gShadowRenderTarget, NULL)))
{
    return false;
}

//그림자맵과 동일한 크기의 깊이버퍼 생성
if (FAILED(gD3DDevice->CreateDepthStencilSurface(
        shadowMapSize, shadowMapSize,
        D3DFMT_D24X8, //텍스처 포맷, 깊이 버퍼로 24비트
        D3DMULTISAMPLE_NONE,
        0, //멀티샘플을 사용하지 않으니 0이다
        TRUE, //깊이버퍼를 바꿀 때, 그 안에 있던 내용을 보존하지 않는다.
        &gShadowDepthStencil, NULL)))
{
    return false;
}

RenderScene( ) 함수

렌더링에서 사용할 변수들을 알아보자.

CreateShadow 패스에서 필요한 변수는 gWorldMat, gLightViewMat, gLightProjMat이다.

ApplyShadowTorus 패스에서 필요한 변수는 gWorldLightPos, gWorldMat, gLightViewMat, gLightProjMat, gViewProjMat, gObjectColor이다.

두 패스에서 필요한 변수들을 미리 구한다.

gLightViewMat, gLightProjMat를 설정하기 위해 광원-뷰 행렬, 광원-투영 행렬을 구한다.

// 광원-뷰 행렬
D3DXMATRIXA16 matLightView;
{
    D3DXVECTOR3 vEyePt(gWorldLightPos.x, gWorldLightPos.y, gWorldLightPos.z);
    D3DXVECTOR3 vLookatPt(0.0f, 0.0f, 0.0f);
    D3DXVECTOR3 vUpVec(0.0f, 1.0f, 0.0f);
    D3DXMatrixLookAtLH(&matLightView, &vEyePt, &vLookatPt, &vUpVec);
}

// 광원-투영 행렬
D3DXMATRIXA16 matLightProjection;
D3DXMatrixPerspectiveFovLH(&matLightProjection, D3DX_PI / 4.0f, 1, 1, 3000);

gViewProjMat를 설정하기 위해 뷰-투영행렬을 구한다.

// 뷰-투영행렬
D3DXMATRIXA16 matViewProjection;
{
    //뷰행렬
    D3DXMATRIXA16 matView;
    D3DXVECTOR3 vEyePt(gWorldCameraPos.x, gWorldCameraPos.y, gWorldCameraPos.z);
    D3DXVECTOR3 vLookatPt(0.0f, 0.0f, 0.0f);
    D3DXVECTOR3 vUpVec(0.0f, 1.0f, 0.0f);
    D3DXMatrixLookAtLH(&matView, &vEyePt, &vLookatPt, &vUpVec);

    //투영행렬
    D3DXMATRIXA16 matProjection;
    D3DXMatrixPerspectiveFovLH(&matProjection, FOV, ASPECT_RATIO, NEAR_PLANE, FAR_PLANE);
    D3DXMatrixMultiply(&matViewProjection, &matView, &matProjection);
}

토러스와 원형판의 월드행렬 matTrousWorld, matDiscWorld를 구한다.

// 토러스 월드행렬을 만든다.
D3DXMATRIXA16 matTorusWorld;
{
    // 프레임마다 0.4도씩 회전을 시킨다.
    gRotationY += 0.004f * PI / 180.0f;
    if (gRotationY > 2 * PI)
    {
        gRotationY -= 2 * PI;
    }

    D3DXMatrixRotationY(&matTorusWorld, gRotationY);
}

//원형판의 월드행렬을 만든다.
D3DXMATRIXA16 matDiscWorld;
{
    D3DXMATRIXA16 matScale;
    D3DXMatrixScaling(&matScale, 2, 2, 2);

    D3DXMATRIXA16 matTrans;
    D3DXMatrixTranslation(&matTrans, 0, -40, 0);

    D3DXMatrixMultiply(&matDiscWorld, &matScale, &matTrans);
}

그림자맵을 만들때 렌더타겟을 설정하기 전에 다시 하드웨어 백버퍼에 그리기 위해 현재 렌더타겟을 다른 변수에 보관해 둔다.

// 현재 하드웨어 백버퍼와 깊이버퍼를 보관한다.
LPDIRECT3DSURFACE9 pHWBackBuffer = NULL;
LPDIRECT3DSURFACE9 pHWDepthStencilBuffer = NULL;
gD3DDevice->GetRenderTarget(0, &pHWBackBuffer);
gD3DDevice->GetDepthStencilSurface(&pHWDepthStencilBuffer);

 

1. 그림자 만들기

그림자맵용 렌더타겟을 설정한다. SetRenderTarget( )에 표면을 인자로 넣어준다.

// 그림자 맵의 렌더타깃과 깊이버퍼를 사용한다.
LPDIRECT3DSURFACE9 pShadowSurface = NULL;
if (SUCCEEDED(gShadowRenderTarget->GetSurfaceLevel(0, &pShadowSurface)))
{
    gD3DDevice->SetRenderTarget(0, pShadowSurface);
    pShadowSurface->Release();
    pShadowSurface = NULL;
}
gD3DDevice->SetDepthStencilSurface(gShadowDepthStencil);

이전 프레임에 그렸던 그림자맵을 하얀색으로 지운다. 한얀색이면 그림자를 적용하지 않는다.
깊이 버퍼도 마찬 가지로 지운다.

//이전 프레임에 그렸던 그림자 정보를 지움
gD3DDevice->Clear(0, NULL, (D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER), 0xFFFFFFFF, 1.0f, 0);

그림자 만들기 셰이더 gCreateShadowShader를 위해 월드행렬, 광원-뷰행렬, 광원-투영행렬을 설정한다.

// 그림자 만들기 쉐이더 전역변수들을 설정
gCreateShadowShader->SetMatrix("gWorldMat", &matTorusWorld);
gCreateShadowShader->SetMatrix("gLightViewMat", &matLightView);
gCreateShadowShader->SetMatrix("gLightProjMat", &matLightProjection);

그림자맵용으로 사용될 메시를 그린다.

// 그림자 만들기 쉐이더를 시작
{
    UINT numPasses = 0;
    gCreateShadowShader->Begin(&numPasses, NULL);
    {
        for (UINT i = 0; i < numPasses; ++i)
        {
            gCreateShadowShader->BeginPass(i);
            gMesh->DrawSubset(0); //torus 그리기
            gCreateShadowShader->EndPass();
        }
    }
    gCreateShadowShader->End();
}

2. 그림자 그리기

토러스와 원형판에 앞에서 만든 그림자맵을 그린다.

렌더타겟을 하드웨어 백버퍼, 깊이버퍼로 원상복구 시킨다.

pHWBackBuffer, pHWDepthStencilBuffer를 해제하는 것은 GetRenderTarget( ), GetDepthStencilSurface( ) 함수를 호출할 때 D3D 내부적으로 버퍼들의 참조카운터를 증가시켜 주기 때문이다.

//하드웨어 백버퍼, 깊이버퍼 사용
gD3DDevice->SetRenderTarget(0, pHWBackBuffer);
gD3DDevice->SetDepthStencilSurface(pHWDepthStencilBuffer);

pHWBackBuffer->Release();
pHWBackBuffer = NULL;
pHWDepthStencilBuffer->Release();
pHWDepthStencilBuffer = NULL;

물체를 그리기 위한 변수들을 설정한다.

난반사를 계산하기 위한 광원의 위치( gWorldLightPos )가 필요하다.
물체 렌더링을 위한 월드 행렬, 뷰-투영행렬이 필요하다.
광원으로부터 물체까지의 깊이를 구하기 위한 광원-뷰행렬과 광원-투영행렬도 필요하다.
물체의 색깔을 위한 gObjectColor도 필요하다.

// 그림자 입히기 쉐이더 전역변수들을 설정
gShader->SetVector("gWorldLightPos", &gWorldLightPos);
gShader->SetMatrix("gWorldMat", &matTorusWorld);    //원환체
gShader->SetMatrix("gLightViewMat", &matLightView);
gShader->SetMatrix("gLightProjMat", &matLightProjection);
gShader->SetMatrix("gViewProjMat", &matViewProjection);

gShader->SetVector("gObjectColor", &gMeshColor);

렌더몽키의 셰이더를 fx로 익스포트하면 texture 이름에 접미사 "_Tex"를 추가한다.
원래 텍스쳐의 이름음 ShadowMap이었지만 익스포트하면 ShadowMap_Tex가 된다.

CreateShadown.fx나 ApplyShadow.fx를 열어보면 "_Tex"가 자동으로 추가된다.

texture ShadowMap_Tex

gShader->SetTexture("ShadowMap_Tex", gShadowRenderTarget);

토러스를 그린다.

UINT numPasses = 0;
gShader->Begin(&numPasses, NULL);
{
    for (UINT i = 0; i < numPasses; ++i)
    {
        gShader->BeginPass(i);
        {
            //torus 그리기
            gMesh->DrawSubset(0);

원형판을 그린다.

BeginPass( )와 EndPass( ) 사이에 변수값을 수정 할 때는 CommitChanges( ) 함수를 호출한다음에 원형판을 그린다.

            //원형판 그리기
            gShader->SetMatrix("gWorldMat", &matDiscWorld);
            gShader->SetVector("gObjectColor", &gBackgroundColor);
            gShader->CommitChanges();
            gBackgroundMesh->DrawSubset(0);
        }
        gShader->EndPass();
    }
}
gShader->End();

다운로드 : ApplyShadow.rfx , CreateShadow.rfx , ShadowMapping5.rfx , DxDemo_ShadowMapping.zip

참고 : 한빛미디어 셰이더 프로그래밍 입문