이번엔 조명 코드를 분석해보자

우선 LightDemo를 실행시켜보면 실시간으로 Spot, Direct, Point Light가 적용돼서 화면이 보이는 것을 볼 수 있다.

또한 전에 배웠던 Diffuse Ambient Specular Light도 모두 적용되어 있는 모습을 볼 수 있다. 

이렇게 다양한 조명을 적용하는 것도 좋지만 조명이 많아질 수록 연산량이 늘어나기 때문에 필요한 조명만 필요한 영역에 사용하는 것이 좋다.

우선 LightDemo 코드를 보면 헤더파일에 LightHelper가 추가되어있는데 이는 각 조명에 대한 구조체 정의가 있는 부분이다. 

우선 DirectionalLight는 일직선으로 가는 조명으로 Ambient Diffuse Specular 3가지 특성을 가지고 있다.

struct DirectionalLight
{
	DirectionalLight() { ZeroMemory(this, sizeof(this)); }

	XMFLOAT4 Ambient;
	XMFLOAT4 Diffuse;
	XMFLOAT4 Specular;
	XMFLOAT3 Direction;
	float Pad; // Pad the last float so we can set an array of lights if we wanted.
};

 

PointLight는 어떤 Point로부터 모든 방향으로 동일한 세기의 빛을 뿜는 광원을 말한. 이 조명에는 Ambient,Diffuse,Specular 특성이 있고 Point의 위치와 영향을 줄 거리, 높이 등으로 이루어져있다. 

struct PointLight
{
	PointLight() { ZeroMemory(this, sizeof(this)); }

	XMFLOAT4 Ambient;
	XMFLOAT4 Diffuse;
	XMFLOAT4 Specular;

	// Packed into 4D vector: (Position, Range)
	XMFLOAT3 Position;
	float Range;

	// Packed into 4D vector: (A0, A1, A2, Pad)
	XMFLOAT3 Att;
	float Pad; // Pad the last float so we can set an array of lights if we wanted.
};

 

SpotLight는 손전등과 무대 조명과 같이 원뿔 모양으로 한곳을 비추는 조명으로 구조체 내부는 Point Light와 같다.

struct SpotLight
{
	SpotLight() { ZeroMemory(this, sizeof(this)); }

	XMFLOAT4 Ambient;
	XMFLOAT4 Diffuse;
	XMFLOAT4 Specular;

	// Packed into 4D vector: (Position, Range)
	XMFLOAT3 Position;
	float Range;

	// Packed into 4D vector: (Direction, Spot)
	XMFLOAT3 Direction;
	float Spot;

	// Packed into 4D vector: (Att, Pad)
	XMFLOAT3 Att;
	float Pad; // Pad the last float so we can set an array of lights if we wanted.
};

 

여기서 조명마다 있는 pad는 16바이트 정렬을 해주기 위한 값이다.

이렇게 정의해준 조명 중에 어떤 조명을 사용할 지 골라준 다음에 쉐이더에서 빛에 따른 색상 연산을 해주어야한다. 쉐이더 코드를 살펴보자면 우선 위의 조명 구조체와 구조를 맞춰준 구조체를 쉐이더쪽에서도 만들어줘야한다.

struct DirectionalLight
{
	float4 Ambient;
	float4 Diffuse;
	float4 Specular;
	float3 Direction;
	float pad;
};

struct PointLight
{ 
	float4 Ambient;
	float4 Diffuse;
	float4 Specular;

	float3 Position;
	float Range;

	float3 Att;
	float pad;
};

struct SpotLight
{
	float4 Ambient;
	float4 Diffuse;
	float4 Specular;

	float3 Position;
	float Range;

	float3 Direction;
	float Spot;

	float3 Att;
	float pad;
};

조명에 대한 값을 매 프레임 받아주기 위해 상수버퍼를 추가해준 것을 볼 수 있다. 

cbuffer cbPerFrame
{
	DirectionalLight gDirLight;
	PointLight gPointLight;
	SpotLight gSpotLight;
	float3 gEyePosW;
};

 

