크기 4짜리 벡터와 4x4 행렬을 우리 프레임워크가 지원가능하도록 만들어보자.
더 자세히 들어가기 전에, 우리의 프레임워크는 row vector(가로로 긴 형태의 벡터)를 기본값으로 한다는 걸 예전 글에서도 언급한 적 있다.
(DirectX 또한 row vector를 기본값으로 한다 - https://nicoschertler.wordpress.com/2012/01/27/directx-and-matrices/)
때문에 행렬-벡터 연산 또한 이에 맞추어 계산된디. (실제로 아래 코드에서 .Translation() 함수를 살펴보면 우리가 흔히 아는 변환행렬의 꼴을 transpose한 모습, 즉 tx ty tz가 행렬 아랫부분에 위치하고 있음을 알 수 있다.)
왜 4x4를 지원해야 하는가에 대한 설명은 잠시 뒤로 미뤄두고, 일단 바로 코드를 살펴보자.
// Mat.h
/*
살펴볼만한 변경점들은 다음과 같다.
Mat은 이제 S라는 행렬의 크기를 생성 시 입력받으며, 이 값에 따라 동적으로 맞는 연산을 계산한다.
Mat.Identity()을 참고. (주의: 현재 프레임워크에서는 3x3 또는 4x4 행렬만은 지원한다)
이때 constexpr를 사용한 비교연산을 볼 수 있는데, 이는 컴파일 단에서 S값에 따라 필요한 부분만을 컴파일한다.
http://egloos.zum.com/sweeper/v/3203973
여기서 아주 중요한 부분이자 주의해야 할 부분이 있다.
일단 우리가 4 벡터를 사용하지만, 여전히 우리가 3차원 세계의 물체를 다룬다는 사실에는 변함이 없다.
실제로 벡터의 1,2,3번째 요소는 기존과 동일하게 활용되며, 4번째(w) 요소도 거의 대부분의 경우 1 값으로 고정될 것이다.
(왜 굳이 1을 넣어놓아야 하는지에 대한 것은 본 포스트 뒤에서 다룬다)
때문에 4-Vec이더라도 기존의 3Vec과 동일하게 곱셈, 덧셈, Transpose 등이 처리되야 하고,
때문에 이러한 연산을 처리할 때 w는 무시되어야 하는 경우가 많다. (거의 항상 1을 유지하기 때문에)
고로 코드를 살펴보면 w값은 애초에 변화를 주지 않도록 코드가 짜여있다는 것을 알 수 있다.
일례로 .Scailing()함수를 살펴보면
else if constexpr( S == 4 )
{
return {
factor,(T)0.0,(T)0.0,(T)0.0,
(T)0.0,factor,(T)0.0,(T)0.0,
(T)0.0,(T)0.0,factor,(T)0.0,
(T)0.0,(T)0.0,(T)0.0,(T)1.0
};
}
즉 w값을 수정하지 않음을 알 수 있다. (즉 실질적으로 3Mat와 동일한 연산이다)
.Translation, .RotationX()함수도 마찬가지다.
*/
#pragma once
#include "Vec3.h"
#include "Vec4.h"
template <typename T,size_t S>
class _Mat
{
public:
_Mat& operator=( const _Mat& rhs )
{
memcpy( elements,rhs.elements,sizeof( elements ) );
return *this;
}
_Mat& operator*=( T rhs )
{
for( auto& row : elements )
{
for( T& e : row )
{
e *= rhs;
}
}
return *this;
}
_Mat operator*( T rhs ) const
{
_Mat result = *this;
return result *= rhs;
}
_Mat& operator*=( const _Mat& rhs )
{
return *this = *this * rhs;
}
_Mat operator*( const _Mat& rhs ) const
{
_Mat result;
for( size_t j = 0; j < S; j++ )
{
for( size_t k = 0; k < S; k++ )
{
T sum = (T)0.0;
for( size_t i = 0; i < S; i++ )
{
sum += elements[j][i] * rhs.elements[i][k];
}
result.elements[j][k] = sum;
}
}
return result;
}
static _Mat Identity()
{
if constexpr( S == 3 )
{
return {
(T)1.0,(T)0.0,(T)0.0,
(T)0.0,(T)1.0,(T)0.0,
(T)0.0,(T)0.0,(T)1.0
};
}
else if constexpr( S == 4 )
{
return {
(T)1.0,(T)0.0,(T)0.0,(T)0.0,
(T)0.0,(T)1.0,(T)0.0,(T)0.0,
(T)0.0,(T)0.0,(T)1.0,(T)0.0,
(T)0.0,(T)0.0,(T)0.0,(T)1.0
};
}
else
{
static_assert(false,"Bad dimensionality");
}
}
static _Mat Scaling( T factor )
{
if constexpr( S == 3 )
{
return{
factor,(T)0.0,(T)0.0,
(T)0.0,factor,(T)0.0,
(T)0.0,(T)0.0,factor
};
}
else if constexpr( S == 4 )
{
return {
factor,(T)0.0,(T)0.0,(T)0.0,
(T)0.0,factor,(T)0.0,(T)0.0,
(T)0.0,(T)0.0,factor,(T)0.0,
(T)0.0,(T)0.0,(T)0.0,(T)1.0
};
}
else
{
static_assert(false,"Bad dimensionality");
}
}
static _Mat RotationZ( T theta )
{
const T sinTheta = sin( theta );
const T cosTheta = cos( theta );
if constexpr( S == 3 )
{
return{
cosTheta, sinTheta, (T)0.0,
-sinTheta, cosTheta, (T)0.0,
(T)0.0, (T)0.0, (T)1.0
};
}
else if constexpr( S == 4 )
{
return {
cosTheta, sinTheta, (T)0.0,(T)0.0,
-sinTheta, cosTheta, (T)0.0,(T)0.0,
(T)0.0, (T)0.0, (T)1.0,(T)0.0,
(T)0.0, (T)0.0, (T)0.0,(T)1.0
};
}
else
{
static_assert(false,"Bad dimensionality");
}
}
static _Mat RotationY( T theta )
{
const T sinTheta = sin( theta );
const T cosTheta = cos( theta );
if constexpr( S == 3 )
{
return{
cosTheta, (T)0.0,-sinTheta,
(T)0.0, (T)1.0, (T)0.0,
sinTheta, (T)0.0, cosTheta
};
}
else if constexpr( S == 4 )
{
return {
cosTheta, (T)0.0, -sinTheta,(T)0.0,
(T)0.0, (T)1.0, (T)0.0, (T)0.0,
sinTheta, (T)0.0, cosTheta, (T)0.0,
(T)0.0, (T)0.0, (T)0.0, (T)1.0
};
}
else
{
static_assert(false,"Bad dimensionality");
}
}
static _Mat RotationX( T theta )
{
const T sinTheta = sin( theta );
const T cosTheta = cos( theta );
if constexpr( S == 3 )
{
return{
(T)1.0, (T)0.0, (T)0.0,
(T)0.0, cosTheta, sinTheta,
(T)0.0,-sinTheta, cosTheta,
};
}
else if constexpr( S == 4 )
{
return {
(T)1.0, (T)0.0, (T)0.0, (T)0.0,
(T)0.0, cosTheta, sinTheta,(T)0.0,
(T)0.0,-sinTheta, cosTheta,(T)0.0,
(T)0.0, (T)0.0, (T)0.0, (T)1.0
};
}
else
{
static_assert(false,"Bad dimensionality");
}
}
template<class V>
static _Mat Translation( const V& tl )
{
return Translation( tl.x,tl.y,tl.z );
}
static _Mat Translation( T x,T y,T z )
{
if constexpr( S == 4 )
{
return {
(T)1.0,(T)0.0,(T)0.0,(T)0.0,
(T)0.0,(T)1.0,(T)0.0,(T)0.0,
(T)0.0,(T)0.0,(T)1.0,(T)0.0,
x, y, z, (T)1.0
};
}
else
{
static_assert(false,"Bad dimensionality");
}
}
public:
// [ row ][ col ]
T elements[S][S];
};
template<typename T>
_Vec3<T>& operator*=( _Vec3<T>& lhs,const _Mat<T,3>& rhs )
{
return lhs = lhs * rhs;
}
template<typename T>
_Vec3<T> operator*( const _Vec3<T>& lhs,const _Mat<T,3>& rhs )
{
return{
lhs.x * rhs.elements[0][0] + lhs.y * rhs.elements[1][0] + lhs.z * rhs.elements[2][0],
lhs.x * rhs.elements[0][1] + lhs.y * rhs.elements[1][1] + lhs.z * rhs.elements[2][1],
lhs.x * rhs.elements[0][2] + lhs.y * rhs.elements[1][2] + lhs.z * rhs.elements[2][2]
};
}
template<typename T>
_Vec4<T>& operator*=( _Vec4<T>& lhs,const _Mat<T,4>& rhs )
{
return lhs = lhs * rhs;
}
template<typename T>
_Vec4<T> operator*( const _Vec4<T>& lhs,const _Mat<T,4>& rhs )
{
return{
lhs.x * rhs.elements[0][0] + lhs.y * rhs.elements[1][0] + lhs.z * rhs.elements[2][0] + lhs.w * rhs.elements[3][0],
lhs.x * rhs.elements[0][1] + lhs.y * rhs.elements[1][1] + lhs.z * rhs.elements[2][1] + lhs.w * rhs.elements[3][1],
lhs.x * rhs.elements[0][2] + lhs.y * rhs.elements[1][2] + lhs.z * rhs.elements[2][2] + lhs.w * rhs.elements[3][2],
lhs.x * rhs.elements[0][3] + lhs.y * rhs.elements[1][3] + lhs.z * rhs.elements[2][3] + lhs.w * rhs.elements[3][3]
};
}
typedef _Mat<float,3> Mat3;
typedef _Mat<double,3> Mad3;
typedef _Mat<float,4> Mat4;
typedef _Mat<double,4> Mad4;
이번에는 Vec4구현을 살펴보자.
행렬과 달리 Vec2, 3, 4가 각각 다른 헤더에 저장되어있는데, 이는 Vec4에서 사용할 수 없는(정확히는 의미가 모호한) 함수가 Vec2,3에 있기 때문이다.
또 Vec3 + float 하나로 Vec4를 생성할 수 있게 생성자를 오버로딩한 것도 확인할 수 있다.
// Vec4.h
#pragma once
#include <algorithm>
#include "ChiliMath.h"
#include "Vec3.h"
template <typename T>
class _Vec4 : public _Vec3<T>
{
public:
_Vec4() = default;
_Vec4( T x,T y,T z,T w )
:
_Vec3( x,y,z ),
w( w )
{}
_Vec4( const _Vec3& v3,float w = 1.0f )
:
_Vec3( v3 ),
w( w )
{}
template <typename T2>
explicit operator _Vec4<T2>() const
{
return{ (T2)x,(T2)y,(T2)z,(T2)w };
}
/*
기본적으로 Vec3를 기반으로 작성되었으나 함수 다량이 삭제되었다. (주석처리)
Vec4의 길이나 normalize된 vec4는 (최소한 3D 프로그래밍에서는) 큰 의미가 없기에 구현하지 않는다.
아래 등장하는 내적이나 외적 또한 마찬가지이다.
그럼에도 덧셈, 곱셈등은 제대로 구현되어 있는데, 색상 및 space interpolation이 제대로 처리되기 위해 필수적이기 때문이다.
*/
//T LenSq() const
//{
// return sq( *this );
//}
//T Len() const
//{
// return sqrt( LenSq() );
//}
//_Vec3& Normalize()
//{
// const T length = Len();
// x /= length;
// y /= length;
// z /= length;
// return *this;
//}
//_Vec3 GetNormalized() const
//{
// _Vec3 norm = *this;
// norm.Normalize();
// return norm;
//}
_Vec4 operator-() const
{
return _Vec4( -x,-y,-z,-w );
}
_Vec4& operator=( const _Vec4 &rhs )
{
x = rhs.x;
y = rhs.y;
z = rhs.z;
w = rhs.w;
return *this;
}
_Vec4& operator+=( const _Vec4 &rhs )
{
x += rhs.x;
y += rhs.y;
z += rhs.z;
w += rhs.w;
return *this;
}
_Vec4& operator-=( const _Vec4 &rhs )
{
x -= rhs.x;
y -= rhs.y;
z -= rhs.z;
w -= rhs.w;
return *this;
}
//T operator*( const _Vec4 &rhs ) const
//{
// return x * rhs.x + y * rhs.y + z * rhs.z;
//}
_Vec4 operator+( const _Vec4 &rhs ) const
{
return _Vec4( *this ) += rhs;
}
_Vec4 operator-( const _Vec4 &rhs ) const
{
return _Vec4( *this ) -= rhs;
}
_Vec4& operator*=( const T &rhs )
{
x *= rhs;
y *= rhs;
z *= rhs;
w *= rhs;
return *this;
}
_Vec4 operator*( const T &rhs ) const
{
return _Vec4( *this ) *= rhs;
}
//_Vec4 operator%( const _Vec4& rhs ) const
//{
// return _Vec4(
// y * rhs.z - z * rhs.y,
// z * rhs.x - x * rhs.z,
// x * rhs.y - y * rhs.x );
//}
_Vec4& operator/=( const T &rhs )
{
x /= rhs;
y /= rhs;
z /= rhs;
w /= rhs;
return *this;
}
_Vec4 operator/( const T &rhs ) const
{
return _Vec4( *this ) /= rhs;
}
bool operator==( const _Vec4 &rhs ) const
{
return x == rhs.x && y == rhs.y && rhs.z == z && rhs.w == w;
}
bool operator!=( const _Vec4 &rhs ) const
{
return !(*this == rhs);
}
// clamp to between 0.0 ~ 1.0
_Vec4& Saturate()
{
x = std::min( 1.0f,std::max( 0.0f,x ) );
y = std::min( 1.0f,std::max( 0.0f,y ) );
z = std::min( 1.0f,std::max( 0.0f,z ) );
w = std::min( 1.0f,std::max( 0.0f,w ) );
return *this;
}
// clamp to between 0.0 ~ 1.0
_Vec4 GetSaturated() const
{
_Vec4 temp( *this );
temp.Saturate();
return temp;
}
// x3 = x1 * x2 etc.
_Vec4& Hadamard( const _Vec4& rhs )
{
x *= rhs.x;
y *= rhs.y;
z *= rhs.z;
w *= rhs.w;
return *this;
}
// x3 = x1 * x2 etc.
_Vec4 GetHadamard( const _Vec4& rhs ) const
{
_Vec4 temp( *this );
temp.Hadamard( rhs );
return temp;
}
public:
T w;
};
typedef _Vec4<float> Vec4;
typedef _Vec4<double> Ved4;
typedef _Vec4<int> Vei4;
프레임워크 전반의 변경사항을 살펴보자.
Vec4로 변경한다고 한들 생각 외로 큰 변화는 없다는 것을 알 수 있다.
단 기존 scene들이 사용불가능하게 되었다는 것에는 유의.
가장 중요한 변경사항은 셰이더의 정점 처리에서 rotation, position에 따라 rotate와 translate를 해주는 부분이다.
기존에는 BindRotation(), BindTranslation() 으로 Rotation matrix와 translation 해줘야 하는 만큼의 Vec3를 입력받아 이를 셰이더에 저장하고, 셰이더가 이를 각 정점에 곱하고 더해 카메라에 대한 정점의 상대적 위치 변화를 처리했다면,
이제는 이 과정을 하나로 통합한다.
Translation과 Rotation 행렬을 하나의 Transform 행렬로 묶어 Transform matrix만을 곱해버리는 것으로 변경해 더 깔끔하다.
또 기존에 normal을 반환하는 것과 그대로 v.n의 값을 normal로써 반환하는데, 이제 Vec4를 사용하는 만큼 Vec4의 형태로 반환한다.
이때 아주 중요한 점은 노멀 Vec4의 w는 0이라는 점이다. 대부분의 Vec4의 w가 1인 것과 상반된다.
w가 0이면 행렬 연산을 할 때 translation 연산만을 무시할 수 있다!
노멀벡터는 오직 방향만을 나타내는 벡터이기 때문에 translation의 영향을 받아 xyz가 더해지거나 하면 안되는데, w가 0이면 translation matrix를 곱해도 xyz에 아무런 변화가 없음을 알 수 있다!
이게 우리가 vec4를 사용하는 이유 중 하나이자, 기존 프레임워크에서 translation matrix 대신 vec3를 사용한 이유이기도 하다.
코드를 살펴보자.
// SpecularPhongPointEffect.h
// ...
public:
void BindTransformation( const Mat4& transformation_in )
{
transformation = transformation_in;
}
Output operator()( const Vertex& v ) const
{
const auto pt = Vec4( v.pos ) * transformation;
return{ pt,Vec4( v.n,0.0f ) * transformation,pt };
}
// ...
// SpecularPhongPointScene.h
virtual void Draw() override
{
pipeline.BeginFrame();
// set pipeline transform
pipeline.effect.vs.BindTransformation(
Mat4::RotationX( theta_x ) *
Mat4::RotationY( theta_y ) *
Mat4::RotationZ( theta_z ) *
Mat4::Translation( 0.0f,0.0f,offset_z )
); // 이제 행렬을 바인딩하는 것을 볼 수 있다.
pipeline.effect.ps.SetLightPosition( { lpos_x,lpos_y,lpos_z } );
// render triangles
pipeline.Draw( itlist );
// draw light indicator with different pipeline
// don't call beginframe on this pipeline b/c wanna keep zbuffer contents
// (don't like this assymetry but we'll live with it for now)
liPipeline.effect.vs.BindTranslation( { lpos_x,lpos_y,lpos_z } );
liPipeline.effect.vs.BindRotation( Mat3::Identity() );
liPipeline.Draw( lightIndicator );
}
실행해보면 잘 작동하는 것을 볼 수 있다.
전과 동일하게 잘 작동한다.
'computer graphics > 3D Graphics' 카테고리의 다른 글
[3DGraphics] 21. Clipping (1) | 2022.10.28 |
---|---|
[3DGraphics] 20. Projection matrix (1) | 2022.10.11 |
[3DGraphics] 18. Specular highlights (1) | 2022.10.11 |
[3DGraphics] 17. Phong shading & Per-pixel lighting (0) | 2022.09.09 |
[3DGraphics] 16. Point lights (0) | 2022.09.09 |