Games202(7·作业3)
作业3
本次作业要求完成屏幕空间GI,WebGLRenderer.js中首先包括了light pass阶段 渲染灯,对场景分3个pass:
- Shadow Pass绘制场景阴影贴图
- GBuffer Pass(延迟渲染),把场景相关的贴图传入shader,最后生成diffuse、depth、normal、shadow、worldPos五个GBuffer信息
- Camera Pass,渲染最终显示内容
这是框架的默认渲染结果,可以看到立方体使用的是albedo颜色
实现直接光照
需要实现srFragment.glsl文件中的3个函数:
- EvalDiffuse、返回(双向散射分布函数)的值(其实就是漫反射部分的值),可以利用Get-GBufferDiffuse(uv)和GetGBufferNormalWorld(uv)来获取线性空间漫反射率和世界空间法线,GetScreenCoordinate(posWorld)获取屏幕坐标
- 这里使用Lambertian兰伯特光照模型
- EvalDirectionalLight函数、返回着色点位于uv处得到的光源的辐射度(是否可视部分),可以利用GetGBufferuShadow(uv)获取可见性信息,利用uLightRadiance光源的颜色亮度
- main中实现直接光照的效果
实现屏幕空间下光线的求交(SSR)
需要实现srFragment.glsl文件中的bool RayMarch(ori, dir, out hitPos)光线步进函数:
- RayMarch函数的返回值为是否相交,当相交的时候你需要将参数hitPos设置为交点
- 参数ori和dir为世界坐标系中的值,分别代表光线的起点和方向,其中方向向量为单位向量
ray march算法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool RayMarch(vec3 ori, vec3 dir, out vec3 hitPos) {
float step = 0.05;//每次步进距离
const int totalStepTimes = 150; //最多步进次数
int curStepTimes = 0;//当前时间
vec3 stepDir = normalize(dir) * step;//每次沿着光线方向步进的向量
vec3 curPos = ori;//当前位置 = 起点
for(int curStepTimes = 0; curStepTimes < totalStepTimes; curStepTimes++)
{
vec2 screenUV = GetScreenCoordinate(curPos);//获取屏幕坐标
float rayDepth = GetDepth(curPos);//获取光线的深度值
float gBufferDepth = GetGBufferDepth(screenUV);//获取深度缓冲的深度值
if(rayDepth - gBufferDepth > 0.0001){//如果找到交点
hitPos = curPos;
return true;
}
curPos += stepDir;//光线步进
}
return false;
}
展开
反射测试:可以看到cube和plan 都反射了彼此,输出hitpos的颜色值
实现间接光照
需要在srFragment.glsl文件中的main函数实现间接光照(2次弹射):
- 计算间接光照我们使用蒙特卡洛方法求解渲染方程,如这个伪代码,其中position0表示直接光照的着色点,position1表示间接光照的着色点
- 遍历每个采样点,每次获取方向,和交点位置
- 如果有交点计算间接光,L_Ind = BSDF(P0) / PDF * BSDF(p1) * light(uv),两个反射点漫反射颜色的乘积 * 光源所在uv处的颜色
- 循环结束,间接光总和 / 采样数量, 获得均值
- 使用InitRand()函数来初始化随机数,可以调用Rand1(out s)和Rand2(out s)来得到类型为float和vec2的随机数
- 对于PDF,使用SampleHemisphereUniform(inout s, out pdf)和SampleHemisphereCos(inout s, out pdf)。他们会返回一个局部坐标系的位置,参数pdf是采样的概率,其中s是随机数
- LocalBasis(n, out b1, out b2)函数,通过传入的世界坐标系的法线n,建立局部坐标系,返回两个切线向量,这样就可以利用返回值将局部坐标系的方向变换到世界坐标系中
evaldiffuse函数计算某点的颜色颜色,依赖于wi和wo方向,因为bound2次,它们需要3个向量
前面的直接光照和现在的间接光相加组成了最终的颜色
可以看暗处部分的环境光是比较明显的,地面上的阴影投射了物体的颜色,不过有噪点,需要提升采样点数量,不过性能瞬间骤降,因此使用性能优化
提高部分:Hierachical ray trace
自带的mipmap不支持设置非0等级,因此需要自己创建不同分辨率fbo实现,但是web1不支持fbo的附件大小不同, 因此只能在WebGL2框架运行
但是要把整个框架升级到WebGL2,不仅要改初始化部分,还需要改一系列的图形API用法和shader的语法
- web2初始化:修改getContext部分为web2环境,以及Extension扩展的支持颜色浮点精度
- fbo文件的附件大小和语法部分的改变:
- 每个mipmap层级都存储在一个FBO中,它的附件的宽高依赖于形参
- 构造函数加入,颜色附件个数(默认有5个,我们只需要一个),宽高以便传参
- texImage2D和颜色、深度附件都应用参数宽高,
- texImage2D的internalformat项需要改成gl.RGBA32F(webgl2语法)
- attachment的访问方式也要修改(webgl2语法)
- drawBuffer部分也要改(webgl2语法)
- MeshRender中drawBuffer部分也要改(webgl2语法)
- DirectionalLight中的fbo中传入数量1,这样一个fbo就只会有一个color attachment了
- 在mesh部分,由于fbo的结果通常以纹理来呈现,添加一个quad的顶点数据部分,以便之后将纹理呈现在一个屏幕大小的quad上
- SceneDepthMaterial作为深度图Mipmap的材质,并把这个文件加入到html中
- WebGLRenderer添加一个存储fbo的数组
- 在engine(创建fbo)
- 生成mipmap,根据log计算屏幕可以生成多少个mipmap,每层的分辨率都是上一层的一半,通过new fbo创建帧缓冲,并把它添加到刚才创建的fbo数组中
- 创建深度图Mipmap的材质和对应的quad mesh
- WebGLRenderer(开始渲染)
- mipmap pass接受GBuffer Pass的深度图作为输入,结果输出给Camera pass,因此它应该放在两者之间
- 利用深度材质渲染深度图到mipmap中,对于调试可以渲染到quad上
- 会渲染fbo数量次,每次渲染时都会绑定深度材质,清理颜色缓冲,以及绑定本次的fbo,更新本次shader中传入的uniform变量, 还要调整gl.viewport视口大小和fbo大小一致,
- 调用渲染命令渲染mipmap
- 原有shader部分语法
- 如果升级到GLSL ES 3.0会有更多的API使用和一些新特性,但是一些语法需要改变一下
- 在所有shader开头声明版本
- 在所有的vertex shader中,attribute部分要改为layout顶点布局的语法
- vs到fs部分的varying改成out
- fs中由vs输入的varying改成out
- fs中对color attachment输出数据时,不再用gl_FragData[],需要先layout声明,在用声明的变量来赋值
- fs输出结果数据时,不再用gl_FragColor,需要先声明变量,在给变量赋值
- 深度 shader:
- vs直接输出quad的顶点位置就行
- fs中会依赖于上一层的深度 mipmap,大小……,
- 首先如果当前mip级别为0,则直接输出底层mipmap颜色
- 否则找到上一级2*2的区域最小值minDepth
- 如果宽高为奇数情况,那方为奇数,那方就以3格的上级区域寻找,比如都是奇数,则查询区域为3*3,如果宽为奇数,则查询区域为 3 * 2
- 将minDepth作为最终的结果输出
- SSRMaterial
- 完成了mipmap pass部分后就可以传给Camera pass
- 在最后的渲染材质中,加入uDepthTexture数组,以便作为shader和数据的接口
- WebGLRenderer
- 在Camera pass阶段,在渲染每个网格,都要像材质传入depthFBOs数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
bool RayMarch_Hiz(vec3 ori, vec3 dir, out vec3 hitPos) { float step = 0.05;//步进距离 float maxDistance = 7.5;//最大可步进距离 int startLevel = 2;//开始mip层级 int stopLevel = 0;//结束mip层级 vec3 curPos = ori;//当前位置初始为起点位置 int level = startLevel;//层级初始为开始层级 while(level >= stopLevel && distance(ori, curPos) < maxDistance){//当层级 >= 结束层级,步进距离 < 最大可步进距离 float rayDepth = GetDepth(curPos);//获取光线当前深度 vec2 screenUV = GetScreenCoordinate(curPos);//获取屏幕对应的uv float gBufferDepth = getMinimumDepthPlane(screenUV, level);//获取在当前层级uv存储的深度 if(rayDepth - gBufferDepth > 0.0001){//如果相交 if(level == 0){//如果为0级,成功找到交点,返回交点位置 hitPos = curPos; return true; } else{//否则进入下一层级 level = level - 1; } } else{//没有相交 level = min(MAX_MIPMAP_LEVEL, level + 1);//进入下一层级 vec3 stepDistance = (dir * step * float(level + 1));//光线沿着方向步进step * 层级(越高层级步进距离越大) curPos += stepDistance;//光线步进 } } return false; }
展开
- 在Camera pass阶段,在渲染每个网格,都要像材质传入depthFBOs数组
- 以上对于之前的ray march 部分实现HRT,动态调整每次的步进距离
场景
engine.js中使用loadGLTF()函数来加载不同场景,也要同时切换不同场景的光源和相机参数
本文由作者按照 CC BY 4.0 进行授权