投射阴影
最初打算将投影内容放在上一篇中,因为实现非常快速简单,没必要单独成篇。不过因为这里面涉及一些问题,我觉得还是单独作为一篇讲一下比较好。
原理
这里要用到的是 Shadow Pass Switch
,它可以为非不透明的材质替换阴影
某些版本UE只能搜中文"阴影通道切换"
简单的演示一下
Shadow Pass Switch
的功能
做一个这样的材质:
效果如下:
制作Shader
创建一个 Custom
,起名为 ShadowRayMarching
,输入节点如图,输出单通道
老样子,这些可以直接右键粘贴到输入:
((InputName="Tex"),(InputName="XYFrames"),(InputName="NumFrames"),(InputName="MaxSteps"),(InputName="StepSize"),(InputName="LightVector"),(InputName="CurPos"),(InputName="LocalObjectBoundsMax",Input=(OutputIndex=3,Mask=1,MaskR=1,MaskG=1,MaskB=1)))
代码如下:
float accumdens = 0;
for (int i = 0; i < MaxSteps; i++)
{
float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;
accumdens += cursample * StepSize ;
CurPos -= LightVector * StepSize ; // 步进方向换成了 LightVector
}
//返回累计结果
return accumdens ;
是不是很熟悉,这就是我们梦开始的地方。还记得我们的起点吗,我们第一步就做了这样一个步进,只是当时是"从相机方向"进行步进。因为现在做的是阴影,也就需要改为从光源方向步进,因此代码中现在是:
CurPos -= LightVector * StepSize;//这里与之前不同
从相机方向步进:
从光源方向步进:
连接变量:
只有 ShadowMask_MaxSteps
是新建变量,其余都是已有变量,我们直接使用
将结果连入 BeersLaw
(还记得吗,这是介质吸收),连到Mask输出打印看看(材质当然也换到了 已遮罩
,因为现在是法线向内的模型,因此也要开启双面
)
看看效果,这就是用来形成阴影的Mask
制作Mesh
到这一步,我们会遇到一个问题
我们目前使用的模型是法线向内的,如果不开启 双面
,你看的实际会是这样:
背面透明的材质无法阻挡光照并投射阴影。
如果继续使用"双面"呢?
可惜投影是不区分 TwoSidedSign
的,实际上投影对很多东西都不支持,稍后会提到
- 创建双面材质
- 打光,可以看到正反面的不同并不会影响阴影
况且,由于体积渲染本身已经很耗资源,开启双面会导致性能难以接受。或者,你可能考虑使用 TwoSidedSign 来从视觉上剔除体积雾的“外部”渲染,但这对性能没有改善。
举例来说,这就像在计算
(1+2+3) * 0
虽然结果是0
,但1+2+3
是会被计算的
简单的因果律
总之,这个办法是无论如何都行不通的。那么我们现在有两个选择:
两种方案
方案1
使用两个Cube模型:一个法线向内的,用来渲染体积;一个法线向外的,渲染阴影遮罩。然后将他们重叠放在一起。
你可以选择这种方法,这会让内外两个mesh有更为独立的静态网格体组件的控制。缺点是整合两个模型,需要制作一个Actor蓝图,类似这样:
方案2
和1的思路一样,制作一个
双面的模型
,并为模型内外表面分别设置材质ID
为保证纯粹性,教程采用"方案2"
建立内外法线cube
在UE直接建模也是可以的,像之前一样
但我发现UE建模功能的更新频繁,用它做教程没啥制导意义,可能过几个版本就没人看得懂了,所以这次直接用3dsMax做演示
-
建立一个
100cm
长宽高的box
,模型的轴在中心
。复制出一个,增加法线
修改器翻转法线,现在我们拥有一正一反两个模型
2.分别为它们增加材质修改器,分别指定材质ID1
和2
(注意,图中两个都为1,是错的)
-
为他们增加平滑修改器,这是为了优化顶点数量
-
将它们移回中心,并选中两个模型,塌陷
-
创建多维子材质并赋予模型,这是为了导出时能正确导出材质ID。(图里给了一个切片,是为了让读者可以看到内外的不同材质。实际不要切哦!)
-
导出FBX并导入UE,注意不要开启这个模型的
Nanite
,可以看到两个材质通道(元素)
组合在一起
模型放入场景
- 将模型放入场景,注意要关掉
影响距离场光照
还记得吗,接收阴影
是使用距离场实现的,关掉它避免影响自身
- 创建子实例
1.为主材质M_VolRayMarching
创建子材质MI_VolRayMarching
。
2.再为子材质MI_VolRayMarching
创建子材质MI_VolRayMarching_Shadow
。
注意父子关系为:
M_VolRayMarching
→MI_VolRayMarching
→MI_VolRayMarching_Shadow
- 将
MI_VolRayMarching
放入法线向内的材质通道,MI_VolRayMarching_Shadow
放入法线向外的材质通道
制作材质
现在把主材质连接好,这个材质需要同时实现半透明和遮罩材质,并在子材质中切换:
主材质
直接看图:
- 增加
Static Switch Parameter
节点(图中1),起名为IsShadow
,用来做子实例切换。 - 注意图中的"2"和"3"是不一样的,"2"是
不透明度 Opacity
,"3"是不透明蒙版 Opacity Mask
。 - 主材质是半透明,因此"不透明蒙版"是灰色,但是同样需要连上。稍后会在子材质切换到Mask材质。
- Switch节点是"Is Shadow",因此下面的实现阴影的部分要连到
True
,如图中1,别连反了。 - 再最后检查一下材质设置:
子材质
MI_VolRayMarching
目前不需要修改,直接打开 MI_VolRayMarching_Shadow
- 勾选
IsShadow
- 修改材质重载,混合模式改为
遮罩
- 检查结果:
检查一下,可以看到,外层的Mask材质为我们投下阴影
Tip:
当你快速改变光照或者改变模型位置时,你可能会发现阴影更新不及时,这是阴影缓存造成的,将模型的阴影缓存无效化改为始终
隐藏Mask材质
两套模型方案有所不同,
如果你选择的是另外一个方案(方案1),也就是"内外是两个独立模型"的方案:
选择法线向外的模型,为其勾选隐藏阴影
(它的意思是"隐藏时阴影"),关闭可视
就可以在非可视情况投射阴影。
我没实际做方案1,因此下图中,对一个圆柱模型进行了设置,模型被隐藏了,但阴影还在:
这里使用老朋友Shadow Pass Switch
也就是在视图里,Default
输入的 Mask
值 0
。阴影 Shadow
则使用我们制作的光线方向步进出来的蒙版(下图1)
考虑到Debug,为了在需要的时候还能看到这个黑色的mask,这里做了一个切换,ShowShadowMask
(上图2)
现在效果如下:
自阴影,接收投影,投射阴影,一切都OOKK的
一些问题
投影的实现本身并不是一件简单的事情,因此我们这种快速实现方式也不可避免地存在一些缺点。
以下是一些试图修正这些问题的无用尝试和昂贵的方法
(也许在某次UE的版本更新后,这些方法会有变得有效。所以先记录下来)
当体积模型与物体穿插时,阴影会相接
为 MI_VolRayMarching_Shadow
Debug一下
能看到这就是问题的原因
阴影是通过Mask实现,也就代表它无法使用 SceneDepth
等数据来修复(体积雾里我们使用了它)。
那如果使用距离场呢?
为 ShadowRayMarching
增加CameraPosWS
输入
代码如下:
// 创建变量,累加步进过程中的总密度
float accumdens = 0;
// 使用 MaxSteps 作为最大步数进行循环,每次循环执行以下操作
for (int i = 0; i < MaxSteps; i++)
{
// PseudoVolumeTexture 函数用于伪体积纹理采样,函数需要的参数在括号内传递
float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;
// 使用距离场排除体积
float3 RayPointPos = 2 * (CurPos - 0.5) * LocalObjectBoundsMax - 0.5 * 2;
RayPointPos = LWCToFloat(TransformLocalPositionToWorld(Parameters,RayPointPos)) - CameraPosWS;
// 在调用 GetDistanceToNearestSurfaceGlobal 时减去 CameraPosWS
float SDFDistance = GetDistanceToNearestSurfaceGlobal(RayPointPos);
if (SDFDistance < 0) break;
accumdens += cursample * StepSize;
// 为下次循环更新射线位置,沿着相机方向步进
CurPos -= LightVector * StepSize;
}
// 返回累计结果
return accumdens;
这里添加了获取距离场的代码。在沿光线步进的过程中,一旦距离场小于0,就可以判断射线已到达表面,此时直接结束并退出射线,返回累积的体积密度结果。
效果如下:
可以看到蒙版确实表达了正确的深度。但意外的,阴影实际上未受影响。
Why?我们做一个快速的实验,制作一个这样简单的材质
能看到使用了距离场作为Mask的面片,未投下任何阴影,那么试着直接看看值
这就是根本原因,投影和距离场是冲突的
如果不使用距离场,还有什么方法可以获取场景深度呢?那就只能采用代价高昂的2D捕获来实现真实的阴影。
这是我的测试场景,我通过Actor实现了一种沿光照方向捕获深度的相机,这样就可以拍摄到目标并实现正确的投影,然后使用贴花进行投射。
不过,这种方法对我来说实在过于昂贵,我认为并不值得尝试。这里只是给你提供一个思路,如果你有一个无情的甲方爸爸,你可以考虑使用这种方法。
下一篇是对目前阶段简短的收拾和整理的总结篇。
再然后就开始制作动态编辑体积雾的功能的新篇章咯!
(收拾它↓)