前面给读者介绍了哈希表的封装,其实我们实现的是数据结构算法,只是把基本的数据结构算法进行了一个适合引擎的封装。引擎中使用了大量的数学知识,下面我们开始封装向量,矩阵运算。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;
}
除了向量和矩阵的封装,我们还会封装一些常用数学公式如下所示:
数学常用的算法很多,比如射判断线与平面相交,射线与球体相交,射线与立方体相交等等,角度与弧度的相互转化等等。封装部分代码如下所示:
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主要的功能包括:碰撞体之间的检测,设置视锥体,透视视锥体,正交视锥体的计算,每个视锥体有六个面,比如我们说的透视视锥体,前,后,左,右,上下;其中前后就是我们通常说的原裁剪面和近裁剪面等等。核心代码如下所示:
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);
}
我们以前都是直接使用引擎为我们提供的摄像机,通过编辑器可以直接拖拽到场景中,其实它在引擎内部也是这么实现的,只是对开发者来说是黑盒子而已。以上是我们引擎封装的功能,这些都是底层的数学运算。