서적 정리/DirectX11을 이용한 3D 게임 프로그래밍 입문
69.조명 예제
민돌이2
2022. 3. 7. 20:21
Lighting.zip
5.36MB
지향광원, 점광원, 점적광원 총 세 개의 광원이 구현되어있다.
1.셰이더 파일
#include "LightHelper.fx"
cbuffer cbPerFrame
{
DirectionalLight gDirLight;
PointLight gPointLight;
SpotLight gSpotLight;
float3 gEyePosW;
};
cbuffer cbPerObject
{
float4x4 gWorld;
float4x4 gWorldInvTranspose;
float4x4 gWorldViewProj;
Material gMaterial;
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
//월드 공간으로 변환
vout.PosW = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
vout.NormalW = mul(vin.NormalL, (float3x3)gWorldInvTranspose);
//동차 절단 공간으로 변환
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
//보간 때문에 법선이 더이상 단위벡터가 아닐 수 있으므로 다시 정규화
pin.NormalW = normalize(pin.NormalW);
float3 toEyeW = normalize(gEyePosW - pin.PosW);
//성분들의 합이 0인 재질들로 시작
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
//각 광원이 기여한 빛을 합한다.
float4 A, D, S;
//지향광
ComputeDirectionalLight(gMaterial, gDirLight, pin.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
//점적광
ComputePointLight(gMaterial, gPointLight, pin.PosW, pin.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
//점적광원
ComputeSpotLight(gMaterial, gSpotLight, pin.PosW, pin.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
float4 litColor = ambient + diffuse + spec;
//분산광 재질의 알파와 텍스처의 알파의 곱을 전체적인 알파 값으로 사용한다.
litColor.a = gMaterial.Diffuse.a;
return litColor;
}
technique11 LightTech
{
pass P0
{
SetVertexShader( CompileShader( vs_5_0, VS() ) );
SetGeometryShader( NULL );
SetPixelShader( CompileShader( ps_5_0, PS() ) );
}
}
2.C++ 응용 프로그램 코드
조명 계산을 위해서 표면 법선이 필요하다. 이 예제는 법선 벡터들을 정점별로 설정한다. 래스터화 과정에서 그 법선들이 삼각형 표면을 따라 보간되어 표면의 각 픽셀의 법선이 결정되고, 그 법선을 이용해서 각 픽셀마다 조명 값을 계산한다. 이전과 다르게 정점 색상을 지정하지 않고, 각 픽셀마다 조명 방정식을 적용하여 표면의 색을 결정한다.
입력 배치 서술 배열을 다음과 같다.
D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}
};
광원과 재질의 구조체는 LightHelper.h에 정의되어 있다.
struct DirectionalLight
{
DirectionalLight() { ZeroMemory(this, sizeof(this)); }
XMFLOAT4 Ambient;
XMFLOAT4 Diffuse;
XMFLOAT4 Specular;
XMFLOAT3 Direction;
float Pad;
};
struct PointLight
{
PointLight() { ZeroMemory(this, sizeof(this)); }
XMFLOAT4 Ambient;
XMFLOAT4 Diffuse;
XMFLOAT4 Specular;
XMFLOAT3 Position;
float Range;
XMFLOAT3 Att;
float Pad;
};
struct SpotLight
{
SpotLight() { ZeroMemory(this, sizeof(this)); }
XMFLOAT4 Ambient;
XMFLOAT4 Diffuse;
XMFLOAT4 Specular;
XMFLOAT3 Position;
float Range;
XMFLOAT3 Direction;
float Spot;
XMFLOAT3 Att;
float Pad;
};
struct Material
{
Material() { ZeroMemory(this, sizeof(this)); }
XMFLOAT4 Ambient;
XMFLOAT4 Diffuse;
XMFLOAT4 Specular;
XMFLOAT4 Reflect;
};
위의 구조체를 생성자에서 아래와 같이 초기화한다.
LightingApp::LightingApp(HINSTANCE hInstance)
: D3DApp(hInstance), mLandVB(0), mLandIB(0), mWavesVB(0), mWavesIB(0),
mFX(0), mTech(0), mfxWorld(0), mfxWorldInvTranspose(0), mfxEyePosW(0),
mfxDirLight(0), mfxPointLight(0), mfxSpotLight(0), mfxMaterial(0),
mfxWorldViewProj(0),
mInputLayout(0), mEyePosW(0.0f, 0.0f, 0.0f), mTheta(1.5f*MathHelper::Pi), mPhi(0.1f*MathHelper::Pi), mRadius(80.0f)
{
...
//지향광원 초기화
mDirLight.Ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
mDirLight.Diffuse = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
mDirLight.Specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
mDirLight.Direction = XMFLOAT3(0.57735f, -0.57735f, 0.57735f);
//점적광원 초기화
mPointLight.Ambient = XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f);
mPointLight.Diffuse = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f);
mPointLight.Specular = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f);
mPointLight.Att = XMFLOAT3(0.0f, 0.1f, 0.0f);
mPointLight.Range = 25.0f;
//점적광원 초기화
mSpotLight.Ambient = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
mSpotLight.Diffuse = XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f);
mSpotLight.Specular = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
mSpotLight.Att = XMFLOAT3(1.0f, 0.0f, 0.0f);
mSpotLight.Spot = 96.0f;
mSpotLight.Range = 10000.0f;
mLandMat.Ambient = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.0f);
mLandMat.Diffuse = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.0f);
mLandMat.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);
mWavesMat.Ambient = XMFLOAT4(0.137f, 0.42f, 0.556f, 1.0f);
mWavesMat.Diffuse = XMFLOAT4(0.137f, 0.42f, 0.556f, 1.0f);
mWavesMat.Specular = XMFLOAT4(0.8f, 0.8f, 0.8f, 96.0f);
}
지향광원의 경우 벡터가 변할일이 없지만, 점광원과 점적광원은 변할 일이 존재한다. 따라서 UpdateScene함수에서 업데이트를 한다.
void LightingApp::UpdateScene(float dt)
{
//구면 좌표를 데카르트 좌표로 변환
float x = mRadius*sinf(mPhi)*cosf(mTheta);
float z = mRadius*sinf(mPhi)*sinf(mTheta);
float y = mRadius*cosf(mPhi);
mEyePosW = XMFLOAT3(x, y, z);
//뷰 행렬
XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
...
//광원이 지형 주위로 원을 그리면서 이동
mPointLight.Position.x = 70.0f*cosf( 0.2f*mTimer.TotalTime() );
mPointLight.Position.z = 70.0f*sinf( 0.2f*mTimer.TotalTime() );
mPointLight.Position.y = MathHelper::Max(GetHillHeight(mPointLight.Position.x,
mPointLight.Position.z), -3.0f) + 10.0f;
//점적광원은 카메라의 위치에서 카메라가 바라보는 방향으로 빛을 보낸다.
mSpotLight.Position = mEyePosW;
XMStoreFloat3(&mSpotLight.Direction, XMVector3Normalize(target - pos));
}
광원들과 재질들을 설정한 후 렌더링을 수행한다.
void LightingApp::DrawScene()
{
...
//매 프레임마다 바뀌는 상수 설정
mfxDirLight->SetRawValue(&mDirLight, 0, sizeof(mDirLight));
mfxPointLight->SetRawValue(&mPointLight, 0, sizeof(mPointLight));
mfxSpotLight->SetRawValue(&mSpotLight, 0, sizeof(mSpotLight));
mfxEyePosW->SetRawValue(&mEyePosW, 0, sizeof(mEyePosW));
D3DX11_TECHNIQUE_DESC techDesc;
mTech->GetDesc(&techDesc);
for (UINT p = 0; p < techDesc.Passes; ++p)
{
//언덕 렌더링
md3dImmediateContext->IASetVertexBuffers(0, 1, &mLandVB, &stride, &offset);
md3dImmediateContext->IASetIndexBuffer(mLandIB, DXGI_FORMAT_R32_UINT, 0);
XMMATRIX world = XMLoadFloat4x4(&mLandWorld);
XMMATRIX worldInvTranspose = MathHelper::InverseTranspose(world);
XMMATRIX worldViewProj = world * view*proj;
mfxWorld->SetMatrix(reinterpret_cast<float*>(&world));
mfxWorldInvTranspose->SetMatrix(reinterpret_cast<float*>(&worldInvTranspose));
mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
mfxMaterial->SetRawValue(&mLandMat, 0, sizeof(mLandMat));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mLandIndexCount, 0, 0);
//파도 렌더링
md3dImmediateContext->IASetVertexBuffers(0, 1, &mWavesVB, &stride, &offset);
md3dImmediateContext->IASetIndexBuffer(mWavesIB, DXGI_FORMAT_R32_UINT, 0);
world = XMLoadFloat4x4(&mWavesWorld);
worldInvTranspose = MathHelper::InverseTranspose(world);
worldViewProj = world * view*proj;
mfxWorld->SetMatrix(reinterpret_cast<float*>(&world));
mfxWorldInvTranspose->SetMatrix(reinterpret_cast<float*>(&worldInvTranspose));
mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
mfxMaterial->SetRawValue(&mWavesMat, 0, sizeof(mWavesMat));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(3 * mWaves.TriangleCount(), 0, 0);
}
HR(mSwapChain->Present(0, 0));
}
3.법선 계산
법선 벡터는 표면의 접평면에 놓인 두 벡터를 구한 후, 이들의 외적을 취해서 얻을 수 있다.

지형 메시를 생성하는 데 사용한 함수는 아래와 같다.

이 함수를 코드로 구현하면 아래와 같다.
float LightingApp::GetHillHeight(float x, float z)const
{
return 0.3f * (z * sinf(0.1f * x) + x * cosf(0.1f * z));
}
위에서 사용한 함수를 편미분을 하면 아래와 같다.

실제로 필요한 것은 정규화한 법선 벡터이므로 반드시 정규화를 해야 한다. 편미분과 정규화를 코드로 구현하면 아래와 같다.
XMFLOAT3 LightingApp::GetHillNormal(float x, float z)const
{
// n = (-df/dx, 1, -df/dz)
XMFLOAT3 n(
-0.03f*z*cosf(0.1f*x) - 0.3f*cosf(0.1f*z),
1.0f,
-0.3f*sinf(0.1f*x) + 0.03f*x*sinf(0.1f*z));
XMVECTOR unitNormal = XMVector3Normalize(XMLoadFloat3(&n));
XMStoreFloat3(&n, unitNormal);
return n;
}
728x90