1. 程式人生 > >手把手教你架構3D引擎高階篇系列六

手把手教你架構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);
}

我們以前都是直接使用引擎為我們提供的攝像機,通過編輯器可以直接拖拽到場景中,其實它在引擎內部也是這麼實現的,只是對開發者來說是黑盒子而已。以上是我們引擎封裝的功能,這些都是底層的數學運算。