【UE5】将2D切片图渲染为体积纹理,最终实现使用RT实时绘制体积纹理【第五篇-着色器投影-投射阴影部分】

投射阴影

最初打算将投影内容放在上一篇中,因为实现非常快速简单,没必要单独成篇。不过因为这里面涉及一些问题,我觉得还是单独作为一篇讲一下比较好。

原理

这里要用到的是 Shadow Pass Switch ,它可以为非不透明的材质替换阴影

某些版本UE只能搜中文"阴影通道切换"

简单的演示一下 Shadow Pass Switch 的功能

做一个这样的材质:

效果如下:

制作Shader

创建一个 Custom ,起名为 ShadowRayMarching ,输入节点如图,输出单通道

老样子,这些可以直接右键粘贴到输入:

cpp 复制代码
((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)))

代码如下:

c 复制代码
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 ;

是不是很熟悉,这就是我们梦开始的地方。还记得我们的起点吗,我们第一步就做了这样一个步进,只是当时是"从相机方向"进行步进。因为现在做的是阴影,也就需要改为从光源方向步进,因此代码中现在是:

c 复制代码
CurPos -= LightVector * StepSize;//这里与之前不同

从相机方向步进:

从光源方向步进:

连接变量:

只有 ShadowMask_MaxSteps 是新建变量,其余都是已有变量,我们直接使用

