有了ppm格式的支持,SOVOS终于可以开始cpp渲染之路了。这次SOVOS打算用cpp渲染一个最简单的情况,渲染一个旋转的小方块。

1. 旋转线框方块

我们先做简单的情况,只渲染出正方形的线框。为了做到这一点,我们需要知道虚拟三维空间中的点其对应的二维坐标是多少,以及需要知道怎么在离散二维画面上画线。

1.1. 三维到二维映射

要让计算机渲染出图片,我们实际上需要的是一个空间3维坐标到2维坐标的映射,也就是我们已知摄像头和物体的相关信息。对于虚拟空间中的某一个点,我们知道它的三维坐标,我想要通过这个三维坐标得到这个点最终出现在画面的什么位置。

之前我以为需要弄一个透镜什么的然后通过模拟啊之类的方法让物体在某一个平面上成像,然后把这个像截取出来blabla的。但对于这么简单的情况我想得似乎过于复杂了,实际上我们只需要如下一个极其简单的方法:

坐标映射图片

直接说结论。物AA与原点连线,与像平面交点A′A'就是他的像,取A′A'的xx、yy坐标就是最终的二维坐标。

直觉上讲我感觉还挺合理的。实际上可以借助小孔成像理解?只不过我们把像平面移动到小孔前面了,效果还是不变的。其实用透镜理解也没有问题,毕竟理想情况下过光心的光线方向不变。

像平面垂直于zz轴。设A(xA,yA,zA)A(x_A,y_A,z_A),相机焦距OO′=fOO'=f,其对应二维坐标如下

xA′=fzAxAyA′=fzAyA x_A'=\frac{f}{z_A}x_A\quad y_A'=\frac{f}{z_A}y_A

这样我们就完成了三维到二维的映射。

1.2. 两点画一线

有一个好用的算法。Bresenham 算法的目标是:给定两个端点,决定像素网格上哪些格子应该被涂黑,使得这些像素连起来看起来像一条直线。

