流水线¶
- 基本流程:
- CPU 准备要渲染你的数据(位置信息、法线方向、顶点颜色等),并将数据加载到显存
- CPU 设置渲染状态(顶点着色器、片元着色器、光源、材质等)
- 调用 Draw Call 使用 GPU 开始绘制
- 逐片元操作中通过模板测试、深度测试决定可见性,之后进行混合,输出到颜色缓冲区
光栅化¶
- 光栅化:将图像显示在屏幕上,矢量图形转化为像素网格
- 视锥
- 影响因素:长宽比、垂直可视角度(红线角度)
- 屏幕
- 将标准正方体映射到屏幕上
- 使用最简单的多边形——三角形来表示一切
-
最简单多边形,任何图形都可以划分为三角形
-
判断点是否在三角形内部
- 通过叉乘得到三个向量后,可以通过两两点乘来确定方向是否相同
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多采样反走样(卷积计算开销大)
- 像素内部添加更多的采样点
- 覆盖采样点数目来决定模糊状态(抗锯齿)
反走样的方法总结¶
- 抗锯齿
- 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);
}
}
}
}
纹理应用¶
- 环境光照
- 上下扭曲严重,不是均匀描述
- 立方体包围法
- 凹凸贴图
- 定义点相对基础面的相对高度
- 通过法线差异模拟高度变化
- 扰动法线
- 法线通过求导得到的切线旋转得到
- 二维图像上同理通过黑白变化计算
- 位移贴图
- 与凹凸贴图的输入相同,但是位移贴图真正改变几何信息,对顶点做位移相比上更逼真,因为凹凸贴图在边界上会露馅