VZRenderer
tgaimage
图像相关操作 所有图片均以tga格式存储 用于读取和存储图片(纹理) 其中TGAColor是以BGRA的顺序存储的 范围[0,255] TGAImage主要用set()来写 get()来读
geometry
几何 模板实现向量以及矩阵相关的操作 主要注意的点为成员函数与非成员函数的区分
camera
摄像机类 存有世界坐标里的相机和目标位置坐标以及 目标的camera坐标系(z为目标指向相机)
handle_events() 根据window所存的鼠标以及键盘相关变量来更改camera中所存的坐标 updata_camera_pos() 更新坐标
其中还存有ViewMatrix 矩阵以及PerspectiveMatrix矩阵函数
ViewMatrix:
将整个场景从相机位置反向平移到原点 (MT),然后以反向方向 (MR) 旋转场景,因此相机位于原点并面向 -Z 轴
MT即camera的位置的负值平移
MR的目的是让目标的camera坐标系和世界坐标系中XYZ重合 旋转矩阵均为正交矩阵 即逆变换=转置 所以先让世界坐标的XYZ和camera坐标系重合再转置即得MR
(l,u,v)即为camera坐标系中xyzPerspectiveMatrix:
代码中r为半宽长 = (r-l)/2 t同理 r+l=t+b=0透视投影:
个人所见三种透视投影矩阵:- -
和正交投影直接去掉z具有相似的问题 不可逆,丢失了 z,没有考虑多个点的重合情况 - -
以上为tinyrenderer中做法 对1进行了改良 增加了一个平移 使得变换后的z仍具有变换之前的单调性和大小关系 - 项目中所用 可以剔除自定义近平面远平面以外的
- -
model
用于模型文件.obj文件的导入与转化 转化为顶点数组verts_ 面数组faces_ 法线数组norms_ uv数组uvs_
同时提供相应查找方法查找具体数值
存储一个纹理的指针指向模型所要应用的纹理
兼容天空盒的读取
读取的模型应均为三角面 否则后续裁剪会出问题
texture
构造时即可读取模型文件同目录下的所有纹理图片 同时储存 提供纹理对应的采样函数(输入uv返回对应值) 以及为ibl准备的非类成员函数的纹理以及正方形贴图的采样函数
相关性质: diffuse normal specualr roughness metalness occlusion emission cubemap
- 纹理采样: https://zhuanlan.zhihu.com/p/143377682
纹理过滤 Texture Filtering (项目中仅采用邻近点- 邻近点(Nearest)
选取与纹理坐标最接近的像素点颜色 - 双线性过滤(Bilinear)
- 邻近点(Nearest)
1 | vec4 Sample2D(vec2 texCoord) { |
邻近点/双线性+mipmap
预先生成一系列以2为倍数缩小的纹理序列,在采样纹理时根据图形的大小自动选择相近等级的Mipmap进行采样
通过计算出纹理坐标在纵向和横向的偏导数(并取最大值)来计算Mipmap级别
假设有一个512x512的图片,贴在屏幕空间上呈300x300大小的正方形上。可知相邻像素的UV坐标差值为1/300,乘上纹理尺寸后偏导数约为1.706,delta为2.91271,计算得到MipmapLevel为0.7712,因此最接近的级别为1,也就是256x256这一级三线性过滤(Trilinear)
Mipmap跳变 对于浮点值MipmapLevel,分别在其前后两个整数级别的Mipmap上进行双线性过滤,然后将两个结果再根据MipmapLevel到两个级别的距离进行平均- 各向异性过滤(Anisotropic filtering)
原因MipmapLevel是取纹理坐标在xy方向上较大的那个变化率计算得到的 而对于倾斜或者长条状的图形xy方向上的纹理坐标变化率差距很大 会被贴上更低一级的Mipmap,导致模糊 - 其他纹理过滤方法
pcf
cubemap采样:
cal_cubemap_uv():- 设采样向量为(x,y,z) 取x,y,z坐标中绝对值最大的为主轴 根据它的符号来判定在哪个面上采样 判定正负轴。例如(0.5, -0.8, 0.4),对应-Y轴。
- 坐标除以主轴的绝对值,映射到[-1,1]区间即换算到立方体上。(0.5, -0.8, 0.4) -> (0.625, -1, 0.5)。
- 根据对应轴的UV坐标换算关系,求得UV。(0.625, -1, 0.5) -> u = x = 0.625, v = -z = -0.5
- 从[-1, 1]缩放到[0, 1] (+1/2) (0.625, -0.5) -> (0.8125, 0.25)
- 从-Y轴的贴图上以UV坐标采样
https://blog.csdn.net/yjr3426619/article/details/81224101
https://zhuanlan.zhihu.com/p/463309766
其他可用于environmentmap的贴图:
hdr的环境光照图: 类似世界地图的经纬度展开 只需要存储一张
球形贴图(sphere mapping): 计算非常简单,缺点就是只能在一个方向起作用。虽然贴图中包括了整个环境中所有方向的光照,但是如果从其他角度观察,因为在边缘处像素很少,所以会导致严重的失真。因此球形贴图一般只会用于一些观察方向不会改变的场合 实时渲染中,一般很少会直接使用球形贴图来表示环境光照,更多地是用来做Matcap着色
shader
用于顶点数据的处理vertex_shader以及着色时的计算fragment_shader fragment_shader其实就是在求解渲染方程
定义payload_t类用于存储shader中所需要与外部交流的数据 成为shader与外界 vertex_shader与fragment_shader间数据交互的桥梁
vertex_shader:
将model中的数据传至payload的中
主要是进行mvp变换
fragment_shader:
PhongShader:
传入重心坐标的值 根据重心坐标得到插值的世界坐标 法线向量 uv坐标
透视插值矫正: (使用屏幕空间的重心坐标) https://zhuanlan.zhihu.com/p/144331875
Z为clip坐标的z分量(mvp变换后) I为任意属性
切线空间法线贴图代替模型中存储的法线向量 计算函数TBN_normal()
求法向量方法:
- 三条边叉乘得面的法向量(Flat Shading) 法向量再得光强
- obj中的vn为顶点法向量 得到顶点的光强后再插值(Gouraud Shading 能够表现的细节不够 插值不能做到三角面内部添加凹陷 开销大)
- 法线贴图 把模型表面的法向量都存储 (Phong Shading 有更多细节,在同一个三角面也能表现出凹凸感) 插值计算uv坐标再访问法线贴图得法向量 这种法线贴图存的是[0,255]的rgb表示 打开图片为彩色的
- 切线空间中法线贴图 可适用于模型只有部分变换时 各部分法向量分开计算 贴图里存储相对法向量(相对于单个三角面而不是模型空间) 图片基本为蓝色(大部分情况下垂直于平面) 蓝色RGB(127,127,255)时相对三角面(0,0,1) 贴图存储的x,y,z分量对应的方向是法线贴图的横轴,法线贴图的竖轴,垂直于平面的方向
TBN坐标系 切线空间
切线空间是针对三角面的每个顶点而言的
计算方法:
1.计算出TBN坐标系中的t, b
先由世界坐标取AB,AC uv坐标取uAB vAB uAC vAC 由定义可得
求出后还需要通过正交化(schmidt orthogonalization)
2.通过插值得到点在法线贴图中的uv坐标读取相应xyz(切线空间) 用TBN和它左乘归一化得出世界坐标中的法向量
Blinn-Phong
自定义light属性
kd由diffuse纹理读出 计算l,v,h,n向量 分别求出ambient diffuse specular 再输出(输出*255)即可
SkyboxShader:
同上插值得世界坐标 在用cubemap采样得color
PbrShader
https://learnopengl-cn.github.io/07%20PBR/01%20Theory/
独立光源情况:
自定义光源属性 同上插值得世界坐标 uv坐标 法线贴图得法线向量 用uv对model中所存贴图采样得roughness metalness ao albedo emission
求解渲染方程:
对独立的光源 渲染方程不需要对角度积分 每个射过来的光源角度都已知 直接累加即可
遍历每个光源求总Lo:
brdf中每项均采用一种近似方法求得 D使用Trowbridge-Reitz GGX,F使用Fresnel-Schlick近似,G使用Smith’s Schlick-GGX
c为表面颜色即diffusemap采样所得 alpha为roughness或其平方 ks即为F kd为(1-ks)(1-metalness)金属不会折射
得到Lo后再加上环境光(ka\albedo*ao(albedo为表面颜色 ao为ao贴图))ibl情况:
同上得世界坐标 uv坐标 法线向量 roughness metalness ao albedo emission
同上求F项(不同处为加上了roughness) 进而求出ks kd
diffuse part: 通过法线n对payload中存储的irradiance_map采样得irradiance diffuse = irradiance Kd albedo
specular part: 通过(ndotv, roughness)对payload中存储的lut采样得scale和bias 配合上述的F0求得brdf项 由payload中存的最大miplevel和roughness算出所需的mipmaplevel 在对payload中存有的对应level的prefiltermap用l采样得L specular = L*brdf (此处可用插值优化)
最终color = diffuse + specular + emission最后得到的color要经过ACES ToneMapping 以及 Gamma Correction处理
Tone Mapping和Gamma Correction:
二者都是为了更好的在LDR设备上显示图片, 将图片的颜色值从一个范围分布变换到另一个范围分布。 而不同的是,Tone Mapping是根据相应的算法将颜色值从一个大的范围映射到了较小的范围, 而Gamma Correction则是从[0,1]映射到[0,1], 映射范围并没有改变,只是改变了不同亮度值颜色的分布情况(人眼对暗色变化敏感 暗色区域应该大)
https://zhuanlan.zhihu.com/p/66558476
https://moontree.github.io/2020/08/30/tone-mapping/
https://juejin.cn/post/7050729460869365767
ibl
把对角度的积分(即对图的采样)改为预计算 (在半球内均匀间隔或随机取方向可以获得一个相当精确的辐照度近似值但仍需要大量样本)
diffuse part
漫反射的计算结果仅与表面基础反射率Albedo/c和辐射照度Irradiance有关
假设位置p位于辐照度图的中心 意味着所有着色点都被认为处在坐标原点的位置 所有漫反射间接光只能来自同一个环境贴图 反射探针可解决 每个反射探针单独预计算其周围环境的辐照度图 位置p处的辐照度取离其最近的反射探针之间的辐照度插值
由于位置不变(中心) Irradiance可被表示为以法线方向为变量的函数 预计算一个新的立方体贴图
IrradianceMap每一个像素点对应的采样向量即为表面法线,像素颜色为表面Irradiance
Irradiance求解方法:
均匀采样 黎曼和(项目中所用)
为保证均匀采样 系数 sin(θ) 用于权衡较高半球区域的较小采样区域的贡献度
- 遍历定义的irradiancemap中每个像素(x,y) set_normal_coord()求过该像素的法线 从而求出TBN
- 对以该像素为中心朝法线方向的半球 对半球Ω上以sampleDelta的增量对2pi和0.5pi的半球面两层循环进行离散采样 均匀地在积分半球Ω产生采样向量sampleVec sampleVec即为入射光线l
- 将得到的采样方向从球面坐标转切线空间3D直角坐标 再由之前的TBN求得世界坐标中的采样方向
- 用l在cubemap上进行texture查询得color(即式中L) 乘以系数后将所有平均得到(x,y)处的 irradiance 存入map中
- 遍历六个面得到六个irradiancemap
蒙特卡洛积分
关键在于N个随机采样点的生成 由于球面性质 随机均匀分布的theta和phi在球面上并不均匀
$d\omega = rd\theta * r\sin\theta d\phi$逆变换采样(Inverse Transform Sampling) 用0~1间均匀分布的随机变量来获得符合其他任何分布的随机变量
利用逆变换采样得到的N对采样点去采样平均即得
可用球谐函数替代Irradiance Map(略) https://zhuanlan.zhihu.com/p/144910975
参考: https://zhuanlan.zhihu.com/p/463309766
https://zhuanlan.zhihu.com/p/49746076
specular part
分割求和近似法(split sum approximation)将方程的镜面部分分割成两个独立的部分,单独求卷积,然后在 PBR 着色器中求和
prefiltermap
第一部分预滤波环境贴图prefiltermap 类似辐照度图 考虑粗糙度 按粗糙度级别把模糊后的结果存储在预滤波贴图的 mipmap 中
已知N与V 通过GGX重要性采样获取半程向量H 则可计算采样方向L
重要性采样:
有偏(样本并不完全随机 集中于特定方向)的蒙特卡洛估算具有更快的收敛速度
拟蒙特卡洛积分: 使用低差异序列生成蒙特卡洛随机样本向量 样本分布更均匀 具有更快的收敛速度
大多数光线最终会反射到一个基于半向量的镜面波瓣内 波瓣的大小取决于表面的粗糙度 因此将样本集中在镜面波瓣内 因此将拟蒙特卡洛采样与低差异序列(Hammersley序列)相结合,并使用GGX重要性采样使用NDF定向和偏移采样向量,可以获得很高的收敛速度
由h向量的pdf(NDF所得)逆变换采样(见上文)得h向量的采样点 和 wo向量的pdf求取方法:
h的pdf转wo的pdf: https://www.graphics.cornell.edu/~bjw/wardnotes.pdf
generate_prefilter_map:
- 首先根据roughness的不同决定mipmap的level进而决定mipmap大小
- 遍历prefiltermap的中每个像素(x,y) set_normal_coord()求过该像素的法线 从而求出TBN以及r与v(先前假设v=r=n)
- 开始一个大循环,生成一个随机序列值(相当于diffuse中两个均匀分布的随机值$\xi_1 \xi_2$),用该序列值在ImportanceSampleGGX中由上述求出的公式得到符合NDF的pdf分布的$\theta 和\phi$ 即得球面坐标中h的样本向量
- 将得到的样本向量从球面坐标转切线空间3D直角坐标 再由之前的TBN求得世界坐标中的样本向量(注意此时的样本向量为h)
- 用v和样本向量h得到样本向量l 对场景的cubemap采样 查询后的color乘系数累加后(同时也要对$n\cdot l$累加(公式中的分母)) 分母分子相除存入map中
- 对十个mipmaplevel对应roughnes(0~9)/9分别制作六个面的prefiltermap(60张)
Reference:
https://www.blurredcode.com/2021/05/dec701b2/#fn:wardnotes
https://zhuanlan.zhihu.com/p/66518450
https://learnopengl-cn.github.io/07%20PBR/03%20IBL/02%20Specular%20IBL/
各项异性GGX分布坐标推导: https://blog.csdn.net/air_liang1212/article/details/106215259
BRDF 积分贴图 lut
通过重要性采样获取H,已知V可以算出L
scale 和 bias可以打表,只与$n \cdot v$和$\alpha$有关,因此可以打表为二维纹理 scale 放在红色通道, bias 放在绿色通道
PBR&IBL Reference:
https://www.blurredcode.com/2021/05/dec701b2/#fn:wardnotes
https://zhuanlan.zhihu.com/p/66518450
https://learnopengl-cn.github.io/07%20PBR/01%20Theory/
https://zhuanlan.zhihu.com/p/61962884
https://zhuanlan.zhihu.com/p/162793239
https://juejin.cn/post/6994946774641147941
pipeline
drawLine&Triangle
drawLine:
Bresenham’s line algorithm
for循环中采用真实长度的步长比例: 效率低
用x做循环控制变量: 不能处理x1
优化: 增量算法 且对增量d*2摆脱浮点数
1 | d += std::abs(y1-y0/float(x1-x0)); |
画线框 遍历obj的每个面以及面中的顶点 画线
drawTriangle:
- 方法一 用线扫描
1 | void drawTriangle(Vec2i v0, Vec2i v1, Vec2i v2, TGAImage& image, TGAColor color) { |
求出重心坐标
在求出的包围盒内枚举每个像素 根据重心坐标(x,y,z均>0) 判断在三角形内 再着色
渲染管线
对一个三角面:
World Space => (Vertex Shader 进行MVP变换) => Clip Space => (透视除法 剪裁空间中做了剔除再除法) => NDC => (视口变换) => Screen Space => (Fragment Shader着色) => 屏幕/FrameBuffer
渲染管线:
应用阶段(Application Stage):
CPU负责 可见性判断(和后面齐次空间裁剪区别在于粒度的不同, 此时是物体层面的剔除包括视锥体剔除(Frustum Culling)和遮挡剔除( Occlusion Culling), 后面为三角面层面的剔除)、控制着色器参数和渲染状态、提交图元至GPU硬件以供渲染(提交Draw Call 可批处理)
几何阶段(Geometry Stage):
顶点着色器(Vertex Shaders)顶点坐标变换 MVP 顶点的着色计算 平面着色 (Flat Shading)和高洛德着色 (Gouraud Shading)
曲面细分着色器(Tessellation Stage)
加入更多的顶点 贴图置换(Displacement Mapping)
几何着色器(Geometry Shader)
控制GPU对顶点进行增删改操作
齐次空间裁剪
视椎体裁剪把完全不在视椎内的图元全部干掉
视口剔除把刚好在坐落于视椎体边上的图元进行裁剪,将多边形分成三角形继续
面剔除放到屏幕映射之后做
透视除法
裁剪空间至NDC (NDC时z分量已无)
屏幕映射(Screen Mapping)
背面剔除
光栅化阶段(Rasterization Stage)
图元组装(Primitive Assembly)把顶点连线(画线框) 计算三角形的重要数据(三条边的方程、深度值)供三角形遍历阶段使用,同样可用于各种着色数据的插值
三角形遍历(Triangle Traversal)遍历包围盒中所有像素判断是否在三角面内 并对这些像素的属性值进行插值
抗锯齿(Anti-aliasing)就在此时 项目中中心点被覆盖即被划入片元 无抗锯齿
像素处理阶段(Pixel Processing Stage)
深度测试提前(Early-Z))与透明度测试冲突
片元着色器(Fragment Shader)计算像素颜色 结合texture
裁剪测试(Scissor Test)
透明度测试(Alpha Test)
模板测试(Stencil Test)
深度测试(Depth Test)
混合(Blend)根据色彩 不透明度
片元颜色送到颜色缓冲区
双缓冲屏幕上显示前置缓冲(Front Buffer),而渲染好的颜色先被送入后置缓冲(Back Buffer),再替换前置缓冲,以此避免在屏幕上显示正在光栅化的图元
Vertex Shader
主要进行mvp变换 Clip Space是一个顶点乘以MVP矩阵之后所在的空间,Vertex Shader的输出就是在Clip Space上 以及将相关数据存入shader的payload中
透视变换矩阵把顶点从视锥体中变换到裁剪空间中 裁剪完成后进行透视除法
齐次空间裁剪
齐次空间裁剪是一个流程,其中主要包括视椎体裁剪,视口剔除,面剔除
homo_clipping()对七个面(包括w 裁剪掉w小于等于0的)进行plane_clipping()
判断顶点和平面的位置关系:
- 当前顶点和上一个顶点不在平面的一边 则与平面有交点 求出边与平面的交点,即插值系数t 将新的顶点存入
- 顶点在裁剪空间内部的则保留
- 顶点不在空间内部的则舍弃
最终返回该三角面经裁剪后的顶点数(一般为0, 3或4)
若为4个则分成将4个顶点分成两个三角面分别进行后续光栅化rasterize() 3个则照常
透视除法
将Clip Space顶点的4个分量都除以w分量,就从Clip Space转换到了NDC NDC是一个长宽高取值范围为[-1,1]的立方体,超过这个范围的顶点,会被GPU剪裁 透视除法只对顶点的position属性 其他属性就是线性插值
viewport
将screen_coords进行视图变换 (screen_coords的z项存取-clip_coords[i].w clip space中坐标w存的是View Space(摄像机的视角)的Z (由投影矩阵可知) 按理来说做完透视除法后w坐标无用了 相当于screen中存的是-z 天空盒存一正大值)
Back-face culling 背面剔除
点乘结果为负说明光线来自多边形后面 简单丢弃该三角形
背面剔除 (视口变换后 用ndc坐标)
对所有多边形进行顺时针编号, 即v1 v2 v3 ….. vz
计算法线向量, 即N1 N1 =(v2-v1)*(v3-v2)
考虑投影仪P, 它是任何顶点的投影计算点积Dot = N.P
测试并绘制表面是否可见。如果Dot≥0, 则表面可见, 否则不可见
bounding box
根据screen_coords的x,y设计一个三角面的包围盒 着色时仅在包围盒中进行
shading
对包围盒中所有x,y 根据screencoords求出在三角面中的重心坐标$(\alpha, \beta, \gamma)$ 根据重心坐标是否均大于等于0判断在三角面内
在则求出z值(根据screen的z坐标)且透视矫正 和zbuffer比较 较小则覆盖
将重心坐标传入fragmentshader得到(x,y)处的color 存入framebuffer即可
光栅化中值得注意的几种情况:
- transformation时的法向量变换: transformation中非等比缩放时的法线变换矩阵(求逆再转置)(其实都是求逆再转置 只不过旋转平移等变换的逆矩阵等于转置矩阵 而等比缩放对法向量乘上一个缩放系数的倒数即可(归一化)) 只需要求Model和View的逆转置即可
- screen space下插值与view space下插值的误差矫正 任何需要插值的属性经过透视投影后都要矫正 不同类型的插值如线性插值和重心坐标插值利用相似的公式 https://zhuanlan.zhihu.com/p/144331875
- 齐次空间透视裁剪 见代码 https://zhuanlan.zhihu.com/p/162190576
scene
如果函数的参数是一个指针,不要指望用该指针去申请动态内存
指针参数p的副本是 _p _p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变
非得要用指针参数去申请内存,那么应该改用“指向指针的指针”
做形参时传入二级指针的地址 如果改变该二级指针地址(**p),对该指针的操作也将无效,但是改变二级指针的内容(例如*p),则该二级指针可以正常返回
用于场景的搭建
model以及shader都是在函数中分配的内存
载入ibl相关的预计算map至shader中
win
利用win32 api 创建窗口且提供将framebuffer画至窗口功能 实现简单ui 同时提供获取鼠标位置的输入为camera使用
msg_dispatch() 提供更新传递信息功能 即传递鼠标键盘等操作 调用它才能更新window中所存属性
window->is_close 整个进程的开关 window->is_start 渲染的开关
main
创建窗口,camera以及相关buffer
进入窗口循环 不断检查scene是否选择
若scene已赋值则加载scene start进入render循环
循环中更新相机参数 计算mvp矩阵赋给shader 对model中存有的每个面调用draw()
将framebuffer中的数据画在窗口
stop后返回窗口循环
- 未解决Bug:
cerberus 模型变换缩小模型时镜头拉近会变黑
helmet 镜面ibl中有黑线