但是我懒得写。以后有空再补上吧(?

1.3. 实现旋转小线框

我们让摄像机在原点,焦距就随便选个200吧,然后相机朝向z轴正方向。小正方体的中心位于(0,0,600)(0,0,600),边长为200。我们希望小正方体能够绕过其中心的平行于x轴的轴(此后称其为小方块的自身x轴)旋转,那么我们就需要各个顶点相对于其中心的相对坐标,然后乘上旋转矩阵就可以得到旋转后的三维坐标。同时为了让画面不那么单调,我们可以初始先让小方块绕过其中心的平行于y轴的轴(此后称其为小方块的自身y轴)旋转一个固定角度θ0\theta_0,让小方块看起来更有视觉上的立体感。

小方块绕自身y轴旋转坐标变换公式为

(x′y′z′)=(cos⁡θ00sin⁡θ0010−sin⁡θ00cos⁡θ0)(xyz)\begin{pmatrix} x'\\ y'\\ z' \end{pmatrix}= \begin{pmatrix} \cos \theta_0 & 0 & \sin \theta_0 \\ 0 & 1 & 0 \\ -\sin \theta_0 & 0 & \cos \theta_0 \end{pmatrix} \begin{pmatrix} x\\y\\z \end{pmatrix}

x′=xcos⁡θ0+zsin⁡θ0y′=yz′=−xsin⁡θ0+zcos⁡θ0\begin{aligned} x'&=x \cos \theta_0+z \sin \theta_0\\ y'&=y\\ z'&=-x \sin \theta_0 + z \cos \theta_0 \end{aligned}

小方块绕自身x轴旋坐标变换公式为

(x′y′z′)=(1000cos⁡ϕ−sin⁡ϕ0sin⁡ϕcos⁡ϕ)(xyz)\begin{pmatrix} x'\\ y'\\ z' \end{pmatrix}= \begin{pmatrix} 1 & 0 & 0\\ 0 & \cos \phi & -\sin \phi\\ 0 & \sin \phi & \cos \phi \end{pmatrix} \begin{pmatrix} x\\y\\z \end{pmatrix}

x′=xy′=ycos⁡ϕ−zsin⁡ϕz′=ysin⁡ϕ+zcos⁡ϕ\begin{aligned} x'&=x\\ y'&=y\cos \phi - z \sin \phi\\ z'&=y \sin \phi + z \cos \phi \end{aligned}

然后就是代码实现了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//please ensure there is a "frames" folder before you run this program
#include<cstdio>
#include<utility>
#include<cmath>
#include<algorithm>
#include<cstring>
const double pi=3.14159265358979;
const double edgeLen=200;
const double deltaPhi=1.0*pi/180;
const double theta0=30.0*pi/180;
const double f=200;
//size of video
const double width=512;
const double high=512;

char frame[512][512][3];
using namespace std;
struct vec{
double x,y,z;
};
//cube points relative to its center
vec points[8]={
{edgeLen/2,-edgeLen/2,edgeLen/2},
{edgeLen/2,edgeLen/2,edgeLen/2},
{-edgeLen/2,edgeLen/2,edgeLen/2},
{-edgeLen/2,-edgeLen/2,edgeLen/2},
{edgeLen/2,-edgeLen/2,-edgeLen/2},
{edgeLen/2,edgeLen/2,-edgeLen/2},
{-edgeLen/2,edgeLen/2,-edgeLen/2},
{-edgeLen/2,-edgeLen/2,-edgeLen/2}
};
std::pair<int,int>edge[12]={
{0,1},
{1,2},
{2,3},
{3,0},
{4,5},
{5,6},
{6,7},
{4,7},
{0,4},
{1,5},
{2,6},
{3,7}
};
vec cubeCenter={0,0,500};
// Bresenham's line drawing algorithm
// 在 frame 上从 (x0,y0) 到 (x1,y1) 画一条颜色为 (r,g,b) 的直线
void drawLine(int x0, int y0, int x1, int y1, unsigned char r, unsigned char g, unsigned char b) {
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
int sx = (x0 < x1) ? 1 : -1;
int sy = (y0 < y1) ? 1 : -1;
int err = dx - dy;

int x = x0, y = y0;
while (true) {
// 边界检查,只在屏幕内绘制
if (x >= 0 && x < width && y >= 0 && y < high) {
frame[y][x][0] = r;
frame[y][x][1] = g;
frame[y][x][2] = b;
}
if (x == x1 && y == y1) break;

int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x += sx;
}
if (e2 < dx) {
err += dx;
y += sy;
}
}
}
int main(){
//generate 30 frames
for(int cnt=0;cnt<360;cnt++){
memset(frame,255,sizeof(frame));
double phi=cnt*deltaPhi;
pair<int,int> rpoints[8];
for(int i=0;i<8;i++){
//rotate around the Y axis
double x=points[i].x,y=points[i].y,z=points[i].z;
double rx,ry,rz;
rx=x*std::cos(theta0)+z*std::sin(theta0);
ry=y;
rz=-1*x*std::sin(theta0)+z*std::cos(theta0);
//rotate around the X axis
double fx,fy,fz;
fx=rx;
fy=ry*std::cos(phi)-rz*std::sin(phi);
fz=ry*std::sin(phi)+rz*std::cos(phi);

fz+=500;
rpoints[i].first=(int)std::max(0.0,std::min(((f/fz*fx)+100)/200*high,high));
rpoints[i].second=(int)std::max(0.0,std::min(((f/fz*fy)+100)/200*width,width));
}
for(int i=0;i<12;i++){
std::pair<double,double>p1,p2;
p1=rpoints[edge[i].first];
p2=rpoints[edge[i].second];
drawLine(p1.first,p1.second,p2.first,p2.second,0,0,0);
}
char str_buffer[50];
sprintf(str_buffer,"frames/%03d.ppm",cnt);
FILE* file=fopen(str_buffer,"wb");
fputs("P6\n",file);
fputs("512 512\n",file);
fputs("255\n",file);
for(int x=0;x<high;x++){
for(int y=0;y<width;y++){
fputc(frame[y][x][0],file);
fputc(frame[y][x][1],file);
fputc(frame[y][x][2],file);
}
}
fclose(file);
}
return 0;
}

除了那个Bresenham 算法的实现是deepseek写的,其余代码都是我纯手写的,并且我刚开始用vim,太不习惯了,写这个代码真的要了我老命。我知道我的码风现在很烂QAQ

代码渲染效果如下:

可以看到作为一个最简单的例子,效果还是挺好的。