Skip to content

流水线

  • 基本流程:
    • CPU 准备要渲染你的数据(位置信息、法线方向、顶点颜色等),并将数据加载到显存image.png|450
    • CPU 设置渲染状态(顶点着色器、片元着色器、光源、材质等)
    • 调用 Draw Call 使用 GPU 开始绘制image.png|500
  • 逐片元操作中通过模板测试、深度测试决定可见性,之后进行混合,输出到颜色缓冲区
    • image.png|500

光栅化

  • 光栅化:将图像显示在屏幕上,矢量图形转化为像素网格
  • 视锥
  • image-20230705195643643
  • 影响因素:长宽比垂直可视角度(红线角度)
  • 屏幕
  • image-20230705200526116
  • 将标准正方体映射到屏幕上
  • image-20230705200732208
  • 使用最简单的多边形——三角形来表示一切
  • 最简单多边形,任何图形都可以划分为三角形

  • 判断点是否在三角形内部

  • 通过叉乘得到三个向量后,可以通过两两点乘来确定方向是否相同
static bool insideTriangle(int x, int y, const Vector3f* _v)
{   
    Vector3f AB = _v[1] - _v[0];
    Vector3f BC = _v[2] - _v[1];
    Vector3f CA = _v[0] - _v[2];
    Vector3f AP = Vector3f(x, y, 0) - _v[0];
    Vector3f BP = Vector3f(x, y, 0) - _v[1];
    Vector3f CP = Vector3f(x, y, 0) - _v[2];
    Vector3f cross1 = AB.cross(AP);
    Vector3f cross2 = BC.cross(BP);
    Vector3f cross3 = CA.cross(CP);
    if(cross1.dot(cross2) > 0 && cross2.dot(cross3) > 0 && cross3.dot(cross1) > 0)
        return true;   
    return false;
}
  • 简易采样(就是离散化)决定像素是否显示
  • 使用像素中心进行采样image-20230705202512612
  • image-20230705202526369

    • 判断中心点是否在三角形内部(使用向量叉乘,在三条边同方向)
    • 不需要遍历所有的点,可以使用包围盒进行优化
    • image-20240318192736159
    • 更加优秀的算法
    • image-20240318192907431
  • 但是这种方法会出现严重的走样问题

    • image-20230705204001597