그리고 쉐이더에서 각 조명을 연산해주는 코드를 살펴볼 것인데 대표적으로 DirectionalLight 코드만 분석해보자.

oid ComputeDirectionalLight(Material mat, DirectionalLight L, 
                             float3 normal, float3 toEye,
					         out float4 ambient,
						     out float4 diffuse,
						     out float4 spec)
{
	// Initialize outputs.
	ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
	diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
	spec    = float4(0.0f, 0.0f, 0.0f, 0.0f);

	// The light vector aims opposite the direction the light rays travel.
	float3 lightVec = -L.Direction;

	// Add ambient term.
	ambient = mat.Ambient * L.Ambient;	

	// Add diffuse and specular term, provided the surface is in 
	// the line of site of the light.
	
	float diffuseFactor = dot(lightVec, normal);

	// Flatten to avoid dynamic branching.
	[flatten]
	if( diffuseFactor > 0.0f )
	{
		float3 v         = reflect(-lightVec, normal);
		float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);
					
		diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
		spec    = specFactor * mat.Specular * L.Specular;
	}
}

 

코드를 보면 조명의 특성 3가지를 동시에 연산한 다음에 최종적인 색상을 반환해주고 있다. 뱉어준 색상을 통해 PS단계에서 다른 조명의 색상이 있다면 이것까지 연산에 포함해서 최종적으로 화면에 보이는 색을 반환해준다. 

float4 PS(VertexOut pin) : SV_Target
{
	// Interpolating normal can unnormalize it, so normalize it.
	pin.NormalW = normalize(pin.NormalW);

	float3 toEyeW = normalize(gEyePosW - pin.PosW);

	// Start with a sum of zero. 
	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);

	// Sum the light contribution from each light source.
	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;

	// Common to take alpha from diffuse material.
	litColor.a = gMaterial.Diffuse.a;

	return litColor;
}

 

그리고 조명 코드 밑에 보면 Material이라는 구조체가 있는데 이는 물체의 특징으로 어떤 재질도 되어 있는지에 관한 정이다. 이 재질에 따라 빛을 반사하는 정도가 다를 것이다. 결국은 재질 과 빛의 특징 연산을 통해 최종적인 빛 연산이 이루어지게 된다.

 

연산의 이론적인 부분을 한번 복습 해보자.

우선 Ambient(주변광)는 빛의 반사를 생각해서 최소한의 빛을 보장하기 위해 빛의 색상을 일정 수준이라도 더 해준다. 

코드에서 보면 단순하게 Ambient값을 곱해주는 것을 볼 수 있다.

ambient = mat.Ambient * L.Ambient;

 

Diffuse(확산광)는 물체에 빛을 쏜다고 했을 때 물체 포면에 닿아서 여러 방향으로 확산되는 특징을 가지고 있으며 표면과 물체의 노멀 벡터 각도가 0도일 때 가장 강하며 90도일 때 가장 약하다. 이 빛은 물체의 재질에 영향을 많이 받는다.

이 값은 -1~ 1까지 의 범위를 가지고 있다. 계산은 -L과 N의 내적을 구해주면 cos값을 구할 수 있는데

이를 물체 재질값과 Diffuse 값을 곱해주면 된다.

코드 상에서 공식은 다음과 같다. 

float3 lightVec = -L.Direction;
float diffuseFactor = dot(lightVec, normal);
diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;

 

Specular(경사)은 물체의 표면에서 특정 각도로 반사되어서 우리 눈에 보이는 조명이다. 반짝이는 효과라고 생각해도 된다. 물체의 재질에 따라 반짝임 정도가 달라진다.

계산은 L과 N의 반사각을 구한 다음 눈까지의 각도와 내적해준 다음 Specular값을 배수값으로 곱해주면 된다.

float3 v         = reflect(-lightVec, normal);
float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);

spec    = specFactor * mat.Specular * L.Specular;

 

이렇게 해준 다음에 최종적으로  PS단계에서는 이 3가지 특성의 합으로 조명의 값을 연산해준다.

float4 litColor = ambient + diffuse + spec;

+ Recent posts