以下谈到的项目在GitHub:shuaitq/Aurora可以找到全部源码

前言

这个东西其实是在大一寒假自学计算机图形学鼓捣出来的,后面被拿去当大一年度项目水了水,终检的时候老师也没有太多的反映,也不知道老师看懂了没,反正最后就水过了也是挺开心的。不过为什么这个时候才来写了这篇博客呢?当然是来填坑的啦,当时做完就直接丢到GitHub上没管了,各种东西都没搞,有几个用户体验的痛点功能也没有做,反正项目通过了就行了(。正如那句话说得好,挖的坑迟早要填的,于是我就回来填这个坑了233。

光栅化

看到光栅化这个词大多数人可能会都不太清楚这个干嘛的。其实这个东西在大家生活中非常常见,除了离线渲染在好多年前就上了光线追踪以外,不管是游戏、字体渲染还是电脑建模、渲染,实际上都离不开光栅化。那光栅化是什么呢,如下图所示,光栅化其实就是把模型的一个三角面编程屏幕上覆盖的像素点。

但是这样其实是有坏处的,因为这种方法并没有真正的模拟光的传播,导致光影效果一直很难做,包括且不止:软阴影、镜面反射、玻璃透射、水面。就算用了各种trick的方式做到了,也和现实物理相差甚远。但大家为什么都“喜欢”这么做呢,因为快啊,基于光线追踪渲染的话计算量太大,根本远远满足不了每秒60帧的要求。俗话说的好:巧妇难为无米之炊,没办法大家只好用这种方法度过了这么多年。这也就是为什么光栅化到现在还是在线渲染的大霸主。

为什么选择固定管线实现

图形管线其实分了两种,固定管线和可编程管线。为什么选择固定管线呢,并不是因为我懒,真的。只是我找到学习图形学的书《3D游戏编程大师技巧》是2012年7月13日出版的,而且还是翻译版。所以我一开始只知道固定管线,并不知道有可编程管线这种东西,所以也就没多想,就按照固定管线实现了。

重点部分及实现

使用json格式定义场景、相机、灯光等参数

为了能够方便的定义场景,设置场景里的模型、相机、灯光等参数。采用了json格式文件定义场景,并采用nlohmann/json库对定义的场景进行解析。具体场景文件格式可以参看配置文件格式

采用obj模型,ppm图片贴图,双线性过滤进行采样

渲染器采用的3D模型格式是obj,这是一种非常常见的3D模型格式,也很好解析,就是有很多软件的私货。于是我选择了

1
usemtl chess.ppm

这样的添加来作为模型贴图的支持。贴图因为并没有支持什么mipmap、法线贴图的高端技术所以直接使用一个图片就行,正好之前用到了ppm图片格式的解析,于是拿来改了改就直接用了。

而双线性过滤呢,就按照取样点四周四个像素的加权平均来算的,权值呢就是离每个像素的距离,使用平铺纹理坐标寻址。

 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
RGB_T<double> Texture::Sample(double u, double v) const
{
    if(pixels.empty())
    {
        return RGB_T<double>(0.9);
    }
    // (-inf, +inf)
    u *= size;
    v *= size;
    // (-inf * size, +inf * size)
    auto f = [&](double a, double b)
    {
        int x = floor(u + a);
        int y = floor(v + b);
        x = ((x % size) + size) % size;
        y = ((y % size) + size) % size;
        return pixels[size * y + x] * fabs((u + a) - floor(u + 0.5)) * fabs((v + b) - floor(v + 0.5));
    };
    RGB_T<double> ret = f(0.5, 0.5);
    ret += f(0.5, -0.5);
    ret += f(-0.5, 0.5);
    ret += f(-0.5, -0.5);
    return ret;
}

支持方向光和点光源两种光源

有一个Light类有光照接口的定义,可以继承这个类实现各种各样的光照类型,我这里实现了方向光和点光源,其中点光源我实现的是按照距离的平方衰减的。

1
2
virtual RGB_T<double> Sample(const Vector4D_T<double> &p, const Vector4D_T<double> &n) = 0;

方向光

1
2
3
4
5
6
RGB_T<double> DirectLight::Sample(const Vector4D_T<double> &p, const Vector4D_T<double> &n)
{
    double NdotD = Dot(n, VD);
    return color * std::max(0.0, -NdotD);
}

点光源

1
2
3
4
5
6
7
8
RGB_T<double> PointLight::Sample(const Vector4D_T<double> &p, const Vector4D_T<double> &n)
{
    double length2 = (p - VP).Length2();
    Vector4D_T<double> direction = Normalize(p - VP);
    double NdotD = Dot(n, direction);
    return color * std::max(0.0, -NdotD) / length2;
}

Z-Buffer保证渲染正确的顺序,背面消影和三角形剔除(未实现三角形裁剪)

Z-Buffer

Z-Buffer我是创建了一个double类型的vector分辨率和输出分辨率一样来存储z值即深度。在每次渲染新的像素到结果的时候都会判断一下当前z值是否比之前的小,小就代表在之前像素的前面,这样就可以正确的实现先后顺序。这里因为z值预先变成了1/z所以是>=。

1
2
3
4
5
if(OnePreZ >= depth[y * width + x])
{
    ...
}

背面消隐

背面消隐就是转换到相机空间的时候对三角面的三个顶点按照一定顺序做一个叉积,根据叉积的结果判断这个面是正面朝相机还是反面朝相机,如果反面朝向相机直接丢弃即可,不需要渲染。

1
2
3
4
5
bool Render::IsBackFace(const Vector4D_T<double> &v0, const Vector4D_T<double> &v1, const Vector4D_T<double> &v2)
{
    return Dot(v0, Cross(v1 - v0, v2 - v0)) >= 0;
}

三角形剔除

三角形剔除呢,这里只做了最简单的三角形剔除,就是把近裁剪面和远裁剪面之间的保留下来,其他的面全部剔除。而且并没有实现裁剪功能,越过近裁剪面和远裁剪面的三角面会直接整体被剔除。

1
2
3
4
5
6
if(v[i].point.z < 0.0 || v[i].point.z > 1.0)
{
    OutOfRange = true;
    break;
}

渲染结果

不同贴图

斯坦福兔子

不同光源 (平行光和点光源

彩色光源叠加 (左边黄色平行光,右边蓝色点光源

配置文件格式

 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
{
    "width" : 1920,                                         
    "height" : 1080,                                        
    "camera" :{                                             相机参数
        "position" : [0.800000, 0.800000, 0.800000],        位置
        "u" : [0.707106, -0.707106, 0],                     右向量
        "v" : [-0.408248, -0.408248, 0.816496],             上向量
        "n" : [-0.577349, -0.577349, -0.577349],            前向量
        "fov" : 80,                                         视角
        "near" : 0.1,                                       近裁剪面
        "far" : 1000                                        远裁剪面
    },
    "object" :[                                             模型参数
        {
            "path" : "cube.obj",                            obj文件目录
            "position" : [0.000000, 0.000000, 0.000000],    位置
            "u" : [1, 0, 0],                                同相机
            "v" : [0, 1, 0],
            "n" : [0, 0, 1]
        }
    ],
    "light" :[                                              光源参数
        {
            "type" : "DirectLight",                         方向光
            "direction" : [-0.267261, -0.534522, -0.801784],方向
            "color" : [1, 1, 1]                             颜色
        },
        {
            "type" : "PointLight",                          点光源
            "position" : [1, 1, 1],                         位置
            "color" : [0.5, 0.5, 0.5]                       颜色
        }
    ]
}

参考

《3D游戏编程大师技巧》