Directional light로는 표현할 수 없는 상황들이 게임 속에는 다수 존재한다.
빛과 물체의 상대적인 위치에 따라 물체에 가해지는 명암이 달라지는 경우 특히 그러한데, 방 안에 전구가 있고 그 전구를 기점으로 물체가 빙글빙글 돌아갈 때의 명암을 생각하면 쉽다.
또한 빛과 물체의 거리와 상관없이 빛의 세기가 일정한 directional light와는 다르게, 먼 물체일 수 록 더 어둡게 표현하고 싶다면 다른 형태의 빛이 필요하다.
이런 상황들에서 사용할 수 있는 게 바로 point light 이다.
상대적인 위치에 따라 명암을 동적으로 계산할 뿐만 아니라, 거리에 따라 빛이 감소(dropoff)하기도 한다.
이는 1/(거리)^2 에 비례한 형태로 나타나는데, 이때 명암을 단순히 1/x^2로 설정해버리면 빛과 물체의 거리가 0에 가까워질 때 물체의 밝기가 무한대를 향해 발산하는 문제가 발생한다.
이를 해결하기 위해 분모 식을 Ad^2+Bd+C라는 일반식의 형태로 바꾸면 다양한 형태의 빛을 표현할 수 있다.
일반적으로 위와 같은 형태가 자주 사용되며, 검색해보면 자주 사용되는 A,B,C (Coefficient)의 값을 찾을 수 있으니 참고.
이렇게 거리에 따른 명암의 구현을 살펴보았으니 이번에는 상대적 위치에 따른 명암의 구현을 살펴보자.
// GouraudPointEffect.h
#pragma once
#include "Pipeline.h"
#include "DefaultGeometryShader.h"
// flat shading with vertex normals
class GouraudPointEffect
{
public:
// the vertex type that will be input into the pipeline
class Vertex
{
public:
Vertex() = default;
Vertex( const Vec3& pos )
:
pos( pos )
{}
Vertex( const Vec3& pos,const Vertex& src )
:
n( src.n ),
pos( pos )
{}
Vertex( const Vec3& pos,const Vec3& n )
:
n( n ),
pos( pos )
{}
Vertex& operator+=( const Vertex& rhs )
{
pos += rhs.pos;
return *this;
}
Vertex operator+( const Vertex& rhs ) const
{
return Vertex( *this ) += rhs;
}
Vertex& operator-=( const Vertex& rhs )
{
pos -= rhs.pos;
return *this;
}
Vertex operator-( const Vertex& rhs ) const
{
return Vertex( *this ) -= rhs;
}
Vertex& operator*=( float rhs )
{
pos *= rhs;
return *this;
}
Vertex operator*( float rhs ) const
{
return Vertex( *this ) *= rhs;
}
Vertex& operator/=( float rhs )
{
pos /= rhs;
return *this;
}
Vertex operator/( float rhs ) const
{
return Vertex( *this ) /= rhs;
}
public:
Vec3 pos;
Vec3 n;
};
// calculate color based on normal to light angle
// no interpolation of color attribute
class VertexShader
{
public:
class Output
{
public:
Output() = default;
Output( const Vec3& pos )
:
pos( pos )
{}
Output( const Vec3& pos,const Output& src )
:
color( src.color ),
pos( pos )
{}
Output( const Vec3& pos,const Vec3& color )
:
color( color ),
pos( pos )
{}
Output& operator+=( const Output& rhs )
{
pos += rhs.pos;
color += rhs.color;
return *this;
}
Output operator+( const Output& rhs ) const
{
return Output( *this ) += rhs;
}
Output& operator-=( const Output& rhs )
{
pos -= rhs.pos;
color -= rhs.color;
return *this;
}
Output operator-( const Output& rhs ) const
{
return Output( *this ) -= rhs;
}
Output& operator*=( float rhs )
{
pos *= rhs;
color *= rhs;
return *this;
}
Output operator*( float rhs ) const
{
return Output( *this ) *= rhs;
}
Output& operator/=( float rhs )
{
pos /= rhs;
color /= rhs;
return *this;
}
Output operator/( float rhs ) const
{
return Output( *this ) /= rhs;
}
public:
Vec3 pos;
Vec3 color;
};
public:
void BindRotation( const Mat3& rotation_in )
{
rotation = rotation_in;
}
void BindTranslation( const Vec3& translation_in )
{
translation = translation_in;
}
Output operator()( const Vertex& v ) const
{
// transform mech vertex position before lighting calc
const auto pos = v.pos * rotation + translation;
// 빛 연산을 해줘야 하므로 좌표계 통일을 위해 월드좌표계로 좌표를 우선 변환시켜준다.
// v.pos는 셰이더 연산을 적용시킬 모델(물체)의 정점이다.
// vertex to light data
const auto v_to_l = light_pos - pos; // vector to light = 물체에서 빛으로 향하는 벡터
const auto dist = v_to_l.Len();
const auto dir = v_to_l / dist; // direction은 그냥 그 벡터를 normalize 시킨 것
// calculate attenuation
// 거리에 따른 명암 적용 (상수들이 곱해진 것을 볼 수 있다)
const auto attenuation = 1.0f /
(constant_attenuation + linear_attenuation * dist * quadradic_attenuation * sq( dist ));
// calculate intensity based on angle of incidence and attenuation
// 각과 거리에 따른 명암 조정 (각에 의한 명암 조정은 directional light때와 동일)
const auto d = light_diffuse * attenuation * std::max( 0.0f,(v.n * rotation) * dir );
// add diffuse+ambient, filter by material color, saturate and scale
// 마찬가지로 directional light와 동일한 과정
const auto c = material_color.GetHadamard( d + light_ambient ).Saturate() * 255.0f;
return{ pos,c };
}
void SetDiffuseLight( const Vec3& c )
{
light_diffuse = c;
}
void SetAmbientLight( const Vec3& c )
{
light_ambient = c;
}
void SetLightPosition( const Vec3& pos_in )
{
light_pos = pos_in;
}
private:
Mat3 rotation;
Vec3 translation;
Vec3 light_pos = { 0.0f,0.0f,0.5f };
Vec3 light_diffuse = { 1.0f,1.0f,1.0f };
Vec3 light_ambient = { 0.1f,0.1f,0.1f };
Vec3 material_color = { 0.8f,0.85f,1.0f };
float linear_attenuation = 1.0f;
float quadradic_attenuation = 2.619f;
float constant_attenuation = 0.382f;
};
// default gs passes vertices through and outputs triangle
typedef DefaultGeometryShader<VertexShader::Output> GeometryShader;
// invoked for each pixel of a triangle
// takes an input of attributes that are the
// result of interpolating vertex attributes
// and outputs a color
class PixelShader
{
public:
template<class Input>
Color operator()( const Input& in ) const
{
return Color( in.color );
}
};
public:
VertexShader vs;
GeometryShader gs;
PixelShader ps;
};
참고사항: 빛의 위치를 시각적으로 나타내기 위해 별도의 파이프라인을 GouraudPointScene.h에 추가했다.
잘 작동하는 듯 해 보이지만, 완전히 별도의 렌더링 파이프라인을 사용하기 때문에 모델 뒤로 빛이 지나가도 계속 빛이 보이는 것을 볼 수 있다.
이유는 단순한데, 각 파이프라인 별로 별도의 z-buffer를 기반으로 occlusion을 처리하기 때문에 이를 하나로 합쳐주는 과정이 필요하다.
이때 하나로 버퍼를 일일히 확인하면서 비교하는 것보다 훨씬 간단한 방법은 애초에 버퍼를 하나만 만들고 이를 공유하는 것인데, 이를 구현한 코드는 사진과 같다.
shared_ptr을 사용해 공동의 리소스를 사용하는 것으로, 크게 어려운 부분은 없다. 에러가 나지 않게 버퍼와 실제 화면의 크기 비교를 해주는 부분이 추가되었다.
잘 작동하는 것을 볼 수 있다.
그러나 우리의 파이프라인에는 아직 한 가지 큰 문제가 남아있는데, 이는 다음 글에서 다루도록 하겠다.
'computer graphics > 3D Graphics' 카테고리의 다른 글
[3DGraphics] 18. Specular highlights (1) | 2022.10.11 |
---|---|
[3DGraphics] 17. Phong shading & Per-pixel lighting (0) | 2022.09.09 |
[3DGraphics] 15. Gouraud shading (0) | 2022.09.09 |
[3DGraphics] 14. Flat shading and Mesh loading (0) | 2022.09.09 |
[3DGraphics] 13. Geometry shader (0) | 2022.07.24 |