反走样&抗锯齿

  • 时间、空间上的问题,采样频率不足就会引起走样
  • image-20230705210016059
  • 先模糊后采样

  • image-20230705205023851

  • 傅里叶级数展开
  • image-20230705205535746
  • 傅里叶变换:实现函数在时域和频域之间的变化
  • image-20230705205610906
  • img
  • 仅仅有频谱(振幅谱)是不够的,我们还需要一个相位谱(不同波的起始相位)
    • img
  • 对于图片来说
  • 时域表示图片上的像素变化快慢
  • 频域用黑色图表示,中间为低频四周为高频

  • 滤波

  • 频域可以十分方便的实现滤波(去除指定竖线(去除某个频率))
  • 卷积
    • image-20230705212537943
    • 信号范围内平均处理
  • 时域的相乘等于频域的卷积,时域的卷积等于频域相乘
    • image-20230705212715013
  • 采样就是重复原始信号频谱image-20230705230234448
    • 左侧的在时域上相乘,域右侧上做卷积是等价的
    • 采样率过低时会发生频谱重叠
    • image-20230705225914520
  • 模糊(卷积)可以减少重叠,减少走样(屏幕分辨率高时走样少,是因为采样频率高
    • image-20230705230138093
    • image-20230705230342691
  • MSAA多采样反走样(卷积计算开销大)
  • 像素内部添加更多的采样点image-20230705230610889
  • 覆盖采样点数目来决定模糊状态(抗锯齿)image-20230705230656738

反走样的方法总结

  • 抗锯齿
    • MSAA:像像素内添加更多的采样点,根据覆盖采样点的数目决定显示状态(不透明度)
    • FXAA:分析渲染后的图像来检测锯齿边缘,性能开销较低
    • TAA:通过多帧信息提高图像质量,在时间上的累计和平滑,减少锯齿同时保持图像细节
  • 超分辨率
    • 深度学习(DLSS)::通过分析图像的局部模式和纹理信息,超分辨率算法尝试推断高分辨率图像中可能出现的细节和结构。通过学习大量低分辨率与高分辨率图像对应的关系,能够生成高质量的高分辨率图像。

深度缓冲(测试)

  • 如何绘制深度不同具有遮挡关系的不同图像(三角形)?
  • image-20230705232435826
  • 深度缓冲
  • 生成图象时额外生成一个深度图,用于表示每个像素的深度信息
  • image-20230705232748652
  • 根据深度信息判断是否要进行覆盖
    • image-20230705233302274
    • image-20230705233135393

着色

  • 对不同物体应用不同材质
  • 着色只考虑物体自身,不考虑其他的物体的影响(如阴影等)

Blinn-Phong 反射模型

  • 高光、漫反射、环境光
  • image-20230705234429839
  • 漫反射
  • 平面与光线的夹角会影响反射的强度,接受的光的比率可以使用 \(cos\theta\) 表示
    • image-20230705235058342
  • 对于点光源,距离光源的距离也会影响反射的强度(总能量一定,距离越大半径越大,单位面积自然越小)对于半径为 r 的位置,光照强度使用 \(I/r^2\) 来表示
    • image-20240318205850062
  • image-20230705235520417
  • 漫反射与 v 的方向无关(均匀反射)
  • 高光
  • 观察方向和镜面反射方向接近时看到高光
    • image-20230706092213901
  • 通过半程向量(角平分线)和法线的夹角判断接近程度
    • image-20240318210959363
  • 用指数表示高光衰减

    • image-20230706093241355
  • 环境光

  • 认为环境光恒定
  • image-20230706094302227

  • image-20230706094343294

    Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload)
    {
        //环境光、漫反射、镜面反射
        Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
        Eigen::Vector3f kd = payload.color;
        Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);
        //光源
        auto l1 = light{{20, 20, 20}, {500, 500, 500}};
        auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
        std::vector<light> lights = {l1, l2};
        //环境光强度
        Eigen::Vector3f amb_light_intensity{10, 10, 10};
        //观察者位置
        Eigen::Vector3f eye_pos{0, 0, 10};
    
        float p = 150;
    
        Eigen::Vector3f color = payload.color;
        Eigen::Vector3f point = payload.view_pos;
        Eigen::Vector3f normal = payload.normal;
    
        Eigen::Vector3f result_color = {0, 0, 0};
        for (auto& light : lights)
        {
            Eigen:Vector3f ambient = amb_light_intensity.cwiseProduct(ka);
            Eigen::Vector3f light_dir = (light.position - point).normalized();
            float r2= (light.position - point).squaredNorm();
            Eigen::Vector3f diffuse = kd.cwiseProduct(light.intensity) / r2 * std::max(0.0f, normal.normalized().dot(light_dir));
            Eigen::Vector3f view_dir = (eye_pos - point).normalized();
            Eigen::Vector3f half_dir = (view_dir + light_dir).normalized();
            Eigen::Vector3f specular = ks.cwiseProduct(light.intensity) / r2 * std::pow(std::max(0.0f, normal.normalized().dot(half_dir)), p);
            result_color += ambient + diffuse + specular;
        }
    
        return result_color * 255.f;
    }
    

着色频率

  • image-20230706094654726
  • 面着色
  • 点着色
  • 像素着色
  • 确定顶点的法线
  • 使用相邻面的法线来求平均
  • image-20230706095747210
  • 像素的法线:使用重心坐标确定
  • image-20230706100014116
  • 随着采样频率提升(模型面数)不同着色频率之间的差距越来越小
  • image-20240318212454444

实时渲染管线

  • 指渲染的一系列过程,图像是如何渲染出来的
  • image-20230706100232686
  • 输入三维空间的点
  • 投影到二维平面上(mvp 矩阵变换)
  • 点构成三角形
  • 对三角形进行光栅化(采样(反走样)+深度缓冲)
  • 对三角形进行着色(如布林冯反射模型、纹理摸映射)

