甜甜圈数学 from 2006

本文尽量逐句翻译

代码1

donut.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
             k;double sin()
,cos();main(){float A=
0,B=0,i,j,z[1760];char b[
1760];printf("\x1b[2J");for(;;
){memset(b,32,1760);memset(z,0,7040)
;for(j=0;6.28>j;j+=0.07)for(i=0;6.28
>i;i+=0.02){float c=sin(i),d=cos(j),e=
sin(A),f=sin(j),g=cos(A),h=d+2,D=1/(c*
h*e+f*g+5),l=cos (i),m=cos(B),n=s\
in(B),t=c*h*g-f* e;int x=40+30*D*
(l*h*m-t*n),y= 12+15*D*(l*h*n
+t*m),o=x+80*y, N=8*((f*e-c*d*g
)*m-c*d*e-f*g-l *d*n);if(22>y&&
y>0&&x>0&&80>x&&D>z[o]){z[o]=D;;;b[o]=
".,-~:;=!*#$@"[N>0?N:0];}}/*#****!!-*/
printf("\x1b[H");for(k=0;1761>k;k++)
putchar(k%80?b[k]:10);A+=0.04;B+=
0.02;}}/*****####*******!!=;:~
~::==!!!**********!!!==::-
.,~~;;;========;;;:~-.
..,--------,*/

gcc -o donut donut.c -lm && ./donut

这是作者第一次尝试混淆C代码, 这个版本是相对简单优雅的

代码2

注意 下面13行,因为我本地的hexo相关工具不能正确工作,为了尽可能展示代码,我在<.之间加了一个空格,实际上是没有空格的,所以如果从这里拷贝代码,记得去掉多出的空格

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
_,x,y,o       ,N;char       b[1840]       ;p(n,c)
{for(;n --;x++) c==10?y +=80,x=
o-1:x>= 0?80>x? c!='~'? b[y+x]=
c:0:0:0 ;}c(q,l ,r,o,v) char*l,
*r;{for (;q>=0; )q=("A" "YLrZ^"
"w^?EX" "novne" "bYV" "dO}LE"
"{yWlw" "Jl_Ja|[ur]zovpu" "" "i]e|y"
"ao_Be" "osmIg}r]]r]m|wkZU}{O}" "xys]]\
x|ya|y" "sm||{uel}|r{yIcsm||ya[{uE" "{qY\
w|gGor" "VrVWioriI}Qac{{BIY[sXjjsVW]aM" "T\
tXjjss" "sV_OUkRUlSiorVXp_qOM>E{BadB"[_/6 ]-
62>>_++ %6&1?r[q]:l[q])-o;return q;}E(a){for (
o= x=a,y=0,_=0;1095>_;)a= " < .,`'/)(\n-" "\\_~"[
c (12,"!%*/')#3" "" "+-6,8","\"(.$" "01245"
" &79",46)+14], p("" "#$%&'()0:439 "[ c(10
, "&(*#,./1345" ,"')" "+%-$02\"! ", 44)+12]
-34,a); }main(k){float A=0,B= 0,i,j,z[1840];
puts("" "\x1b[2J");;; for(;; ){float e=sin
(A), n= sin(B),g=cos( A),m= cos(B);for(k=
0;1840> k;k++)y=-10-k/ 80 ,o=41+(k%80-40
)* 1.3/y+n,N=A-100.0/y,b[k]=".#"[o+N&1], z[k]=0;
E( 80-(int)(9*B)%250);for(j=0;6.28>j;j +=0.07)
for (i=0;6.28>i;i+=0.02){float c=sin( i), d=
cos( j),f=sin(j),h=d+2,D=15/(c*h*e+f *g+5),l
=cos(i) ,t=c*h*g-f*e;x=40+2*D*(l*h* m-t*n
),y=12+ D *(l*h*n+t*m),o=x+80*y,N =8*((f*
e-c*d*g )*m -c*d*e-f*g-l*d*n) ;if(D>z
[o])z[o ]=D,b[ o]=" ." ".,,-+"
"+=#$@" [N>0?N: 0];;;;} printf(
"%c[H", 27);for (k=1;18 *100+41
>k;k++) putchar (k%80?b [k]:10)
;;;;A+= 0.053;; B+=0.03 ;;;;;}}

