手把手教你架構3D引擎高階篇系列六
前面給讀者介紹了雜湊表的封裝,其實我們實現的是資料結構演算法,只是把基本的資料結構演算法進行了一個適合引擎的封裝。引擎中使用了大量的數學知識,下面我們開始封裝向量,矩陣運算。C++為我們提供了Vector3和Matrix的封裝,但是引擎在這塊為了擴充套件方便還是需要自己封裝的,比如Ogre,Unity,UE4以及一些大公司自研引擎都是自己封裝。
向量
向量在遊戲中用的非常多,計算兩個物體之間的距離,一個物體朝另一個物體移動等等,都會用用到向量的運算,向量運算包括:向量相加,向量相減,向量點乘,向量叉乘,向量長度,向量單位化等等。向量有二維,三維,四維向量之分,引擎都需要逐一封裝,向量的封裝沒有什麼特殊的地方,隨便在網上一搜索就是一大堆,這裡給讀者把程式碼分享一二。
void Vec3::normalize() { float x = this->x; float y = this->y; float z = this->z; float inv_len = 1 / sqrt(x * x + y * y + z * z); x *= inv_len; y *= inv_len; z *= inv_len; this->x = x; this->y = y; this->z = z; } Vec3 Vec3::normalized() const { float x = this->x; float y = this->y; float z = this->z; float inv_len = 1 / sqrt(x * x + y * y + z * z); x *= inv_len; y *= inv_len; z *= inv_len; return Vec3(x, y, z); } float Vec3::length() const { float x = this->x; float y = this->y; float z = this->z; return sqrt(x * x + y * y + z * z); }
以上是計算向量的單位化以及向量長度,再看看矩陣的封裝:
矩陣
矩陣運算用的最多,模型的位移,旋轉,縮放等都與矩陣的變換相關,矩陣有相乘,單位化,旋轉,轉置以及與尤拉角的轉換等運算。
void Matrix::fromEuler(float yaw, float pitch, float roll) { float sroll = sinf(roll); float croll = cosf(roll); float spitch = sinf(pitch); float cpitch = cosf(pitch); float syaw = sinf(yaw); float cyaw = cosf(yaw); m11 = sroll * spitch * syaw + croll * cyaw; m12 = sroll * cpitch; m13 = sroll * spitch * cyaw - croll * syaw; m14 = 0.0f; m21 = croll * spitch * syaw - sroll * cyaw; m22 = croll * cpitch; m23 = croll * spitch * cyaw + sroll * syaw; m24 = 0.0f; m31 = cpitch * syaw; m32 = -spitch; m33 = cpitch * cyaw; m34 = 0.0f; m41 = 0.0f; m42 = 0.0f; m43 = 0.0f; m44 = 1.0f; } Matrix Matrix::rotationX(float angle) { Matrix m = IDENTITY; float c = cosf(angle); float s = sinf(angle); m.m22 = m.m33 = c; m.m32 = -s; m.m23 = s; return m; } Matrix Matrix::rotationY(float angle) { Matrix m = IDENTITY; float c = cosf(angle); float s = sinf(angle); m.m11 = m.m33 = c; m.m31 = s; m.m13 = -s; return m; } Matrix Matrix::rotationZ(float angle) { Matrix m = IDENTITY; float c = cosf(angle); float s = sinf(angle); m.m11 = m.m22 = c; m.m21 = -s; m.m12 = s; return m; }
除了向量和矩陣的封裝,我們還會封裝一些常用數學公式如下所示:
Math
數學常用的演算法很多,比如射判斷線與平面相交,射線與球體相交,射線與立方體相交等等,角度與弧度的相互轉化等等。封裝部分程式碼如下所示:
Vec3 degreesToRadians(const Vec3& v)
{
return Vec3(degreesToRadians(v.x), degreesToRadians(v.y), degreesToRadians(v.z));
}
Vec3 radiansToDegrees(const Vec3& v)
{
return Vec3(radiansToDegrees(v.x), radiansToDegrees(v.y), radiansToDegrees(v.z));
}
float angleDiff(float a, float b)
{
float delta = a - b;
delta = fmodf(delta, PI * 2);
if (delta > PI) return -PI * 2 + delta;
if (delta < -PI) return PI * 2 + delta;
return delta;
}
bool getRayPlaneIntersecion(const Vec3& origin,
const Vec3& dir,
const Vec3& plane_point,
const Vec3& normal,
float& out)
{
float d = dotProduct(dir, normal);
if (d == 0)
{
return false;
}
d = dotProduct(plane_point - origin, normal) / d;
out = d;
return true;
}
bool getRaySphereIntersection(const Vec3& origin,
const Vec3& dir,
const Vec3& center,
float radius,
Vec3& out)
{
ASSERT(dir.length() < 1.01f && dir.length() > 0.99f);
Vec3 L = center - origin;
float tca = dotProduct(L, dir);
if (tca < 0) return false;
float d2 = dotProduct(L, L) - tca * tca;
if (d2 > radius * radius) return false;
float thc = sqrt(radius * radius - d2);
float t0 = tca - thc;
out = origin + dir * t0;
return true;
}
bool getRayAABBIntersection(const Vec3& origin,
const Vec3& dir,
const Vec3& min,
const Vec3& size,
Vec3& out)
{
Vec3 dirfrac;
dirfrac.x = 1.0f / (dir.x == 0 ? 0.00000001f : dir.x);
dirfrac.y = 1.0f / (dir.y == 0 ? 0.00000001f : dir.y);
dirfrac.z = 1.0f / (dir.z == 0 ? 0.00000001f : dir.z);
Vec3 max = min + size;
float t1 = (min.x - origin.x) * dirfrac.x;
float t2 = (max.x - origin.x) * dirfrac.x;
float t3 = (min.y - origin.y) * dirfrac.y;
float t4 = (max.y - origin.y) * dirfrac.y;
float t5 = (min.z - origin.z) * dirfrac.z;
float t6 = (max.z - origin.z) * dirfrac.z;
float tmin = Math::maximum(
Math::maximum(Math::minimum(t1, t2), Math::minimum(t3, t4)), Math::minimum(t5, t6));
float tmax = Math::minimum(
Math::minimum(Math::maximum(t1, t2), Math::maximum(t3, t4)), Math::maximum(t5, t6));
if (tmax < 0)
{
return false;
}
if (tmin > tmax)
{
return false;
}
out = tmin < 0 ? origin : origin + dir * tmin;
return true;
}
以上程式碼封裝了射線與球體,AABB相交的判斷,球體和AABB我們自己也要定義的,這也是我們說的幾何封裝。
geometry
Geometry主要的功能包括:碰撞體之間的檢測,設定視錐體,透視視錐體,正交視錐體的計算,每個視錐體有六個面,比如我們說的透視視錐體,前,後,左,右,上下;其中前後就是我們通常說的原裁剪面和近裁剪面等等。核心程式碼如下所示:
void Frustum::computePerspective(const Vec3& position,
const Vec3& direction,
const Vec3& up,
float fov,
float ratio,
float near_distance,
float far_distance,
const Vec2& viewport_min,
const Vec2& viewport_max)
{
ASSERT(near_distance > 0);
ASSERT(far_distance > 0);
ASSERT(near_distance < far_distance);
ASSERT(fov > 0);
ASSERT(ratio > 0);
float scale = (float)tan(fov * 0.5f);
Vec3 right = crossProduct(direction, up);
Vec3 up_near = up * near_distance * scale;
Vec3 right_near = right * (near_distance * scale * ratio);
Vec3 up_far = up * far_distance * scale;
Vec3 right_far = right * (far_distance * scale * ratio);
Vec3 z = direction.normalized();
Vec3 near_center = position + z * near_distance;
Vec3 far_center = position + z * far_distance;
setPoints(*this, near_center, far_center, right_near, up_near, right_far, up_far, viewport_min, viewport_max);
}
void Frustum::computePerspective(const Vec3& position,
const Vec3& direction,
const Vec3& up,
float fov,
float ratio,
float near_distance,
float far_distance)
{
computePerspective(position, direction, up, fov, ratio, near_distance, far_distance, {-1, -1}, {1, 1});
}
void Frustum::computeOrtho(const Vec3& position,
const Vec3& direction,
const Vec3& up,
float width,
float height,
float near_distance,
float far_distance,
const Vec2& viewport_min,
const Vec2& viewport_max)
{
Vec3 z = direction;
z.normalize();
Vec3 near_center = position - z * near_distance;
Vec3 far_center = position - z * far_distance;
Vec3 x = crossProduct(up, z).normalized() * width;
Vec3 y = crossProduct(z, x).normalized() * height;
setPoints(*this, near_center, far_center, x, y, x, y, viewport_min, viewport_max);
}
我們以前都是直接使用引擎為我們提供的攝像機,通過編輯器可以直接拖拽到場景中,其實它在引擎內部也是這麼實現的,只是對開發者來說是黑盒子而已。以上是我們引擎封裝的功能,這些都是底層的數學運算。