将结果连入 BeersLaw (还记得吗,这是介质吸收),连到Mask输出打印看看(材质当然也换到了 已遮罩 ,因为现在是法线向内的模型,因此也要开启双面

看看效果,这就是用来形成阴影的Mask


制作Mesh

到这一步,我们会遇到一个问题

我们目前使用的模型是法线向内的,如果不开启 双面 ,你看的实际会是这样:

背面透明的材质无法阻挡光照并投射阴影。

如果继续使用"双面"呢?

可惜投影是不区分 TwoSidedSign 的,实际上投影对很多东西都不支持,稍后会提到

  1. 创建双面材质
  2. 打光,可以看到正反面的不同并不会影响阴影

况且,由于体积渲染本身已经很耗资源,开启双面会导致性能难以接受。或者,你可能考虑使用 TwoSidedSign 来从视觉上剔除体积雾的"外部"渲染,但这对性能没有改善。

举例来说,这就像在计算 (1+2+3) * 0

虽然结果是 0 ,但 1+2+3 是会被计算的

简单的因果律

总之,这个办法是无论如何都行不通的。那么我们现在有两个选择:

两种方案

方案1

使用两个Cube模型:一个法线向内的,用来渲染体积;一个法线向外的,渲染阴影遮罩。然后将他们重叠放在一起。

你可以选择这种方法,这会让内外两个mesh有更为独立的静态网格体组件的控制。缺点是整合两个模型,需要制作一个Actor蓝图,类似这样:

方案2

和1的思路一样,制作一个 双面的模型,并为模型内外表面分别设置材质ID

为保证纯粹性,教程采用"方案2"

建立内外法线cube

在UE直接建模也是可以的,像之前一样

但我发现UE建模功能的更新频繁,用它做教程没啥制导意义,可能过几个版本就没人看得懂了,所以这次直接用3dsMax做演示

  1. 建立一个 100cm 长宽高的 box ,模型的轴在中心 。复制出一个,增加 法线 修改器翻转法线,现在我们拥有一正一反两个模型

    2.分别为它们增加材质修改器,分别指定材质ID 12

    (注意,图中两个都为1,是错的)

  2. 为他们增加平滑修改器,这是为了优化顶点数量

  3. 将它们移回中心,并选中两个模型,塌陷

  4. 创建多维子材质并赋予模型,这是为了导出时能正确导出材质ID。(图里给了一个切片,是为了让读者可以看到内外的不同材质。实际不要切哦!)

  5. 导出FBX并导入UE,注意不要开启这个模型的 Nanite ,可以看到两个材质通道(元素)

组合在一起

模型放入场景

  1. 将模型放入场景,注意要关掉 影响距离场光照
    还记得吗,接收阴影 是使用距离场实现的,关掉它避免影响自身
  2. 创建子实例
    1.为主材质 M_VolRayMarching 创建子材质 MI_VolRayMarching
    2.再为子材质 MI_VolRayMarching 创建子材质 MI_VolRayMarching_Shadow

注意父子关系为:
M_VolRayMarchingMI_VolRayMarchingMI_VolRayMarching_Shadow

  1. MI_VolRayMarching 放入法线向内的材质通道,MI_VolRayMarching_Shadow 放入法线向外的材质通道

制作材质

现在把主材质连接好,这个材质需要同时实现半透明和遮罩材质,并在子材质中切换:

主材质

直接看图:

  1. 增加 Static Switch Parameter 节点(图中1),起名为 IsShadow ,用来做子实例切换。
  2. 注意图中的"2"和"3"是不一样的,"2"是不透明度 Opacity,"3"是不透明蒙版 Opacity Mask
  3. 主材质是半透明,因此"不透明蒙版"是灰色,但是同样需要连上。稍后会在子材质切换到Mask材质。
  4. Switch节点是"Is Shadow",因此下面的实现阴影的部分要连到True,如图中1,别连反了。
  5. 再最后检查一下材质设置:

子材质

MI_VolRayMarching 目前不需要修改,直接打开 MI_VolRayMarching_Shadow

  1. 勾选IsShadow
  2. 修改材质重载,混合模式改为遮罩
  3. 检查结果:

    检查一下,可以看到,外层的Mask材质为我们投下阴影

Tip:

当你快速改变光照或者改变模型位置时,你可能会发现阴影更新不及时,这是阴影缓存造成的,将模型的阴影缓存无效化改为 始终

隐藏Mask材质

两套模型方案有所不同,

如果你选择的是另外一个方案(方案1),也就是"内外是两个独立模型"的方案:

选择法线向外的模型,为其勾选 隐藏阴影 (它的意思是"隐藏时阴影"),关闭 可视

就可以在非可视情况投射阴影。

我没实际做方案1,因此下图中,对一个圆柱模型进行了设置,模型被隐藏了,但阴影还在:

这里使用老朋友Shadow Pass Switch

也就是在视图里,Default 输入的 Mask0。阴影 Shadow 则使用我们制作的光线方向步进出来的蒙版(下图1)

考虑到Debug,为了在需要的时候还能看到这个黑色的mask,这里做了一个切换,ShowShadowMask (上图2)

现在效果如下:


自阴影,接收投影,投射阴影,一切都OOKK的

一些问题

投影的实现本身并不是一件简单的事情,因此我们这种快速实现方式也不可避免地存在一些缺点。

以下是一些试图修正这些问题的无用尝试和昂贵的方法

(也许在某次UE的版本更新后,这些方法会有变得有效。所以先记录下来)

当体积模型与物体穿插时,阴影会相接

MI_VolRayMarching_Shadow Debug一下

能看到这就是问题的原因

阴影是通过Mask实现,也就代表它无法使用 SceneDepth 等数据来修复(体积雾里我们使用了它)。

那如果使用距离场呢?

ShadowRayMarching 增加CameraPosWS输入

代码如下:

c 复制代码
// 创建变量,累加步进过程中的总密度
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实现了一种沿光照方向捕获深度的相机,这样就可以拍摄到目标并实现正确的投影,然后使用贴花进行投射。

不过,这种方法对我来说实在过于昂贵,我认为并不值得尝试。这里只是给你提供一个思路,如果你有一个无情的甲方爸爸,你可以考虑使用这种方法。


下一篇是对目前阶段简短的收拾和整理的总结篇。

再然后就开始制作动态编辑体积雾的功能的新篇章咯!

(收拾它↓)

相关推荐
htsitr6 小时前
Cesium的模型(ModelVS)顶点着色器浅析
着色器
小美元12 小时前
cesium导入3dtiles
ue5
前端熊猫15 小时前
Cesium着色器
着色器·1024程序员节
灵境引路人19 小时前
【虚幻引擎UE】UE5 音频共振特效制作
ue5·音视频·虚幻
draracle2 天前
UE5 不同的编译模式下,module的组织形式
ue5
前端熊猫2 天前
着色器的认识
着色器
Extraovo4 天前
利用 Direct3D 绘制几何体—7.编译着色器
c++·笔记·学习·3d·着色器
优雅永不过时·4 天前
Three.js 使用着色器 实现跳动的心
前端·javascript·webgl·threejs·three·着色器·1024程序员节
XR-AI-JK5 天前
UE5遇到问题-UE5可正常打包出来但是运行不了
ue5
AgilityBaby5 天前
UE5蓝图中整理节点的方法
ue5·游戏引擎·unreal engine·1024程序员节