同样的编译和运行命令, 这次是有背景, 有弹幕的版本

原理

2011 年,有人提起了作者的2006年的作品,有很多请求作者讲解原理的,但过去了5年,作者并不能清晰记得,所以作者打算从零开始,非常详尽的细节,希望能得到相近的结果

这个内容的核心是利用 帧buffer 和 Z-buffer 来渲染像素(通过ascii渲染低分辨率的图像)

通过固定的角度增量的,来绘制对应的环面(torus), 亮度是从暗到亮对应的ASCII.,-~:;=!*#$@.(不需要射线追踪

3D物体 到 2D屏幕

3D和2D映射

如图, 当一个人看一个3D 物体时,眼睛和物体连线与屏幕的交点, 就是我们要的投影

把3D物品渲染到2D屏幕上

$(x,y,z)$实际的点投影到 距离眼睛为$z’$的屏幕的$(x’,y’)$上

显然相似三角形

$\frac{y’}{y}=\frac{x’}{x}=\frac{z’}{z}$

换句话说$(x’,y’) = (z’\frac{x}{z},z’\frac{y}{z})$, 左边是要求的,右边是三维空间中已知

其中呢,人到屏幕距离$z’$是常量, 这个常量变化相当于把屏幕推远或拉近,取决于你希望展示多少内容


因为物体有深度,所以可能多个点对应到2D屏幕上同一个点,那我们需要一个记录每个我们绘制的点的z坐标,这样可以保证后面的点不会覆盖前面的点

