流水线¶
- 基本流程:
- CPU 准备要渲染你的数据(位置信息、法线方向、顶点颜色等),并将数据加载到显存
- CPU 设置渲染状态(顶点着色器、片元着色器、光源、材质等)
- 调用 Draw Call 使用 GPU 开始绘制
- CPU 准备要渲染你的数据(位置信息、法线方向、顶点颜色等),并将数据加载到显存
- 逐片元操作中通过模板测试、深度测试决定可见性,之后进行混合,输出到颜色缓冲区
光栅化¶
- 光栅化:将图像显示在屏幕上,矢量图形转化为像素网格
- 视锥
- 影响因素:长宽比、垂直可视角度(红线角度)
- 屏幕
- 将标准正方体映射到屏幕上
- 使用最简单的多边形——三角形来表示一切
-
最简单多边形,任何图形都可以划分为三角形
-
判断点是否在三角形内部
- 通过叉乘得到三个向量后,可以通过两两点乘来确定方向是否相同
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;
}
- 简易采样(就是离散化)决定像素是否显示
- 使用像素中心进行采样
-
- 判断中心点是否在三角形内部(使用向量叉乘,在三条边同方向)
- 不需要遍历所有的点,可以使用包围盒进行优化
- 更加优秀的算法
-
但是这种方法会出现严重的走样问题
反走样&抗锯齿¶
- 时间、空间上的问题,采样频率不足就会引起走样
-
先模糊后采样
-
- 傅里叶级数展开
- 傅里叶变换:实现函数在时域和频域之间的变化
- 仅仅有频谱(振幅谱)是不够的,我们还需要一个相位谱(不同波的起始相位)
- 对于图片来说
- 时域表示图片上的像素变化快慢
-
频域用黑色图表示,中间为低频四周为高频
-
滤波
- 在频域可以十分方便的实现滤波(去除指定竖线(去除某个频率))
- 卷积
- 信号范围内平均处理
- 时域的相乘等于频域的卷积,时域的卷积等于频域相乘
- 采样就是重复原始信号频谱
- 左侧的在时域上相乘,域右侧上做卷积是等价的
- 采样率过低时会发生频谱重叠
- 模糊(卷积)可以减少重叠,减少走样(屏幕分辨率高时走样少,是因为采样频率高)
- MSAA多采样反走样(卷积计算开销大)
- 像素内部添加更多的采样点
- 覆盖采样点数目来决定模糊状态(抗锯齿)
TAA¶
- 将多次采样的过程分布到每一帧当中去,也即每一帧都会采样历史几帧的数据(而不是)
- \(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)::通过分析图像的局部模式和纹理信息,超分辨率算法尝试推断高分辨率图像中可能出现的细节和结构。通过学习大量低分辨率与高分辨率图像对应的关系,能够生成高质量的高分辨率图像。
深度缓冲(测试)¶
- 如何绘制深度不同具有遮挡关系的不同图像(三角形)?
- 深度缓冲
- 生成图象时额外生成一个深度图,用于表示每个像素的深度信息
- 根据深度信息判断是否要进行覆盖
着色¶
- 对不同物体应用不同材质
- 着色只考虑物体自身,不考虑其他的物体的影响(如阴影等)
Blinn-Phong 反射模型¶
- 高光、漫反射、环境光
- 漫反射
- 平面与光线的夹角会影响反射的强度,接受的光的比率可以使用 \(cos\theta\) 表示
- 对于点光源,距离光源的距离也会影响反射的强度(总能量一定,距离越大半径越大,单位面积自然越小)对于半径为 r 的位置,光照强度使用 \(I/r^2\) 来表示
- 漫反射与 v 的方向无关(均匀反射)
- 高光
- 观察方向和镜面反射方向接近时看到高光
- 通过半程向量(角平分线)和法线的夹角判断接近程度
-
用指数表示高光衰减
-
环境光
- 认为环境光恒定
-
-
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; }
着色频率¶
- 面着色
- 点着色
- 像素着色
- 确定顶点的法线
- 使用相邻面的法线来求平均
- 像素的法线:使用重心坐标确定
- 随着采样频率提升(模型面数)不同着色频率之间的差距越来越小
实时渲染管线¶
- 指渲染的一系列过程,图像是如何渲染出来的
- 输入三维空间的点
- 投影到二维平面上(mvp 矩阵变换)
- 点构成三角形
- 对三角形进行光栅化(采样(反走样)+深度缓冲)
- 对三角形进行着色(如布林冯反射模型、纹理摸映射)
纹理映射¶
- 定义任何一个点的属性
- 纹理映射:3 维->2 维
-
通过坐标三角形顶点颜色映射
-
知道了顶点的着色,还需要插值,对内部其他点进行着色(如何通过三角形的顶点得到内部参数的平滑过渡)
-
重心坐标
- 三角型内任一点可以使用顶点坐标的线性组合表示(参数和为 1(在三角形所在平面上)且非负(在三角形内部))
- 可以使用三角形面积之比计算出来
- 将任何一点的转化为用顶点表示
- 计算得到参数后可以用这几个参数做插值 \(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\)
- 本质上就是求解这个方程组(之后得到 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);
}
}
}
}







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