简介
mini3d是前网易员工@韦易笑开发的3d软渲染引擎,总代码量不到1000行,短小精悍,适合初学者学习。
本文结合源码给出自己的理解,并在原作基础上实现功能扩展:
- 补充缺少的三维变换功能(平移、缩放)
- 增加简单光照(漫反射)
代码简析
这个项目实现了一个方块的3d渲染,主要功能点有:
- 支持平移和旋转
- 支持三种不同状态的显示(线框、颜色、纹理)
下面来对代码做个简析,主要是把握脉络,不求深入细节。
先看主函数,第一行定义了类型为device_t
的变量device
。
结构体device_t
包含渲染方块用到的所有参数和数据结构:
typedef struct {
transform_t transform; // 坐标变换器
int width; // 窗口宽度
int height; // 窗口高度
IUINT32 **framebuffer; // 像素缓存:framebuffer[y] 代表第 y行
float **zbuffer; // 深度缓存:zbuffer[y] 为第 y行指针
IUINT32 **texture; // 纹理:同样是每行索引
int tex_width; // 纹理宽度
int tex_height; // 纹理高度
float max_u; // 纹理最大宽度:tex_width - 1
float max_v; // 纹理最大高度:tex_height - 1
int render_state; // 渲染状态
IUINT32 background; // 背景颜色
IUINT32 foreground; // 线框颜色
} device_t;
其后完成一系列初始化:
函数 | 作用 |
---|---|
screen_init | 初始化屏幕(windows上显示窗口所必需) |
device_init | 初始化设备(为device_t 中的成员变量赋初值) |
camera_at_zero | 初始化相机位置,建立相机坐标系 |
init_texture | 初始化纹理(蓝白相间图案,存在device_t 的texture 变量中) |
随后进入主循环,每次循环会依次做如下操作:
device_clear
:清空framebuffer
和z-buffer
。camera_at_zero
:重设相机位置。- 监听键盘事件,修改旋转角度
alpha
和相机x
坐标的值,以及切换render_state
。 draw_box
:画方块,具体来说是从方块 -> 平面 -> 三角形 -> 线段 -> 像素,一层层深入;当然这个过程中还包括三维观察坐标变换,以及按z-buffer
处理遮挡。最后在framebuffer
中为每个像素点设置颜色。screen_update
:调用windows api将framebuffer
中的像素显示到屏幕上。
功能扩展
以上只是概述。话说要深入理解一段代码,最好的办法还是动手改点东西。
我们来看看有哪些新feature是可以添加进去的。
- 原作只支持旋转和前后平移(其实是相机的平移,而非方块),我们很容易想到可以完善三维变换的所有功能:平移(包括六个方向)、旋转、缩放。
- 现在的方块无论从哪个角度看都一个样,原因是缺乏光照。我们可以添加简单的光照,如漫反射和镜面反射。
完善三维变换功能
添加三维变换的关键是:在三维观察坐标变换的流水线中,找到合适的地方对顶点做三维变换。
我们先处理缩放和旋转,通过改写draw_box:
void draw_box(device_t *device, float alpha, float scale) {
matrix_t m1;
matrix_set_scale(&m1, scale, scale, scale);
matrix_t m2;
matrix_set_rotate(&m2, -1, -0.5, 1, alpha);
matrix_t m;
matrix_mul(&m, &m1, &m2);
device->transform.world = m;
再在transform_apply中处理平移, 插入点是在完成相机坐标系的变换之后:
void transform_apply(const transform_t *ts, vector_t *y, const vector_t *x, vector_t *_3d_transform) {
vector_t t1;
vector_t t2;
vector_t t3;
matrix_apply(&t1, x, &ts->world);
matrix_apply(&t2, &t1, &ts->view);
vector_add(&t3, &t2, _3d_transform);
matrix_apply(y, &t3, &ts->projection);
}
增加简单光照
这里实现的是简单的漫反射。
根据漫反射的实现原理,物体上的顶点在光照下的颜色取决于以下因素:
- 材质颜色
- 光的颜色
- 光的入射方向
- 顶点所在平面的法向量
按照这些因素一个个来实现。
先定义光源:
// 平行光源
typedef struct {
color_t color; // 颜色
vector_t direction; // 方向
} light_t;
再根据平面上三点计算法向量:
// 计算平面法向量
void calc_plane_normal(const transform_t *ts, vector_t *normal, const vector_t *x, const vector_t *y, const vector_t *z) {
vector_t wx;
vector_t wy;
vector_t wz;
vector_t xy;
vector_t yz;
matrix_apply(&wx, x, &ts->world);
matrix_apply(&wy, y, &ts->world);
matrix_apply(&wz, z, &ts->world);
vector_sub(&xy, &wy, &wx);
vector_sub(&yz, &wz, &wy);
vector_crossproduct(normal, &xy, &yz);
vector_normalize(normal);
}
然后将法向量传入device_draw_scanline
,插入下面一段代码,真正实现光照对颜色的影响:
// 处理光照
float dot = vector_dotproduct(&device->light.direction, normal);
if (dot < 0)
dot = -dot;
b *= dot * device->light.color.b;
g *= dot * device->light.color.g;
r *= dot * device->light.color.r;
最后实现的效果如图(迎着摄像机的一面因光线影响较亮):
代码提交到github了,可供参考。
小结
阅读mini3d代码所需基础要求:
- 理解三维观察坐标变换流水线
- 理解z-buffer的用法
- 理解三维变换(平移、旋转、缩放)
- 了解windows编程
不清楚的部分需要查询计算机图形学书籍或相关资料。
实现这么个软渲染,最大意义在于理论联系实际,将理论知识真正落地。麻雀虽小,五脏俱全。以后还可以继续扩展更多的功能。