在记录z-buffer同时,我们可以同时增加1/z-buffer.

  1. 1/z 表示无穷远
  2. 计算$x’,y’$时,可以直接乘1/z,比除两次代价小(?

现在可以开始绘制 甜甜圈了(torus)


torus 是一个旋转体, 一个方法是把2D的圆,绕着一个圆外的轴旋转, 下图是个示意的切面

圆绕轴旋转

先谈圆, 一个半径$R_1$的圆,其圆心在$(R_2,0,0)$

那么圆上的点$(x,y,z) = (R_2,0,0) + (R_1 \cos \theta,R_1 \sin \theta,0)$

如果绕y轴旋转, 其y轴不变, 根据旋转矩阵为

$\left( \begin{matrix} R_2 + R_1 \cos \theta, & R_1 \sin \theta, & 0 \end{matrix} \right)
\cdot \left( \begin{matrix} \cos \phi & 0 & \sin \phi \\ 0 & 1 & 0 \\ -\sin \phi & 0 & \cos \phi \end{matrix} \right)$

$= \left( \begin{matrix} (R_2 + R_1 \cos \theta)\cos \phi, & R_1 \sin \theta, & -(R_2 + R_1 \cos \theta)\sin \phi \end{matrix} \right)$

如果你希望也绕 x轴转$A$,或者 z轴旋转$B$, 只需要多乘上对应的旋转矩阵就完了

$\left( \begin{matrix}
1 & 0 & 0 \\
0 & \cos A & \sin A \\
0 & -\sin A & \cos A \end{matrix} \right)
\cdot
\left( \begin{matrix}
\cos B & \sin B & 0 \\
-\sin B & \cos B & 0 \\
0 & 0 & 1 \end{matrix} \right)$


但对于上面来说, 会发现它其实整个的旋转中心都是原点,而我们上面给出的3D到2D的投影,过原点的是屏幕而不是物品, 不过幸运的是,我们仅需要把物体沿着z轴平移,就可以让它不再在原点上

对此,我们 令$K_1 = z’$控制人屏之间的距离,$K_2 = $ 人到物体旋转中心的距离,

$(x’,y’) = (z’\frac{x}{z},z’\frac{y}{z}) = (\frac{K_1 x}{K_2+z},\frac{K_1 y}{K_2+z})$

其中,第二第三个式子中的z并不相同,第二个是描述的距离人的z,而第三个是距离旋转中心的z


把上述的矩阵展开

$\left( \begin{matrix} x \\ y \\ z \end{matrix} \right) = \left( \begin{matrix} (R_2 + R_1 \cos \theta) (\cos B \cos \phi + \sin A \sin B \sin \phi) - R_1 \cos A \sin B \sin \theta \\ (R_2 + R_1 \cos \theta) (\cos \phi \sin B - \cos B \sin A \sin \phi) + R_1 \cos A \cos B \sin \theta \\ \cos A (R_2 + R_1 \cos \theta) \sin \phi + R_1 \sin A \sin \theta \end{matrix} \right) = \left( \begin{matrix} C_x(\cos B \cos \phi + \sin A \sin B \sin \phi) - C_y \cos A \sin B \\ C_x(\cos \phi \sin B - \cos B \sin A \sin \phi) + C_y \cos A \cos B \\ C_x \cos A \sin \phi + C_y \sin A \end{matrix} \right)$

其中$C_x,C_y$ 表示绕轴旋转前,圆上的点

实际上和最开始代码中的乘法因子完全不同(留给读者展开,原来的代码也交换了A的sin和cos, 高效的旋转90度,所以估计原来推导有些不同,但都能用)


现在呢,知道了像素放在哪里,但是还没有阴影的概念, (也有意义,如果把投影全部同色绘制,就可以当影子看

要计算照明, 我们需要知道平面上每一点的法向量

如果知道了法向量,再和光的方向点积,其中光的方向自定义,

接下来就是向量点级的绝对值越大,亮度越大,正负选一面为看不见(根据法向量方向不同)

如何求法向量呢, 首先注意到法向量和平面同时一起旋转的, 那么对于圆上的点的法向量

是 $(\cos \theta,\sin \theta, 0)$

剩下的无非是乘上旋转矩阵了

$\left( \begin{matrix} N_x, & N_y, & N_z \end{matrix} \right) = \left( \begin{matrix} \cos \theta, &
\sin \theta, & 0 \end{matrix} \right) \cdot \left( \begin{matrix} \cos \phi & 0 & \sin \phi \\ 0 & 1 & 0 \\ -\sin \phi & 0 & \cos \phi \end{matrix} \right) \cdot \left( \begin{matrix} 1 & 0 & 0 \\
0 & \cos A & \sin A \\ 0 & -\sin A & \cos A \end{matrix} \right) \cdot \left( \begin{matrix} \cos B & \sin B & 0 \\ -\sin B & \cos B & 0 \\ 0 & 0 & 1 \end{matrix} \right)$

光的方向,这里选择45度夹角的光$(0,1,-1)$ (这里不是单位向量,长度根号2)

$\begin{aligned} L &= \left( \begin{matrix} N_x, & N_y, & N_z \end{matrix} \right) \cdot \left( \begin{matrix} 0, & 1, & -1 \end{matrix} \right) \\ &= \cos \phi \cos \theta \sin B - \cos A \cos \theta \sin \phi - \sin A \sin \theta + \cos B ( \cos A \sin \theta - \cos \theta \sin A \sin \phi) \end{aligned}$

这样我们有了亮度

剩下的就是选择你希望的 $K_1$(眼睛屏幕距离, 缩放大小),$K_2$(眼睛物体距离),$R_1$ 切面圆形半径,$R_2$ 圆形中心到旋转轴距离

对于代码中,选取的是$R_1=1,R_2=2$


综上

$(x,y,z)$ 由 两个半径,枚举旋转体角度,和绕x轴,z轴旋转 得到

$(x,y,z)$ 投影到$(\frac{K_1 x}{K_2+z},\frac{K_1 y}{K_2+z})$

亮度为$L$

我们有了伪代码

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
const float theta_spacing = 0.07; // 旋转体切面的圆上的点弧度旋转
const float phi_spacing = 0.02; // 旋转体绕轴每份旋转弧度

const float R1 = 1; // 旋转体切面圆半径
const float R2 = 2; // 旋转体切面圆心和旋转轴距离
const float K2 = 5; // 人和旋转体旋转中心的距离
// 基于屏幕大小计算 K1
// 对于torus的边缘, 也就是距离旋转中心$R1+R2$
// 我们希望它能被展示到屏幕的 3/8,也就是整个展示到屏幕的6/8,上下左右留白1/8
// 根据3D和2D转换关系
// 屏幕宽度*3/8 = K1*(R1+R2)/(K2+0)
const float K1 = screen_width*K2*3/(8*(R1+R2));

render_frame(float A, float B) { // A,B就是上面的旋转角度
// 预计算 A,B的sin和cos
float cosA = cos(A), sinA = sin(A);
float cosB = cos(B), sinB = sin(B);
// 输出字符
char output[0..screen_width, 0..screen_height] = ' ';
// 2D屏幕上的点原来在3D物体上的z坐标记录
float zbuffer[0..screen_width, 0..screen_height] = 0;

// 且面圆上的点
for (float theta=0; theta < 2*pi; theta += theta_spacing) {
// 预先计算 theta 的sin和cos
float costheta = cos(theta), sintheta = sin(theta);

// 旋转体 ,上面的圆绕旋转轴旋转phi
for(float phi=0; phi < 2*pi; phi += phi_spacing) {
// 同样预先计算 phi 的 sin和cos
float cosphi = cos(phi), sinphi = sin(phi);

// 绕y轴旋转前 圆上的点 的 x,y
float circlex = R2 + R1*costheta;
float circley = R1*sintheta;

// 计算出最终的 3D (x,y,z) 坐标, 上面公式是想对于旋转中心,这里z加上K2 表示相对于人的眼睛的
float x = circlex*(cosB*cosphi + sinA*sinB*sinphi)
- circley*cosA*sinB;
float y = circlex*(sinB*cosphi - sinA*cosB*sinphi)
+ circley*cosA*cosB;
float z = K2 + cosA*circlex*sinphi + circley*sinA;
float ooz = 1/z; // "one over z"

// 计算在屏幕上的x和y,因为是屏幕上原点要移动到 屏幕中心, 至此有了2D上的位置
int xp = (int) (screen_width/2 + K1*ooz*x);
int yp = (int) (screen_height/2 - K1*ooz*y);

// 计算光照
float L = cosphi*costheta*sinB - cosA*costheta*sinphi -
sinA*sintheta + cosB*(cosA*sintheta - costheta*sinA*sinphi);
// 因为选取的光的模长度sqrt2, 所以光强度[-sqrt2,sqrt2]之间, 小于零的视作在背面直接看不见
if (L > 0) {
// 检查z级别, z越小ooz越大,离人越近,覆盖优先级更高
if(ooz > zbuffer[xp,yp]) {
zbuffer[xp, yp] = ooz;
int luminance_index = L*8;
// luminance_index 现在范围是 0..11 (8*sqrt(2) = 11.3)
// 因此字符映射如下
output[xp, yp] = ".,-~:;=!*#$@"[luminance_index];
}
}
}
}
// 最后输出output到屏幕即可
// 在终端里 控制光标到终端的起始,可以重用屏幕输出
printf("\x1b[H");
for (int j = 0; j < screen_height; j++) {
for (int i = 0; i < screen_width; i++) {
putchar(output[i,j]);
}
putchar('\n');
}
}

代码

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<header>
<title>donut.js</title>
<script type="text/javascript" async="" src="./index.js"></script>
</header>
<body>
<div>
<button onclick="anim1();">toggle animation</button>
<pre id="d" style="background-color:#000; color:#ccc; font-size: 10pt;"></pre>
<button onclick="anim2();">toggle animation</button>
<canvas id="canvasdonut" width="300" height="240"></canvas>
<button onclick="anim3();">toggle animation</button>
<canvas id="canvascube" width="300" height="240"></canvas>
</div>
</body>
</html>

index.js:

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
(function() {
let _onload = () => {
let pretag = document.getElementById('d');
let canvastag = document.getElementById('canvasdonut');
let canvascube = document.getElementById('canvascube');

let tmr1 = undefined;
let tmr2 = undefined;

// This is copied, pasted, reformatted, and ported directly from my original
// donut.c code
let asciiframe= ((A,B) => () => {
let b=[];
let z=[];
A += 0.07;
B += 0.03;
let cA=Math.cos(A), sA=Math.sin(A),
cB=Math.cos(B), sB=Math.sin(B);
for(let k=0;k<1760;k++) {
b[k]= k%80 == 79 ? "\n" : " ";
z[k]= 0;
}
for(let j=0;j<6.28;j+=0.07) { // j <=> theta
let ct=Math.cos(j),st=Math.sin(j);
for(i=0;i<6.28;i+=0.02) { // i <=> phi
let sp=Math.sin(i),cp=Math.cos(i),
h=ct+2, // R1 + R2*cos(theta)
D=1/(sp*h*sA+st*cA+5), // this is 1/z
t=sp*h*cA-st*sA; // this is a clever factoring of some of the terms in x' and y'

let x=0|(40+30*D*(cp*h*cB-t*sB)),
y=0|(12+15*D*(cp*h*sB+t*cB)),
o=x+80*y,
N=0|(8*((st*sA-sp*ct*cA)*cB-sp*ct*sA-st*cA-cp*ct*sB));
if(y<22 && y>=0 && x>=0 && x<79 && D>z[o]) {
z[o]=D;
b[o]=".,-~:;=!*#$@"[N>0?N:0];
}
}
}
pretag.innerHTML = b.join("");
})(Math.random()*Math.PI,Math.random()*Math.PI);

window.anim1 = () => {
if(tmr1 === undefined) {
tmr1 = setInterval(asciiframe, 50);
} else {
clearInterval(tmr1);
tmr1 = undefined;
}
};

// This is a reimplementation according to my math derivation on the page
let canvasframe = ((A,B) => (R1,R2,K1,K2,rA,rB) => {
let ctx = canvastag.getContext('2d');
ctx.fillStyle='#000';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

A += rA*(Math.random()+0.5);
B += rB*(Math.random()+0.5);
// precompute cosines and sines of A, B, theta, phi, same as before
let cA=Math.cos(A), sA=Math.sin(A),
cB=Math.cos(B), sB=Math.sin(B);
for(let j=0;j<6.28;j+=0.3) { // j <=> theta
let ct=Math.cos(j),st=Math.sin(j); // cosine theta, sine theta
for(i=0;i<6.28;i+=0.1) { // i <=> phi
let sp=Math.sin(i),cp=Math.cos(i); // cosine phi, sine phi
let ox = R2 + R1*ct, // object x, y = (R2,0,0) + (R1 cos theta, R1 sin theta, 0)
oy = R1*st;

let x = ox*(cB*cp + sA*sB*sp) - oy*cA*sB; // final 3D x coordinate
let y = ox*(sB*cp - sA*cB*sp) + oy*cA*cB; // final 3D y
let ooz = 1/(K2 + cA*ox*sp + sA*oy); // one over z
let xp=(150+K1*ooz*x); // x' = screen space coordinate, translated and scaled to fit our 320x240 canvas element
let yp=(120-K1*ooz*y); // y' (it's negative here because in our output, positive y goes down but in our 3D space, positive y goes up)
// luminance, scaled back to 0 to 1
let L=0.7*(cp*ct*sB - cA*ct*sp - sA*st + cB*(cA*st - ct*sA*sp));
if(L > 0) {
ctx.fillStyle = `rgba(255,255,255,${L}`;
ctx.fillRect(xp, yp, 10*L, 10*L);
}
}
}
})(Math.random()*Math.PI,Math.random()*Math.PI);

window.anim2 = () => {
if(tmr2 === undefined) {
tmr2 = setInterval(() => canvasframe(1,2,150,5,0.07,0.03), 50);
} else {
clearInterval(tmr2);
tmr2 = undefined;
}
};

// Cube
let canvasframe2 = ((A,B) => (W,K1,K2,rA,rB,sX,sY) => {
let ctx = canvascube.getContext('2d');
ctx.fillStyle='#000';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

A += rA*(Math.random()+0.5);
B += rB*(Math.random()+0.5);
// precompute cosines and sines of A, B, theta, phi, same as before
let cA=Math.cos(A), sA=Math.sin(A),
cB=Math.cos(B), sB=Math.sin(B);
for(let j=-W/2;j<W/2;j+=0.2) { // j <=> theta
for(i=-W/2;i<W/2;i+=0.2) { // i <=> phi
for(z=-W/2;z<W/2;z+=0.2){ // (j,i,z)
let x = j*cB - z*sA*sB - i*cA*sB; // final 3D x coordinate
let y = j*sB + z*cB*sA + i*cA*cB; // final 3D y
let ooz = K1/(K2 - z*cA + i*sA); // one over z
let xp=(sX/2+ooz*x); // x' = screen space coordinate, translated and scaled to fit our 320x240 canvas element
let yp=(sY/2-ooz*y); // y' (it's negative here because in our output, positive y goes down but in our 3D space, positive y goes up)
// luminance, scaled back to 0 to 1
let L=(y + W/2 + (z*cA - i*sA)+W/2)/(2*W);
if(L > 0) {
ctx.fillStyle = `rgba(255,255,255,${L}`;
ctx.fillRect(xp, yp, 10*L, 10*L);
}
}
}
}
})(Math.random()*Math.PI,Math.random()*Math.PI);

window.anim3 = ((tmr)=> () => {
if(tmr === undefined) {
tmr = setInterval(() => canvasframe2(1,300,5,0.07,0.03,300,240), 50);
} else {
clearInterval(tmr);
tmr = undefined;
}
})();


window.anim1();
window.anim2();
window.anim3();
}

if(document.all)
window.attachEvent('onload',_onload);
else
window.addEventListener("load",_onload,false);
})();

去掉数学库

Lex Fridman Youtube

Joma Tech

早知道会受到这么多关注,我就会在它上面花更多时间

对于程序中来说,sin,cos这两个函数消耗最大,并且需要-lm

既然有旋转公式,那么

$\begin{bmatrix} c’ \\ s’ \end{bmatrix} = \begin{bmatrix} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{bmatrix} \begin{bmatrix} c \\ s \end{bmatrix}$

可以每次旋转一度,来完成转换,而不是计算sin/cos, 我们只需要硬编码sin(2pi/314),cos(2pi/314) 即可

1
2
3
4
5
6
7
float c=1, s=0;  // c for cos, s for sin
for (int i = 0; i < 314; i++) { // 314 * .02 ~= 2π
// (use c, s in code)
float newc = 0.9998*c - 0.019998666*s;
s = 0.019998666*c + 0.9998*s;
c = newc;
}

然而如果如此做下去,会出现误差累积(不论多少精度),最终导致不是个圆形

误差累积后

一个思路是每次单位化结果.但如果直接去根号,那还是需要数学库,这一块可以用牛顿切线,不论是直接处理根号,还是根号分之1, 即使没有牛顿切的知识,二分也是类似的思路,就是手动实现开根

不过这里 作者说只做一步牛顿切,觉得它足够接近1

CORDIC

  1. 提取cos

$\begin{bmatrix} c’ \\ s’ \end{bmatrix} = \frac{1}{\cos \theta}\begin{bmatrix} 1 & -\tan \theta \\ \tan \theta & 1 \end{bmatrix} \begin{bmatrix} c \\ s \end{bmatrix}$

  1. cos足够接近1, 我们甚至可以省略它,直接只用tan的部分加上牛顿切

其中t 是 tan, 宏编程

1
2
3
4
5
6
7
#define R(t,x,y) \
f = x; \
x -= t*y; \
y += t*f; \
f = (3-x*x-y*y)/2; \
x *= f; \
y *= f;

去掉float

We can use exactly the same ideas with integer fixed-point arithmetic, and not use any float math whatsoever. I’ve redone all the math with 10-bit precision and produced the following C code which runs well on embedded devices which can do 32-bit multiplications and have ~4k of available RAM:

总结

  1. 本身上数学难度算不高,但是涉及了不少现有技术的基础,如3D到2D,光照计算,3D物体旋转,ASCII亮度表现法
  2. 混乱后的代码看上去更”有趣”,不过博主没有介绍混淆,只介绍了实现,实际上混淆方法多,用就是了.
  3. 基于每一步的详细解读,修改内容可以实现正方体,不同光源,的渲染,如上面代码中就有正方体的渲染

Ref

原文0 2006

原文1 2006

原文2 2011

原文3 2021

Z-buffering

环面 torus

旋转体 solid of revolution

旋转矩阵 rotation matrix

平面法线 surface normal

CORDIC