透视投影
在之前的渲染过程中,我们在渲染过程中只是简单的省略掉z坐标轴,完成了正交渲染,而为了更好的呈现渲染效果,我们需要完成透视投影中完成渲染。
二维平面
线性变换
平面上的线性变换可以用相应的矩阵来表示。如果我们取一个点(x,y),那么它的变换可以写成如下:
[
a
b
c
d
]
[
x
y
]
=
[
a
x
+
b
y
c
x
+
d
y
]
\left[\begin{array}{l} a&b \\ c&d \end{array}\right] \left[\begin{array}{l} x \\ y \end{array}\right] =\left[\begin{array}{l} ax + by \\ cx+dy \end{array}\right]
[acbd][xy]=[ax+bycx+dy]
其中最简单的就是变换就是单位变换,它不移动任何点:
[
1
0
0
1
]
[
x
y
]
=
[
x
y
]
\left[\begin{array}{l} 1&0 \\ 0&1 \end{array}\right] \left[\begin{array}{l} x \\ y \end{array}\right] =\left[\begin{array}{l} x \\ y \end{array}\right]
[1001][xy]=[xy]
如果我们只改变对角线上矩阵的元素,代表对原物体进行缩放:
[
3
2
0
0
3
2
]
[
x
y
]
=
[
3
2
x
3
2
y
]
\left[\begin{array}{l} \frac{3}{2}&0 \\ 0&\frac{3}{2} \end{array}\right] \left[\begin{array}{l} x \\ y \end{array}\right] =\left[\begin{array}{l} \frac{3}{2}x \\ \frac{3}{2}y \end{array}\right]
[230023][xy]=[23x23y]
使用矩阵,我们可以将对象的变换直接转换为矩阵的乘法:
[
3
2
0
0
3
2
]
[
−
1
1
1
0
−
1
−
1
−
1
0
1
1
]
=
[
−
3
2
3
2
3
2
0
−
3
2
−
3
2
−
3
2
0
3
2
3
2
]
\left[\begin{array}{l} \frac{3}{2}&0 \\ 0&\frac{3}{2} \end{array}\right] \left[\begin{array}{l} -1&1&1&0&-1 \\ -1&-1&0&1&1 \end{array}\right] =\left[\begin{array}{l} -\frac{3}{2}&\frac{3}{2}&\frac{3}{2}&0&-\frac{3}{2} \\ -\frac{3}{2}&-\frac{3}{2}&0&\frac{3}{2}&\frac{3}{2} \end{array}\right]
[230023][−1−11−11001−11]=[−23−2323−23230023−2323]
上式右侧矩阵是正方体对象的顶点,我们取一个数组,乘上变换矩阵,得到变换后的对象。
如果我们对左侧矩阵中除对角线元素进行改变时,得到的图形将会如何变换呢?对于下面的变换,结果如下:
[
1
1
3
0
1
]
[
x
y
]
=
[
x
+
y
3
y
]
\left[\begin{array}{l} 1&\frac{1}{3} \\ 0&1 \end{array}\right] \left[\begin{array}{l} x \\ y \end{array}\right]= \left[\begin{array}{l} x + \frac{y}{3} \\ \ \ \ \ y \end{array}\right]
[10311][xy]=[x+3y y]
这时切变,即垂直度坐标不变(y坐标不变),最下边的偏移量为0,当y = 1时,最上边的偏移量为 1 3 \frac{1}{3} 31,事实证明,任何旋转(围绕原点)都可以表示为三个剪切的合成动作,这里白色物体被转换为红色物体,然后转换为绿色物体,最后转换为蓝色物体:
为了简化上面的旋转过程,我们可以直接给出旋转矩阵:
[
c
o
s
(
α
)
−
s
i
n
(
α
)
s
i
n
(
α
)
−
c
o
s
(
α
)
]
[
x
y
]
=
[
x
cos
(
α
)
−
y
sin
(
α
)
x
s
i
n
(
α
)
+
y
c
o
s
(
α
)
]
\left[\begin{array}{l} cos(\alpha) & -sin(\alpha) \\ sin(\alpha) & -cos(\alpha) \end{array}\right] \left[\begin{array}{l} x \\ y \end{array}\right] = \left[\begin{array}{l} x\cos(\alpha)- y\sin(\alpha)\\ x\ sin(\alpha) + y\ cos(\alpha) \end{array}\right]
[cos(α)sin(α)−sin(α)−cos(α)][xy]=[xcos(α)−ysin(α)x sin(α)+y cos(α)]
二维平面上的仿射变换
由上面的实验我们可以了解,任何平面上的线性变换都可以由缩放、平移和旋转组合而成,具体的公式如下:
[
a
b
c
d
]
[
x
y
]
+
[
e
f
]
=
[
a
x
+
b
y
+
e
c
x
+
d
y
+
f
]
\left[\begin{array}{} a&b \\ c&d \end{array}\right] \left[\begin{array}{} x \\ y \end{array}\right] + \left[\begin{array}{} e \\ f \end{array}\right] = \left[\begin{array}{} ax + by + e \\ cx + dy + f \end{array}\right]
[acbd][xy]+[ef]=[ax+by+ecx+dy+f]
我们可以通过这个方程完成平移,缩放和旋转,进而可以完成一切二维平面上的仿射变换。
齐次坐标
在前面我们得到了仿射变换的具体变换过程,但是对于上面的公式我们认为还是太复杂了,对于我们操作物体进行上百次或者上千次变换后,上面的式子就会变得复杂而臃肿,我们希望用一种简洁的方式表达。我们可以使用齐次坐标完成对上面式子的简化,我们在坐标中增加一维,得到下式:
[
a
b
e
c
d
f
0
0
1
]
[
x
y
1
]
=
[
a
x
+
b
y
+
e
c
x
+
d
y
+
f
1
]
\left[\begin{array}{} a&b&e \\ c&d&f \\ 0&0&1 \end{array}\right] \left[\begin{array}{} x \\ y \\ 1 \end{array}\right] = \left[\begin{array}{} ax + by + e \\ cx + dy + f \\ 1 \end{array}\right]
ac0bd0ef1
xy1
=
ax+by+ecx+dy+f1
事实上,这个想法真的很简单。平行平移在二维空间中不是线性的。因此,我们将2D嵌入到3D空间中(只需为第三个组件添加1)。这意味着我们的2D空间是3D空间中的平面z=1。然后我们进行线性三维变换,并将结果投影到我们的二维物理平面上。
我们如何将3D投影回2D平面?简单地除以三维分量:
[
x
y
z
]
⟶
[
x
/
z
y
/
z
]
\left[\begin{array}{} x \\ y \\ z \end{array}\right] \longrightarrow \left[\begin{array}{} x/z \\ y/z \end{array}\right]
xyz
⟶[x/zy/z]
我们引入了齐次坐标后,我们可以将上面的变换拆分,即对物体进行旋转、平移和缩放,这称之为Model矩阵:
M
=
[
1
0
x
0
0
1
y
0
0
0
1
]
[
c
o
s
(
α
)
−
s
i
n
(
α
)
0
s
i
n
(
α
)
−
c
o
s
(
α
)
0
0
0
1
]
[
1
0
−
x
0
0
1
−
y
0
0
0
1
]
M= \left[\begin{array}{} 1&0&x_0 \\ 0&1&y_0 \\ 0&0&1 \end{array}\right] \left[\begin{array}{l}cos(\alpha)&-sin(\alpha)&0 \\ sin(\alpha)&-cos(\alpha)&0 \\ 0&0&1\end{array}\right] \left[\begin{array}{} 1&0&-x_0 \\ 0&1&-y_0 \\ 0&0&1 \end{array}\right]
M=
100010x0y01
cos(α)sin(α)0−sin(α)−cos(α)0001
100010−x0−y01
那么对于变换矩阵中的最后一行代表什么呢?当我们使用下面这个变换矩阵时:
[
1
0
0
0
1
0
−
1
5
0
1
]
\left[\begin{array}{} 1&0&0 \\ 0&1&0 \\ -\frac{1}{5}&0&1 \end{array}\right]
10−51010001
生成结果如下:
我们在原图(白色方形框中)使用类似之前的y-buffer的想法,我们想要将物体投影到垂直线x=0处,并且我们的相机在(0,5)的位置并且指向原点,为了找到投影,我们需要在相机和要投影的点之间绘制直线(黄色),并找到与屏幕线的交点(白色垂直)
之后替换原先的物体,但不改变之前绘制的黄线:
如果我们使用标准正交投影将红色物体投影到屏幕上,那么我们会找到完全相同的点!让我们仔细看看变换是如何工作的:所有垂直片段都被变换为垂直片段,但靠近相机的片段被拉伸,远离相机的片段则被收缩。
三维平面
同样的对于三位平面,我们也是用齐次坐标
(
x
,
y
,
z
,
1
)
(x,y,z,1)
(x,y,z,1)扩充点
(
x
,
y
,
z
)
(x,y,z)
(x,y,z),然后我们在4D中进行变换并将其投影回3D。转换公式如下:
[
1
0
0
0
0
1
0
0
0
0
1
0
0
0
r
1
]
[
x
y
z
1
]
=
[
x
y
z
r
z
+
1
]
\left[\begin{array}{} 1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&0&r&1 \end{array} \right] \left[\begin{array}{} x \\ y \\ z \\ 1 \end{array}\right] = \left[\begin{array}{} x \\ y \\ z \\ rz + 1 \end{array}\right]
10000100001r0001
xyz1
=
xyzrz+1
[ x y z r z + 1 ] ⟶ [ x r z + 1 y r z + 1 z r z + 1 ] \left[\begin{array}{} x \\ y \\ z \\ rz + 1 \end{array}\right] \longrightarrow \left[\begin{array}{} \frac{x}{rz + 1} \\ \frac{y}{rz+1} \\ \frac{z}{rz+1} \end{array}\right] xyzrz+1 ⟶ rz+1xrz+1yrz+1z
我们在3D中给出一个点p的坐标
(
x
,
y
,
z
)
(x,y,z)
(x,y,z),我们想要将其投影到平面z上,同时相机在z轴上(0,0,c)处。
通过相似三角形的等比关系,我们可以推出下面两个等式:
x
′
=
x
1
−
z
/
c
y
′
=
y
1
−
z
/
c
x'=\frac{x}{1-z/c}\\ y'=\frac{y}{1-z/c}
x′=1−z/cxy′=1−z/cy
最终我们可以将上面所有变换组合起来
[
x
y
z
]
⟶
[
x
y
z
1
]
[
1
0
0
0
0
1
0
0
0
0
1
0
0
0
0
1
]
[
x
y
z
1
]
=
[
x
y
z
1
−
z
/
c
]
[
x
y
z
1
−
z
/
c
]
⟶
[
x
1
−
z
/
c
y
1
−
z
/
c
z
1
−
z
/
c
]
\left[\begin{array}{} x \\ y \\ z \end{array}\right] \longrightarrow \left[\begin{array}{} x \\ y \\ z \\ 1 \end{array}\right] \\ \\ \left[\begin{array}{} 1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&0&0&1 \end{array}\right] \left[\begin{array}{} x \\ y \\ z \\ 1 \end{array}\right] = \left[\begin{array}{} x \\ y \\ z \\ 1-z/c \end{array}\right] \\ \\ \left[\begin{array}{} x \\ y \\ z \\ 1 - z/c \end{array}\right] \longrightarrow \left[\begin{array}{} \frac{x}{1 - z/c} \\ \frac{y}{1 - z/c} \\ \frac{z}{1 - z/c} \end{array}\right]
xyz
⟶
xyz1
1000010000100001
xyz1
=
xyz1−z/c
xyz1−z/c
⟶
1−z/cx1−z/cy1−z/cz
#include <vector>
#include <cmath>
#include <limits>
#include "tgaimage.h"
#include "model.h"
#include "geometry.h"
const int width = 800;
const int height = 800;
const int depth = 255;
Model* model = NULL;
int* zbuffer = NULL;
Vec3f light_dir(0, 0, -1);
Vec3f camera(0, 0, 3);
Vec3f m2v(Matrix m) {
return Vec3f(m[0][0] / m[3][0], m[1][0] / m[3][0], m[2][0] / m[3][0]);
}
Matrix v2m(Vec3f v) {
Matrix m(4, 1);
m[0][0] = v.x;
m[1][0] = v.y;
m[2][0] = v.z;
m[3][0] = 1.f;
return m;
}
Matrix viewport(int x, int y, int w, int h) {
Matrix m = Matrix::identity(4);
m[0][3] = x + w / 2.f;
m[1][3] = y + h / 2.f;
m[2][3] = depth / 2.f;
m[0][0] = w / 2.f;
m[1][1] = h / 2.f;
m[2][2] = depth / 2.f;
return m;
}
void triangle(Vec3i t0, Vec3i t1, Vec3i t2, Vec2i uv0, Vec2i uv1, Vec2i uv2, TGAImage& image, float intensity, float* zbuffer) {
if (t0.y == t1.y && t0.y == t2.y) return;
if (t0.y > t1.y) { std::swap(t0, t1); std::swap(uv0, uv1); }
if (t0.y > t2.y) { std::swap(t0, t2); std::swap(uv0, uv2); }
if (t1.y > t2.y) { std::swap(t1, t2); std::swap(uv1, uv2); }
//这里直接使用画上下三角形的方法对三角形着色
//自己写的时候是用重心坐标,但重心坐标在透视投影下的校正写的一直不对
int total_height = t2.y - t0.y;
for (int i = 0; i < total_height; i++) {
bool second_half = i > t1.y - t0.y || t1.y == t0.y;
int segment_height = second_half ? t2.y - t1.y : t1.y - t0.y;
float alpha = (float)i / (float)total_height;
float beta = (float)(i - (second_half ? t1.y - t0.y : 0)) / (float)segment_height;
Vec3i A = t0 + Vec3f(t2 - t0) * alpha;
Vec3i B = second_half ? t1 + Vec3f(t2 - t1) * beta : t0 + Vec3f(t1 - t0) * beta;
Vec2i uvA = uv0 + (uv2 - uv0) * alpha;
Vec2i uvB = second_half ? uv1 + (uv2 - uv1) * beta : uv0 + (uv1 - uv0) * beta;
if (A.x > B.x) { std::swap(A, B); std::swap(uvA, uvB); }
for (int j = A.x; j <= B.x; j++) {
float phi = B.x == A.x ? 1. : (float)(j - A.x) / (float)(B.x - A.x);
Vec3f P = Vec3f(A) + Vec3f(B - A) * phi;
Vec2i uvP = uvA + (uvB - uvA) * phi;
int idx = P.x + P.y * width;
if (zbuffer[idx] < P.z) {
zbuffer[idx] = P.z;
TGAColor color = model->diffuse(uvP);
TGAColor lightcolor = TGAColor(color[0] * intensity, color[1] * intensity, color[2] * intensity, 1);
image.set(P.x, P.y, lightcolor);
}
}
}
}
int main() {
model = new Model("obj/african_head/african_head.obj");
model->load_texture("obj/african_head/african_head_diffuse.tga");
float* zbuffer = new float[width * height];
for (int i = 0; i < width * height; i++) {
zbuffer[i] = std::numeric_limits<float>::min();
}
{ // draw the model
Matrix Projection = Matrix::identity(4);
Matrix ViewPort = viewport(width / 8, height / 8, width * 3 / 4, height * 3 / 4);
Projection[3][2] = -1.f / camera.z;
TGAImage image(width, height, TGAImage::RGB);
for (int i = 0; i < model->nfaces(); i++) {
std::vector<int> face = model->face(i);
Vec3i screen_coords[3];
Vec3f world_coords[3];
for (int j = 0; j < 3; j++) {
Vec3f v = model->vert(face[j]);
screen_coords[j] = m2v(ViewPort * Projection * v2m(v));
world_coords[j] = v;
}
Vec3f n = (world_coords[2] - world_coords[0]) ^ (world_coords[1] - world_coords[0]);
n.normalize();
float intensity = n * light_dir;
if (intensity > 0) {
Vec2i uv[3];
for (int k = 0; k < 3; k++) {
uv[k] = model->uv(i, k);
}
triangle(screen_coords[0], screen_coords[1], screen_coords[2], uv[0], uv[1], uv[2], image, intensity, zbuffer);
}
}
image.write_tga_file("translation.tga");
}
{ // dump z-buffer (debugging purposes only)
TGAImage zbimage(width, height, TGAImage::GRAYSCALE);
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
TGAColor color = TGAColor(zbuffer[i + j * width], zbuffer[i + j * width], zbuffer[i + j * width], 1);
zbimage.set(i, j, color);
}
}
zbimage.write_tga_file("zbuffer.tga");
}
delete model;
delete[] zbuffer;
return 0;
}