纹理映射

  • 定义任何一个点的属性
  • image-20230706154430087
  • 纹理映射:3 维->2 维
  • image-20230706155402173
  • 通过坐标三角形顶点颜色映射

  • 知道了顶点的着色,还需要插值,对内部其他点进行着色(如何通过三角形的顶点得到内部参数的平滑过渡)

  • 重心坐标

  • 三角型内任一点可以使用顶点坐标的线性组合表示(参数和为 1(在三角形所在平面上)且非负(在三角形内部))
  • image-20230706161338947
  • 可以使用三角形面积之比计算出来
    • image-20230706162806562
  • 将任何一点的转化为用顶点表示
  • image-20230706163045057
  • 计算得到参数后可以用这几个参数做插值 \(V=\alpha V_A+\beta V_B+r V_C\)
  • 问题:投影之后的重心坐标会发生变化,因此要先插值再投影
  • 如果需要再投影之后进行插值,则需要进行透视矫正插值

  • 计算质心坐标

    • \(P=(1-u-v)A+uB+vC\) (对于三角形的点 u, v, 1-u-v 均大于 0)
    • \(P=A+uAB+vAC\to uAB+vAC+PA=0\)
    • image.png|271
    • 本质上就是求解这个方程组(之后得到 uv 判断是否大于零,进而判断是否在三角形内部)对于三维点也是一样的,因为两个方程就能解出两个量了
    • 求解方程就是找一条直线与 (ABx, ACx, PAx) and (ABy, ACy, PAy) 垂直,这可以通过一次叉积解决
  • 透视矫正

    • 因为线性插值假设了所有点在同一平面上,但实际上,由于透视效果,远处的物体应该在视觉上显得更小,这导致了深度(z 值)和其它依赖于深度的属性不能简单地线性插值。(比如深度变化的的位置的颜色变化在投影之后被压密了
    • 透视变换和透视除法确保了三维场景在二维屏幕上的正确投影,但是在处理纹理映射和顶点属性插值时,直接应用线性插值会因为透视效果的非线性特征而导致失真。
    • TODO

void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos) 
{
    auto v = t.toVector4();
    float aabb_min_x = std::min(v[0].x(), std::min(v[1].x(), v[2].x()));
    float aabb_min_y = std::min(v[0].y(), std::min(v[1].y(), v[2].y()));
    float aabb_max_x = std::max(v[0].x(), std::max(v[1].x(), v[2].x()));
    float aabb_max_y = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));
    for(int i=aabb_min_x; i<=aabb_max_x; i++){
        for(int j=aabb_min_y; j<=aabb_max_y; j++){
            if(!insideTriangle(i, j, t.v))
                continue;
            auto[alpha, beta, gamma] = computeBarycentric2D(i, j, t.v);
            float Z = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
            float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
            zp *= Z;
            int index = get_index(i, j);
            if(zp < depth_buf[index]){
                depth_buf[index] = zp;
                auto interpolated_color = interpolate(alpha, beta, gamma, t.color[0], t.color[1], t.color[2], 1.0);
                auto interpolated_normal = interpolate(alpha, beta, gamma, t.normal[0], t.normal[1], t.normal[2], 1.0);
                auto interpolated_texcoords = interpolate(alpha, beta, gamma, t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1.0);
                auto interpolated_shadingcoords = interpolate(alpha, beta, gamma, view_pos[0], view_pos[1], view_pos[2], 1.0);
                fragment_shader_payload payload(interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
                payload.view_pos = interpolated_shadingcoords;
                auto pixel_color = fragment_shader(payload);
                set_pixel(Eigen::Vector2i(i, j), pixel_color);
            }
        }
    }
}
- 纹理映射(双线性插值)(纹理过小) - 当显示分辨率远高于纹理分辨率时可能存在映射问题,如坐标转化后为小数,可能使得多个坐标映射到相同的纹理位置,造成显示不准确 - 采用周围 4 个点的数值进行插值(两次水平一次垂直) - image-20230706172208264 - Mipmap 范围查询(纹理过大) - image-20230706174502149 - 范围查询:快速获取一个(方形)区域内颜色的近似平均值 - 建立不同的层 - - mipmap 所占用的额外空间为原图的1/3 - 映射相邻点区域,选取距离最大值为映射范围正方型的边长image-20230706175452958 - 问题:不同层之间的过度不连续 - image-20230706180309960 - 不同层上双线性插值,再在两层之间插值。(三线性插值) - 并不是所有像素都是压缩正方形(过采样) - image-20230706181304364 - 各向异性过滤(对长条区域快速范围查询)总开销为原本的三倍 - image-20230706181428953

纹理应用

  • 环境光照
  • image-20230706182726472
  • 上下扭曲严重,不是均匀描述image-20230706183455340
  • 立方体包围法image-20230706183506530
  • image-20230706183515877
  • 凹凸贴图
  • 定义点相对基础面的相对高度
  • image-20230706183842672
  • 通过法线差异模拟高度变化
  • 扰动法线image-20230706184104600
    • 法线通过求导得到的切线旋转得到
    • image-20230706184802484
  • 二维图像上同理通过黑白变化计算image-20230706184817165
  • 位移贴图
    • 与凹凸贴图的输入相同,但是位移贴图真正改变几何信息,对顶点做位移相比上更逼真,因为凹凸贴图在边界上会露馅
    • image.png