现在表示2D / 3D变换的标准方法是使用齐次坐标。 2D为[x,y,w],3D为[x,y,z,w]。由于在3D中有三个轴以及平移,因此该信息完美地适合4x4变换矩阵中。在本说明中,我将使用列主要矩阵符号。除非另有说明,否则所有矩阵均为4x4。
从3D点到光栅化的点,线或多边形的阶段如下:
- 使用逆相机矩阵转换您的3D点,然后进行任何所需的变换。如果有表面法线,请将它们转换为零设置的w,因为您不想平移法线。您用于转换法线的矩阵必须是各向同性的;缩放和剪切会使法线畸形。
- 使用裁剪空间矩阵转换点。该矩阵使用视场角和宽高比缩放x和y,使用近和远裁剪平面缩放z,并将“旧”z插入w。变换后,应通过w除以x、y和z。这称为透视除法。
- 现在,您的顶点位于裁剪空间中,您希望执行裁剪,以便不渲染视口范围外的任何像素。Sutherland-Hodgeman裁剪是最广泛使用的裁剪算法。
- 相对于w和半宽度和半高度转换x和y。现在,您的x和y坐标位于视口坐标中。舍弃w,但通常保存1/w和z,因为需要1/w才能在多边形表面上进行透视校正插值,并且z存储在z缓冲区中并用于深度测试。
这个阶段是实际的投影,因为z不再用作位置的组成部分。
算法:
计算视场角
这个计算视野的范围。tan函数使用弧度或角度均可,但是angle必须匹配。请注意,当angle接近180度时,结果会趋近于无穷大。这是一个奇点,因为不可能有如此宽的焦点。如果想要数值稳定性,请将angle保持小于等于179度。
fov = 1.0 / tan(angle/2.0)
请注意,1.0 / tan(45) = 1。这里有人建议只需除以z。结果很清楚。您将获得90度的视场角和1:1的宽高比。像这样使用齐次坐标还有其他几个优点;例如,我们可以执行针对近平面和远平面的裁剪而不将其视为特殊情况。
计算剪辑矩阵
这是剪辑矩阵的布局。aspectRatio是宽度/高度。因此,x分量的FOV基于y分量的FOV进行缩放。Far和near是系数,它们是近和远裁剪平面的距离。
[fov * aspectRatio][ 0 ][ 0 ][ 0 ]
[ 0 ][ fov ][ 0 ][ 0 ]
[ 0 ][ 0 ][(far+near)/(far-near) ][ 1 ]
[ 0 ][ 0 ][(2*near*far)/(near-far)][ 0 ]
屏幕投影
剪裁之后,这是获取屏幕坐标的最终转换。
new_x = (x * Width ) / (2.0 * w) + halfWidth;
new_y = (y * Height) / (2.0 * w) + halfHeight;
C++中的简单示例实现
#include <vector>
#include <cmath>
#include <stdexcept>
#include <algorithm>
struct Vector
{
Vector() : x(0),y(0),z(0),w(1){}
Vector(float a, float b, float c) : x(a),y(b),z(c),w(1){}
float Length() const
{
return std::sqrt(x*x + y*y + z*z);
}
Vector Unit() const
{
const float epsilon = 1e-6;
float mag = Length();
if(mag < epsilon){
std::out_of_range e("");
throw e;
}
return *this / mag;
}
};
inline float Dot(const Vector& v1, const Vector& v2)
{
return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
}
class Matrix
{
public:
Matrix() : data(16)
{
Identity();
}
void Identity()
{
std::fill(data.begin(), data.end(), float(0));
data[0] = data[5] = data[10] = data[15] = 1.0f;
}
float& operator[](size_t index)
{
if(index >= 16){
std::out_of_range e("");
throw e;
}
return data[index];
}
Matrix operator*(const Matrix& m) const
{
Matrix dst;
int col;
for(int y=0; y<4; ++y){
col = y*4;
for(int x=0; x<4; ++x){
for(int i=0; i<4; ++i){
dst[x+col] += m[i+col]*data[x+i*4];
}
}
}
return dst;
}
Matrix& operator*=(const Matrix& m)
{
*this = (*this) * m;
return *this;
}
void SetupClipMatrix(float fov, float aspectRatio, float near, float far)
{
Identity();
float f = 1.0f / std::tan(fov * 0.5f);
data[0] = f*aspectRatio;
data[5] = f;
data[10] = (far+near) / (far-near);
data[11] = 1.0f;
data[14] = (2.0f*near*far) / (near-far);
data[15] = 0.0f;
}
std::vector<float> data;
};
inline Vector operator*(const Vector& v, const Matrix& m)
{
Vector dst;
dst.x = v.x*m[0] + v.y*m[4] + v.z*m[8 ] + v.w*m[12];
dst.y = v.x*m[1] + v.y*m[5] + v.z*m[9 ] + v.w*m[13];
dst.z = v.x*m[2] + v.y*m[6] + v.z*m[10] + v.w*m[14];
dst.w = v.x*m[3] + v.y*m[7] + v.z*m[11] + v.w*m[15];
return dst;
}
typedef std::vector<Vector> VecArr;
VecArr ProjectAndClip(int width, int height, float near, float far, const VecArr& vertex)
{
float halfWidth = (float)width * 0.5f;
float halfHeight = (float)height * 0.5f;
float aspect = (float)width / (float)height;
Vector v;
Matrix clipMatrix;
VecArr dst;
clipMatrix.SetupClipMatrix(60.0f * (M_PI / 180.0f), aspect, near, far);
for(VecArr::iterator i=vertex.begin(); i!=vertex.end(); ++i){
v = (*i) * clipMatrix;
v /= v.w;
dst.push_back(v);
}
for(VecArr::iterator i=dst.begin(); i!=dst.end(); ++i){
i->x = (i->x * (float)width) / (2.0f * i->w) + halfWidth;
i->y = (i->y * (float)height) / (2.0f * i->w) + halfHeight;
}
return dst;
}
如果你还在思考这个问题,OpenGL规范是涉及数学的一个非常好的参考。Devmaster论坛
http://www.devmaster.net/上有很多与软件光栅化器相关的好文章。