물체가 질감에 따라 다르게 보이는 이유는 무엇일까?
질감에 따라 표면의 울퉁불퉁한 정도가 다르고, 때문에 빛이 표면에서 반사되는 방향이 표면 자체의 방향과 일치하지 않기 때문이다.
이번 포스트에서는 이러한 질감을 구현하기 위해 물체의 Diffuse와 Specular한 정도에 따라서 빛이 적절한 수준으로 퍼지게 만들어 보다 현실적인 그래픽을 그려보겠다.
구현에 앞서 한가지 질문을 던져보겠다.
반짝거리는 금속과 같은 물체에 보이는 "광"은 왜 나타나는 것일까?
빛이 물체의 표면에 맞고 반사되어 카메라(관찰자)를 향해 맞는다면 그 부분이 더 밝게 보이기 때문에 광이 보이는 것이다.
이를 컴퓨터그래픽스에서는 어떻게 구현해야 할까?
단순히 어떤 빛이 물체의 표면 위의 점 A에 맞고 튕겨나온 반사광이 카메라가 위치한 정점을 향했을 때 물체의 그 부분을 더 밝게 보이도록 만드는 형태로(=점 A의 intensity를 높이는 형태로) 구현할 수도 있겠다.
그러나 이를 구현하고나면 한 가지 문제가 발생하는데, 바로 카메라의 크기가 정점 하나만을 차지하는 만큼, 이렇게 구현하게 될 경우 물체가 반사광을 가지기 아주 어려워질 것이라는 점이다. (정확히 한 점에 반사광이 닿아야 하므로 확률이 0에 가까워진다)
고로 우리는 이를 구현하기 위해 카메라로 향하는 벡터가 일정 각도 내에 들 경우 그 각도에 따라 반사광의 밝기를 조정하는 형태로 이를 구현한다.
이 방식을 실제로 구현해보자.
우선 반사광과 카메라가 이루는 각 세타는 두 벡터의 내적으로 쉽게 구할 수 있다. (둘 다 normalized 되어있다는 전제 하에)
그리고 우리는 이 각의 크기가 작을 수록 intensity(밝기)를 높여줄 것이므로, 각과 밝기에 대한 함수가 필요한데, 이는 코사인을 거듭제곱해주는 것으로 구현할 수 있다.
(cos(세타) ^ x 일때 x가 커질 수록 더 그래프를 더 가파르게 만들 수 있다)
필요한 공식이 정리가 되었으니, 필요한 벡터값을 주어진 값들로부터 구해보자.
우리에게 주어진 값이 광원 L벡터, 정점의 노멀값인 n벡터, 그리고 카메라의 위치와 정점까지의 벡터인 V벡터라고 하자.
우리가 구해야 하는 벡터는 반사광을 나타내는 R벡터이므로 이를 구해보자.
우선 L과 R이 입사각과 반사각의 성질을 갖으며, 길이가 같다는 점을 이용해 w를 구할 수 있다.
L의 역방향으로 -L을 그리고 L,-L,R이 만드는 삼각형을 살펴보면 n의 정수배인 w벡터와 -L~R까지를 잇는 u벡터를 그렸을 때 삼각형의 닮음에 의해 w:u는 1:2임을 알 수 있다.
즉, u = 2w이다.
이 식을 사용해 R을 구해보자.
R = -L + u
R = -L + 2w
이때 내적과 정사영의 성질에 의해 다음 식이 성립한다.
(L * n => n은 길이가 1이므로 |L| cos 세타, 그런데 w는 n과 방향이 같은 벡터이므로 |L| cos 세타 * n = w)
w = (L * n) * n // (L * n)이 스칼라임에 유의
고로 R은 다음과 같이 L과 n으로 나타낼 수 있다.
R = -L + 2(L * n) * n
코드와 함께 살펴보자.
// SpecularPhongPointEffect.h
#pragma once
#include "Pipeline.h"
#include "DefaultGeometryShader.h"
// flat shading with vertex normals
class SpecularPhongPointEffect
{
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 )
{}
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 )
:
n( src.n ),
worldPos( src.worldPos ),
pos( pos )
{}
Output( const Vec3& pos,const Vec3& n,const Vec3& worldPos )
:
n( n ),
pos( pos ),
worldPos( worldPos )
{}
Output& operator+=( const Output& rhs )
{
pos += rhs.pos;
n += rhs.n;
worldPos += rhs.worldPos;
return *this;
}
Output operator+( const Output& rhs ) const
{
return Output( *this ) += rhs;
}
Output& operator-=( const Output& rhs )
{
pos -= rhs.pos;
n -= rhs.n;
worldPos -= rhs.worldPos;
return *this;
}
Output operator-( const Output& rhs ) const
{
return Output( *this ) -= rhs;
}
Output& operator*=( float rhs )
{
pos *= rhs;
n *= rhs;
worldPos *= rhs;
return *this;
}
Output operator*( float rhs ) const
{
return Output( *this ) *= rhs;
}
Output& operator/=( float rhs )
{
pos /= rhs;
n /= rhs;
worldPos /= rhs;
return *this;
}
Output operator/( float rhs ) const
{
return Output( *this ) /= rhs;
}
public:
Vec3 pos;
Vec3 n;
Vec3 worldPos;
};
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
{
const auto pos = v.pos * rotation + translation;
return{ pos,v.n * rotation,pos };
}
private:
Mat3 rotation;
Vec3 translation;
};
// 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
{
// re-normalize interpolated surface normal
const auto surf_norm = in.n.GetNormalized();
// vertex to light data
const auto v_to_l = light_pos - in.worldPos;
const auto dist = v_to_l.Len();
const auto dir = v_to_l / dist;
// 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
const auto d = light_diffuse * attenuation * std::max( 0.0f,surf_norm * dir );
// reflected light vector
// 앞서 구한 공식을 사용한다
const auto w = surf_norm * (v_to_l * surf_norm);
const auto r = w * 2.0f - v_to_l;
// calculate specular intensity based on angle between viewing vector and reflection vector, narrow with power function
// 반사광의 각도에 따라 추가적인 intensity 배수를 결정한다.
// specular_power로 거듭제곱을 해주어 각도에 따라 밝아지는 경사의 정도를 결정하고,
// specular_intensity로 어느 정도 수준으로 밝게 해 줄지를 결정한다.
// V벡터는 in.worldPos.GetNormalized()로 구하고 있다. 잘 보면 내적하기 전에 - 부호를 붙여 방향을 반대로 만들어주는 걸 볼 수 있는데, 이는 in.worldPos.GetNormalized()는 카메라의 좌표는 항상 0,0,0 이므로 worldPos는 카메라가 0,0,0일때 월드의 상대적인 좌표이므로, 이를 뒤집어줘야 우리가 원하는 V벡터를 구할 수 있기 때문이다.
// light_diffuse는 Vec3로, 반사광의 색상(광택의 색상)을 나타낸다.
const auto s = light_diffuse * specular_intensity * std::pow( std::max( 0.0f,-r.GetNormalized() * in.worldPos.GetNormalized() ),specular_power ); // 내적한 결과가 cos그래프꼴로 나온기 때문에 거듭제곱하면 앞서 본 그래프 형태가 나온다
// add diffuse+ambient, filter by material color, saturate and scale
// Specular (s)를 더해주는 것을 볼 수 있다.
return Color( material_color.GetHadamard( d + light_ambient + s ).Saturate() * 255.0f );
}
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:
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 };
// diffuse
float linear_attenuation = 1.0f;
float quadradic_attenuation = 2.619f;
float constant_attenuation = 0.382f;
// specular
float specular_power = 30.0f;
float specular_intensity = 0.6f;
};
public:
VertexShader vs;
GeometryShader gs;
PixelShader ps;
};
아주 잘 작동함을 볼 수 있다!
specular_power를 낮춰 조금 더 매트한 느낌을 줄 수 있다.
Specular highlight는 당연히 point light 외의 light들에도 적용할 수 있다.
또 지금은 Pixel shader단에서 적용중이지만 Vertex shader에서 사용할 수도 있다.
다만 그 경우 훨씬 러프한 결과가 나오며, 때문에 대체적으로 specular highlighting은 pixel shader 단에서 사용된다.
'computer graphics > 3D Graphics' 카테고리의 다른 글
[3DGraphics] 20. Projection matrix (1) | 2022.10.11 |
---|---|
[3DGraphics] 19. Implementing Vec4, Mat4 (2) | 2022.10.11 |
[3DGraphics] 17. Phong shading & Per-pixel lighting (0) | 2022.09.09 |
[3DGraphics] 16. Point lights (0) | 2022.09.09 |
[3DGraphics] 15. Gouraud shading (0) | 2022.09.09 |