我目前正在使用贝塞尔曲线和曲面来绘制著名的犹他州茶壶。使用 16 个控制点的 Bezier 补丁,我已经能够绘制茶壶并使用“世界到相机”功能显示它,该功能能够旋转生成的茶壶,并且目前正在使用正交投影。
结果是我有一个“扁平”茶壶,这是预期的,因为正投影的目的是保留平行线。
但是,我想使用透视投影来赋予茶壶深度。我的问题是,如何获取从“世界到相机”函数返回的 3D xyz 顶点,并将其转换为 2D 坐标。我想在 z=0 处使用投影平面,并允许用户使用键盘上的箭头键确定焦距和图像大小。
我正在用 java 编程并设置了所有输入事件处理程序,并且还编写了一个处理基本矩阵乘法的矩阵类。我已经阅读维基百科和其他资源有一段时间了,但我不太了解人们如何执行这种转换。
我看到这个问题有点老了,但我还是决定给那些通过搜索找到这个问题的人一个答案。
现在表示 2D/3D 变换的标准方法是使用齐次坐标。[x,y,w]用于 2D,而[x,y,z,w]用于 3D。由于您在 3D 和平移中具有三个轴,因此该信息非常适合 4x4 变换矩阵。我将在本说明中使用列主矩阵表示法。除非另有说明,否则所有矩阵均为 4x4。
从 3D 点到光栅化点、线或多边形的阶段如下所示:
此阶段是实际投影,因为 z 不再用作位置中的组件。
算法:
计算视场
这将计算视场。tan 取弧度还是度数无关紧要,但角度必须匹配。请注意,当角度接近 180 度时,结果将达到无穷大。这是一个奇点,因为不可能有那么宽的焦点。如果您想要数值稳定性,请保持角度小于或等于 179 度。
fov = 1.0 / tan(angle/2.0)
还要注意 1.0 / tan(45) = 1。这里的其他人建议只除以 z。这里的结果很明显。您将获得 90 度 FOV 和 1:1 的纵横比。像这样使用齐次坐标还有其他几个优点;例如,我们可以对近平面和远平面执行裁剪,而不将其视为特殊情况。
裁剪矩阵的计算
这是剪辑矩阵的布局。纵横比是宽度/高度。所以 x 分量的 FOV 是基于 y 的 FOV 缩放的。远和近是系数,它们是近剪裁平面和远剪裁平面的距离。
[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){} /* Assume proper operator overloads here, with vectors and scalars */ 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; } /* The interesting stuff */ 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; /* this 'plugs' the old z into w */ 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); /* Here, after the perspective divide, you perform Sutherland-Hodgeman clipping by checking if the x, y and z components are inside the range of [-w, w]. One checks each vector component seperately against each plane. Per-vertex data like colours, normals and texture coordinates need to be linearly interpolated for clipped edges to reflect the change. If the edge (v0,v1) is tested against the positive x plane, and v1 is outside, the interpolant becomes: (v1.x - w) / (v1.x - v0.x) I skip this stage all together to be brief. */ for(VecArr::iterator i=vertex.begin(); i!=vertex.end(); ++i){ v = (*i) * clipMatrix; v /= v.w; /* Don't get confused here. I assume the divide leaves v.w alone.*/ dst.push_back(v); } /* TODO: Clipping here */ 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 规范对于所涉及的数学是一个非常好的参考。http://www.devmaster.net/ 上的 DevMaster 论坛也有很多与软件光栅化器相关的好文章。