游戏中有哪些看上去很简单,但实际上需要极高技术力或是极高成本的细节?
Ray March 体积云
这是一种大家平时不太会接触到,或者比较难理解的一种TA进阶的技术:Ray March体积云技术,Ray March的意思是光线步进,它跟光线追踪是比较像的,下图的游戏画面中的云就是使用Ray March体积云实现的,可以清晰地看到云的层次感
体积云的特点
体积云是有体积的,而不是平面的
体积云渲染时的步骤
1、云的噪点图生成
- 要生成噪点图需要先在图片上随机生成一些小红点(如右图),生成完以后,图片上的每一个像素格子都要判断靠它距离最近的那个小红点是谁,并且通过它们的距离值来代表亮度,距离越近,就把它渲染的越黑
- 这种随机生成若干个随机点,靠随机点之间的距离来代表这个格子的亮度的算法叫做Worley Noise算法
- 接下来再把中间是黑的,边缘是白的图做一下亮度值的反转,就会形成左边的中间是白的,越靠近红点越白,越远离红点越黑的图片
- 在平面上生成噪点图的开销其实是挺大的,它需要一分钟甚至几分钟时间才能生成一张,那有没有办法让它的速度更快一些呢?
- 可以只生成中间这一小块噪点图,然后再把它重复九次(如右图)
- 但复制出的平面噪点图是不连续的,缝隙非常明显,可以把这张复制出的图片进行水平和垂直翻转,让平面噪点图变成连续的状态,并把渲染开销降低到原来的1/9
- 接着再在高度上生成跟平面噪点图分辨率一样的若干个切片就形成了3D噪点图
- 如何生成Worley Noise噪点图?
- 随机确定一些中心点
- 计算每个像素到中心点的距离
- 算法层面的优化方法可以参考一下文末的《Unity全栈开发大师》
- 如何在Unity中生成这样的噪点图?如何用Worley Noise算法生成噪点图呢?像素数量太多如何优化呢?
- 优化只要用九宫格算法就可以了,但用九宫格去生成其中一个格子也有不少的时间开销的,
- 所以为了解决这些问题可以开启Unity Shader的Compute Shader来进行并行运算,Compute Shader可以利用CPU和GPU来高效的调动整个噪点图的生成
- 具体怎样做才能使效率达到更高,可以参考文末的《Unity全栈开发大师》
- 上图是我们的噪点图生成器
- 第一个步骤完成以后就已经能生成云的形状了,那应该如何按照云的噪点来把云渲染出来呢?这里需要使用光线追踪技术来把云渲染到一个盒子范围里
2、屏幕后处理
- 屏幕后处理就是把整个屏幕看成是一个四边形,然后整个屏幕的内容都已经渲染在一张纹理贴图上面了(左图代码中的_MianTex),然后再传入这张图片的每一个像素点的uv坐标(左图代码中的input.uv),就能够对屏幕四边形进行采样并渲染在场景里
- 至于为什么要在这里讲解屏幕后处理呢?是因为接下来要做光线追踪
如果想要对场景里的物件实现勾边效果,可以在片元着色器中写右边用黄色圈起来的这段代码,这段代码是把像素点坐标做一个偏移,然后进行采样
3、光线追踪
- 这一阶段的目标是在执行左边的代码时能渲染出一个盒子(如右上图)这个盒子我们叫它AABB包围盒
- 算法解析:
- 在渲染每一个像素点时从摄像机的位置发出一条射线,射线的目标点就是这个像素点的位置,也就是说:
- 射线的起点是摄像机的原点,方向指向屏幕上的一个像素点,然后通过rayBoxDst(快速求交)算法,算得这个像素点是否与云盒中的云有相交
- 代码解析:
sampler2D _MainTex;
float3 BoundsMin;
float3 BoundsMax;
Texture3D<float4> ShapeNoise;
Texture3D<float4> DetailNoise;
float4 frag(v2f input) : SV_Target
{
float4 col = tex2D(_Miantex, input.uv);
float3 rayOrigin = _WorldSpaceCameraPos;
float3 rayDir = normalize(input.viewVector);
float2 rayBoxInfo = rayBoxDst(BoundsMin, BoundsMax, rayOrigin, rayDir);
float dstToBox = rayBoxInfo.x;
float dstInsideBox = rayBoxInfo.y;
bool rayHitBox = dstInsideBox > 0;
if(!rayHitBox)
{
col = 0;
}
return col;
}
- 获取到碰撞信息后,通过返回的X坐标来记录射线到盒子的距离并用变量保存,叫做dstToBox,然后再通过返回的y值记录盒子内部的长度,同样是用变量保存叫做dstInsideBox
- 下面的if语句是判断如果没有碰到,就把颜色设置为黑色,碰到了就会把显示颜色,这样就能把盒子画出来了
- 这种技术叫做SDF算法,其实这种算法不仅可以在屏幕四边形上画盒子,还可以画各种各样的形状,大家如果想要了解如何绘制各种SDF形状,可以参考我们的其他内容
- 现在可以反过来把盒子外面绘制出来,将盒子里面绘制成黑色,方法很简单,就是把上图代码中的if语句中的感叹号去掉
- 现在假设盒子是隐藏在红色盒子的后面(如右上角图),但黑色盒子并没有被红色盒子遮挡,这又是什么原因呢?
- 因为在屏幕后处理阶段写的Shader代码不能在平面上区分每一个像素点的前后关系
- 解决方法:
- 可以利用Unity URP渲染管线的深度采样功能取得每一个像素点的深度值,然后再通过图形学里的深度重建的技巧来得到每一个像素的深度值
- 关于深度重建的原理和推导会在系统课程里详细介绍
4、光线进步
- 并不是盒子范围内全部都要渲染成云,因为前面的噪点图代表云是否生成,而噪点图是一个灰度图,当它的灰度值小于一定的值时,我们就不会把它渲染成云
- 所以要写一个算法来采样当前位置里的云的浓度
float sampleDensity(float3 position)
{
float3 uvw = position * CloudScale * 0.001 + CloudOffset * 0.01; // 通过当前位置取到当前位置的uv坐标
float4 shape = ShapeNoise.SampleLevel(samplerShapeNoise, uvw, 0); // 采样
float density = max(0, sharp.r - DensityThreshold) * DensityMultiplier; // 取红色通道里的一个噪点的灰度值,减去云浓度的预值
return density;
}
- 注意点:
- 由于云在立方体盒子里是立体的,所以这个3D噪点图会有很多层,无法一次步进完,需要进行多次步进(如下图)
- 这也是为什么这个技术叫做光线步进的原因,每次步进都要进行采样、判断
圈中代码解析:
float stepSize = dstInsideBox / NumSteps; //设置每次光线步进的距离
while(dstTravelied < dstLimit)
{
float3 rayPos = rayOrigin + rayDir * (dstToBox + dstTravelled);
totalDensity += sampleDensity(rayPos) * stepSize;
dstTravelled += stepSize;
}
// 只要没有离开云盒的范围就不停的采样云的浓度,并把它加到总浓度里面
float transmittance = exp(-totalDensity);return col * transmittance;
//把总浓度与整个场景渲染出的颜色做乘法混合,这样浓度比较高的部分在渲染时就没有场景里本身的颜色,而浓度比较低的部分渲染时就有场景本身的颜色
- 上图是经过上面一系列步骤之后实现的效果,虽然还不是最终效果,但已经比较接近了
- 虽然是在屏幕后处理,但我们会使用一个立方体的盒子来承载这个效果,这样可以传入想要渲染的范围,以做到动态控制
5、大气光线散射
- 实现大气光线散射需要采用LightMarch技术
- LightMarch原理:
- 从摄像机的位置射出一条射线,这条射线会穿越云层,每当它穿越一点,就判断一下它到太阳光方向的浓度值,浓度值越大,说明它产生的散射就越多
- 云里的每一个点都要判断一下,最后把LightMarch的结果加起来,就得到光线散射的系数值了,然后用这个系数值作为最终渲染云效果的系数,这样就能得到:
- ”云越靠近光线的地方,就越透明;越远离光线的地方,就能更多的展现它本身的颜色”
6、云的形态控制
体积云最大的好处就是可以控制云的形状,所以我们通过Unity实现了编辑器扩展,可以编辑、调整云生成时的形状,包括实时渲染出的形状
以上就是体积云的渲染过程了,体积云的渲染是一个有难度的渲染技术,希望本文内容能对你有所帮助,想掌握各种各样的游戏技术,可以看一看我们的《Unity全栈开发大师》,带你学习更多游戏知识