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

TAA

  • 将多次采样的过程分布到每一帧当中去,也即每一帧都会采样历史几帧的数据(而不是)
    • image.png|450
    • \(P_t=(1-\alpha)\cdot P_{t-1}+\alpha\cdot c_t\) 动态混合,结合上一帧的历史信息 \(P_{t-1}\) 和当前帧的颜色结果 \(c_{t}\)
  • 由于物体运动,遮挡状况会发生变化,可能产生鬼影和闪烁问题,有以下优化
    • 设置色差阈值,对比当前帧和历史帧,将历史帧的颜色截断在合理的范围内。
    • 抖动投影 (Jitter),在每一帧渲染时,给相机投影矩阵加入亚像素级的偏移(通常使用预先定义的抖动序列,如 Halton、蓝噪声),使得同一场景多帧渲染的采样点稍有不同。
    • 使用运动向量修正坐标,根据运动向量,将上一帧累积的颜色、深度等信息重投影到当前帧像素上。
  • 基本思路
    输入:
      当前帧渲染颜色 C_cur[i,j]
      深度 D_cur[i,j]
      运动向量 MV[i,j] (从前帧到当前帧的屏幕空间偏移)
      历史累积颜色 C_hist_last
      历史累积深度 D_hist_last
    
    输出:
      当前累积颜色 C_accum
      更新后的历史颜色 C_hist_new, 深度 D_hist_new
    
    参数:
      α(历史权重系数,0~1)
    
    步骤:
    1. 投影抖动
       // 在渲染过程中,已对投影矩阵进行 jitter 偏移
    
    2. 重投影历史颜色
       for each 像素 p = (i,j):
         p_prev = p + MV[i,j]        // 将当前像素映射回前一帧位置
         C_reproj = sample(C_hist_last, p_prev)
         D_reproj = sample(D_hist_last, p_prev)
    
    3. 验证有效性(抗鬼影处理)
         如果 |D_reproj - D_cur[i,j]| > depthThreshold 或
            |C_reproj - C_cur[i,j]| > colorThreshold:
             C_reproj = C_cur[i,j]
             α_effective = 0
         否则:
             α_effective = α
    
    4. 融合
         C_accum[i,j] = lerp(C_cur[i,j], C_reproj, α_effective)
         // lerp(a,b,α) = α*b + (1-α)*a
    
    5. 更新历史缓冲
         C_hist_new[i,j] = C_accum[i,j]
         D_hist_new[i,j] = D_cur[i,j]
    
    6. 输出 C_accum 作为最终颜色
    

采样插值

  • 按照参数的多项式的最高次数可以分成一次插值(如双线性插值)、二次插值等
Lanczos 插值
  • 权重分布是钟形(近似sinc函数形状),中心大、远离中心迅速减小。
  • 通常会采样更多邻居像素(如3x3、4x4),不是只看最近的四个。
  • 能更好地保留高频细节、抗混叠,画质更佳,但计算略复杂。
  • \(L(x)=\begin{cases}\frac{a\cdot\sin(\pi x)\cdot\sin(\pi x/a)}{(\pi x)^2},&|x|<a\\0,&\mathrm{otherwise}&\end{cases}\)
    • 其中 x 为距离中心的归一化距离
    • a 为窗口参数,决定核的宽度
  • 代码示例

    float lanczos_weight(float x)
    {
        //return fastLanczos(x);
        float a = 0.75;
        float pi = 3.141592653589793;
        x = abs(x);
        /*if (x == 0.0)
            return 1.0;*/
        /*if (x > a)
            return 0.0;*/
        float x_safe = x;
        float pi_x = pi * x_safe;
        float pi_x_over_a = pi * x_safe / a;
        float xe0 = step(x, 0);
    
        return (a * sin(pi_x) * sin(pi_x_over_a)) / (pi_x * pi_x); // * (1 - xe0) * step(x, a) + xe0;
    }
    

  • 快速(近似)Lanczos 插值

    //输入到中心店的偏移距离,输出对应的权重
    float fastLanczos(float base)  
    {  
        float y = base - 1.0;  
        float y2 = y * y;  
        float y_temp = 0.75 * y + y2;  
        return abs(y_temp * y2);  
    }
    

基于 Catmull-Rom 样条插值的双三次采样
float4 BiCubicCatmullRom5Tap(Texture2D<float4> iChannel0, float2 P, float2 c_textureSize)  
{  
    float2 InvSize = 1.0 / c_textureSize;  
    float2 Weight[3];  
    float2 Sample[3];  
    float2 UV = P * c_textureSize;      
//计算得到所在格子的中心      
float2 tc = floor(UV - 0.5) + 0.5;  
    //预计算  
    float2 f = UV - tc;  
    float2 f2 = f * f;  
    float2 f3 = f2 * f;  
    float2 w0 = f2 - 0.5 * (f3 + f);  
    float2 w1 = 1.5 * f3 - 2.5 * f2 + float2(1.0f, 1.0f);  
    float2 w3 = 0.5 * (f3 - f2);  
    float2 w2 = float2(1.0f, 1.0f) - w0 - w1 - w3;  
        Weight[0] = w0;  
    Weight[1] = w1 + w2;  
    Weight[2] = w3;  

    Sample[0] = tc - float2(1.0f,1.0f);  
    Sample[1] = tc + w2 / Weight[1];  
    Sample[2] = tc + float2(2.0f,2.0f);  

    Sample[0] *= InvSize;  
    Sample[1] *= InvSize;  
    Sample[2] *= InvSize;  

    float sampleWeight[5];  
    sampleWeight[0] = Weight[1].x * Weight[0].y;  
    sampleWeight[1] = Weight[0].x * Weight[1].y;  
    sampleWeight[2] = Weight[1].x * Weight[1].y;  
    sampleWeight[3] = Weight[2].x * Weight[1].y;  
    sampleWeight[4] = Weight[1].x * Weight[2].y;  

    float4 Ct = iChannel0.SampleLevel(smpLinearClamp,float2(Sample[1].x, Sample[0].y),0) * sampleWeight[0];  
    float4 Cl = iChannel0.SampleLevel(smpLinearClamp,float2(Sample[0].x, Sample[1].y),0) * sampleWeight[1];  
    float4 Cc = iChannel0.SampleLevel(smpLinearClamp,float2(Sample[1].x, Sample[1].y),0) * sampleWeight[2];  
    float4 Cr = iChannel0.SampleLevel(smpLinearClamp,float2(Sample[2].x, Sample[1].y),0) * sampleWeight[3];  
    float4 Cb = iChannel0.SampleLevel(smpLinearClamp,float2(Sample[1].x, Sample[2].y),0) * sampleWeight[4];  
    float WeightMultiplier = 1. / (sampleWeight[0] + sampleWeight[1] + sampleWeight[2] + sampleWeight[3] + sampleWeight[4]);  

return (Ct + Cl + Cc + Cr + Cb) * WeightMultiplier;  
}

反走样的方法总结

  • 抗锯齿
    • 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