【UE5】可以实时绘制的体积渲染 【第二章 体积渲染 - 1.采样体积】

在上一篇文章中,我们已经总结了实现思路。

接下来,我们将在UE5中,从头开始一步一步地构建整个流程。通过这种方法,我们可以利用一个熟悉的开发环境,使那些对着色器不太熟悉的朋友们更好地理解着色器的工作原理。

总体来说,整个流程分为两个部分:绘制部分和渲染部分。为了能够看到实际效果,我们需要先完成渲染部分。因此,在实现绘制功能之前,我们将使用一个预先准备好的体积纹理作为占位。

在这篇文章中,我们将使用如下纹理,点击此处下载

在这里插入图片描述
为了在制作过程中有良好的正反馈,我倾向于先实现着色器,因此使用这张贴图作为临时占位。
这是一张体积纹理的二维展开图。最终,我们会实现实时绘制功能,用绘制的纹理来替换这张静态的贴图。

创建材质

在这里插入图片描述
首先创建材质,并将混合模式调整为 半透明 ,着色模式调整为 无光照

制作基础体积着色器

一、光线步进

采样贴图并通过步进对密度进行累计


在这里插入图片描述
首先,我们先创建Custom节点,并添加输入
在这里插入图片描述

Tip:
在这里插入图片描述
可以复制下方代码,然后在UE中,通过 输入 右键鼠标,进行粘贴

((InputName="Tex"),(InputName="XYFrames"),(InputName="NumFrames"),(InputName="MaxSteps"),(InputName="StepSize"),(InputName="LocalCamVec"),(InputName="CurPos"))
输入说明
Tex将要进行采样的2D切片纹理对象
XYFramesfloat2 切片的XY数量
NumFrames切片总数
MaxSteps最大步进数
StepSize步大小
LocalCamVec本地空间相机方向
CurPos采样位置

接下来,我们将编写一个用于光线步进(Ray Marching)的着色器,在这里可以说是"Camera Ray Marching"。其基本原理是,让屏幕上的每个像素沿着相机的朝向发射一条射线。这条射线从起点到终点的过程被称为"步进(Step)"。

在步进过程中,我们会在每一步采样纹理。如果采样到的纹理中存在有效值,则将其累加,这个累加值称为每一步的密度。经过预定的最大步数后,通过查看这条射线累积的总密度值,我们就可以确定该像素的光线穿透程度。

在这里插入图片描述

在代码中粘贴如下代码,对每一步的功能进行了注释

//创建变量,从0开始累加沿相机方向步进过程中的总密度
float accumdens = 0;

//使用 MaxSteps 作为最大步数进行循环,每次循环执行以下操作
for (int i = 0; i < MaxSteps; i++)
{
	// 在当前步进位置进行纹理采样,采样的是 R 通道
    // PseudoVolumeTexture 函数用于伪体积纹理采样,函数需要的参数在括号内传递
    float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;
    // 将当前采样到的密度值累加到总密度中
    // 乘以步长是为了将采样密度与步进距离相匹配
    accumdens += cursample * StepSize;
    // 为下次循环更新射线位置,沿着相机方向步进
    CurPos += -LocalCamVec * StepSize;
}

//返回累计结果
return accumdens;

在这里插入图片描述
创建对应变量输入,并将其连接到输出,就可以查看结果了

有几个新手要注意的点:

  • Tex 输入的是“纹理对象”而不是“纹理采样”
  • 使用TransfromVector函数将CameraVector由场景转换为本地
  • 如果你是较早的UE5版本,Custom的代码要填在"这里"
    在这里插入图片描述
  • 代码中的PseudoVolumeTexture是内建的采样函数,运作原理在第一篇
  • 函数BoundingBoxBased_0-1_UVW 就像他的名字一样,输出bound体积中的每个位置,你可以将RGB连接到输出查看

在这里插入图片描述
在这里插入图片描述
他会基于Bound提供位置


二、修复伪影

当从侧方看去,会发现有伪影

正面侧面
在这里插入图片描述在这里插入图片描述

这里发生的事:
在这里插入图片描述

我们可以力大砖飞!

我们可以力大砖飞 的通过提高MaxSteps解决,但我们有更好的办法

在这里插入图片描述

为了按照上图中的方式进行采样,我们将要使用这个函数:
在这里插入图片描述
他属于Volumetrics插件,如果你开启了这个插件就可以直接使用
在这里插入图片描述

注意:

  • 如果并不想开启这个插件,接下来将制作这个函数
  • 如果你启用了这个插件,也需要将此函数复制一个,因为后续需要对其修改
为VolumeBoxIntersect函数建立输入和输出
输入①输入说明输出②输出说明
Plane alignment对齐起始位置与平面间隔以减少采样伪影Box Distance-
Steps应该使用多少步进来进行平面对齐。应与步进匹配Intersection Position-
创建Custom函数

重命名为 Ray March Cube Setup
在这里插入图片描述

要注意:

  1. 输出类型为float4
  2. 在这里将Custom节点重命名

输入粘贴为:

((InputName="PlaneAlignment",Input=(Expression="/Script/Engine.MaterialExpressionFunctionInput'/Engine/Transient.MaterialFunction_0:MaterialExpressionFunctionInput_1'")),(InputName="MaxSteps",Input=(Expression="/Script/Engine.MaterialExpressionFunctionInput'/Engine/Transient.MaterialFunction_0:MaterialExpressionFunctionInput_0'")),(InputName="scenedepth",Input=(Expression="/Script/Engine.MaterialExpressionSceneDepth'/Engine/Transient.MaterialFunction_0:MaterialExpressionSceneDepth_1'",Mask=1,MaskR=1)))

代码为:

float localscenedepth = scenedepth;

float3 camerafwd = mul(float3(0.00000000,0.00000000,1.00000000),(float3x3)ResolvedView.ViewToTranslatedWorld);

float3 depthpos = ((Parameters.CameraVector * localscenedepth) / abs( dot( camerafwd, Parameters.CameraVector ) ) );

depthpos = mul(depthpos,  (float3x3)WSDemote(GetWorldToLocal(Parameters))).xyz;

depthpos /= 256;

localscenedepth = length(depthpos);

//0-1
//localscenedepth /= (GetPrimitiveData(Parameters).LocalObjectBoundsMax.x * 2 * scale);
//localscenedepth /= abs( dot( camerafwd, Parameters.CameraVector ) );

//bring vectors into local space to support object transforms
float3 localcampos = mul(float4( DFDemote(ResolvedView.WorldCameraOrigin),1.00000000), (WSDemote(GetWorldToLocal(Parameters)))).xyz;
float3 localcamvec = -normalize( mul(Parameters.CameraVector, (float3x3)WSDemote(GetWorldToLocal(Parameters))) );

//make camera position 0-1
localcampos = (localcampos / (GetPrimitiveData(Parameters).LocalObjectBoundsMax.x * 2)) + 0.5;

float3 invraydir = 1 / localcamvec;

float3 firstintersections = (0 - localcampos) * invraydir;
float3 secondintersections = (1 - localcampos) * invraydir;
float3 closest = min(firstintersections, secondintersections);
float3 furthest = max(firstintersections, secondintersections);

float t0 = max(closest.x, max(closest.y, closest.z));
float t1 = min(furthest.x, min(furthest.y, furthest.z));

float planeoffset = 1-frac( ( t0 - length(localcampos-0.5) ) * MaxSteps );

t0 += (planeoffset / MaxSteps) * PlaneAlignment;
t1 = min(t1, localscenedepth);
t0 = max(0, t0);

float boxthickness = max(0, t1 - t0);

float3 entrypos = localcampos + (max(0,t0) * localcamvec);


return float4( entrypos, boxthickness );

如果您的项目中遇到 DFDemoteWSDemote 报错,这可能是因为您使用的不是 UE5.4 或更高版本。
它们主要用于处理浮点数的精度问题。
在这种情况下,您可以使用 LWCToFloat 进行替换。

计算过程上一篇有介绍,这里不赘述,效果如下
在这里插入图片描述

Done

现在我们已经完成了对伪影的修复。可以看到,原本四周存在伪影的部分现在呈现出一种“扭曲”效果,这是因为当前的 MaxSteps 设定为 16。

为了获得更好的效果,可以将 MaxSteps 适当提升到 32 或 64,这样仍能保持不错的性能。

如果不进行伪影修复,而是通过增加步数的方法(力大砖飞)来提升细节质量,通常需要将 MaxSteps 调至 256 甚至更高,才能达到相同的细节水平。
而循环里目前仅仅只有三个步骤,后续扩展效果几乎不可能,因此这一步伪影处理是必须的

Tips
再描述一下原理,最早是直接使用Cube的UVW时,相当于:
在这里插入图片描述

这样在有透视的情况下,Step是不均匀的
在这里插入图片描述

而 VolumeBoxIntersect修复的方法是:
在这里插入图片描述

这样从相机方向step就均匀了
在这里插入图片描述

找了个图,能更好的表达意思↓
在这里插入图片描述

创建材质函数VolumeBoxIntersect

在这里插入图片描述

三、相机进入内部

现在,相机不可以进入box内部。因为着色器只在模型表面进行渲染

我们可以力大砖飞!

我们可以力大砖飞 的通过开启材质双面解决,但我们依旧有更好的办法

创建法线向内的Box模型

使用建模模式,快速的创建一个法线向内的Box
(如果你有轴在中心的法线向内的box模型则可以跳过)

进入建模模式

在这里插入图片描述

如果你没找到建模模式,你需要开启引擎的插件:在这里插入图片描述

创建Box

在这里插入图片描述

  1. 选择建模模式
  2. 选择创建功能
  3. 选择创建Box
  4. 枢纽点选择居中
  5. 在场景中单击,放置模型
  6. 点击接受创建模型
调整法线

在这里插入图片描述

  1. 保持选中模型
  2. 选择属性修改
  3. 选择法线修改
  4. 勾选翻转法线
  5. 接受修改

现在将材质放入
在这里插入图片描述
Done
可以看到相机进入"内部"也不会超出模型承载的渲染范围

四、介质光吸收

在这里插入图片描述
在将输出连接到不透明度并添加颜色后,我们可以更直观地观察到密度效果。然而,此时的效果可能显得较为“扁平”。为了改善这一点,我们需要运用布格-朗伯-比尔定律(Beer-Lambert Law),以更精确地计算介质对光的吸收。

BeersLaw是非常便宜的粒子体积的透射率计算公式,为我们提供了一种有效的方法来模拟光在介质中传播时的衰减。

在上一篇文章中,我们已经介绍过这个公式,因此这里不再赘述,而是直接使用UE5中自带的函数BeersLaw

在这里插入图片描述
在这里插入图片描述

现在看上去好多了。
虽然还没有阴影和光照,但在这之前,我们先去处理另外的问题

五、体积深度

在这里插入图片描述
我们目前还缺乏体积的深度,体积只是被渲染到了模型的内表面 ,通过这个对比可以看到发生了什么:

shadermesh
在这里插入图片描述在这里插入图片描述

1.禁用深度测试

在这里插入图片描述
现在我们为其重新制作深度
为此进入材质,禁用深度测试,接下来深度由我们说的算

Tips
透明度可以被视为一种后期材质处理(尽管相对于“后期处理”,它稍微靠前)。它是在场景渲染完成后再进行处理的,这意味着透明度本质上是没有深度信息的,它呈现于屏幕的最表层。
在选项中提到的“深度测试”实际上是通过利用场景深度来计算透明度的遮罩。这个遮罩会遮挡被场景中的物体阻挡的部分,然后把被挡住的部分修改为透明,使得透明度模型看起来像是在场景中自然存在。
而“禁用深度测试”则是指关闭这个自动计算的深度遮挡功能。
这样一来,透明效果就不再受到场景深度的影响,所有透明材质将直接渲染在最前面。然后由我们手动来计算这个“蒙版”。

2.制作深度

计算 MaxSteps

BoxDistance是框距离,相当于是射线穿透框的厚度,将其乘MaxSteps对其缩放,使用Floor取整,且使用clamp确保它不会超过限制
在这里插入图片描述

修改 VolumeBoxIntersect

在这里插入图片描述
可以看到当前的深度对的不多
现在要修复这个问题:

在这里插入图片描述
重新进入到 VolumeBoxIntersect 函数的 Custom
在这里插入图片描述
这里有几行被注释掉的内容,大体上是用于将局部场景深度转换到 0-1 范围内

为Custom增加输入 scale

scale = length(TransformLocalVectorToWorld(Parameters,float3( 1.0 , 0.0 , 0.0 )).xyz);

其蓝图也就是:
在这里插入图片描述

Tip:
Q:为什么不直接写进cutom?
A:尽最大可能的把计算过程写为蓝图,而不要在custom内计算
看编译后的HLSL就会知道,UE的材质编译器会对蓝图的内容进行接近疯狂的优化(而custom不会,写什么就是什么)
(后续会用蓝图重制这个custom的大部分内容,但目前先这样,当下还有很多工作没完成)

然后我们注释掉红框,并解除绿框的注释:
在这里插入图片描述
修改后的函数如下:
在这里插入图片描述
修改后的 Ray March Cube Setup 代码为:

float localscenedepth = scenedepth;


float3 camerafwd = mul(float3(0.00000000,0.00000000,1.00000000),(float3x3)ResolvedView.ViewToTranslatedWorld);

/*
float3 depthpos = ((Parameters.CameraVector * localscenedepth) / abs( dot( camerafwd, Parameters.CameraVector ) ) );

depthpos = mul(depthpos,  (float3x3)WSDemote(GetWorldToLocal(Parameters))).xyz;

depthpos /= 256;

localscenedepth = length(depthpos);
*/

//0-1
localscenedepth /= (GetPrimitiveData(Parameters).LocalObjectBoundsMax.x * 2 * scale);
localscenedepth /= abs( dot( camerafwd, Parameters.CameraVector ) );

//bring vectors into local space to support object transforms
float3 localcampos = mul(float4( DFDemote(ResolvedView.WorldCameraOrigin),1.00000000), (WSDemote(GetWorldToLocal(Parameters)))).xyz;
float3 localcamvec = -normalize( mul(Parameters.CameraVector, (float3x3)WSDemote(GetWorldToLocal(Parameters))) );

//make camera position 0-1
localcampos = (localcampos / (GetPrimitiveData(Parameters).LocalObjectBoundsMax.x * 2)) + 0.5;

float3 invraydir = 1 / localcamvec;

float3 firstintersections = (0 - localcampos) * invraydir;
float3 secondintersections = (1 - localcampos) * invraydir;
float3 closest = min(firstintersections, secondintersections);
float3 furthest = max(firstintersections, secondintersections);

float t0 = max(closest.x, max(closest.y, closest.z));
float t1 = min(furthest.x, min(furthest.y, furthest.z));

float planeoffset = 1-frac( ( t0 - length(localcampos-0.5) ) * MaxSteps );

t0 += (planeoffset / MaxSteps) * PlaneAlignment;
t1 = min(t1, localscenedepth);
t0 = max(0, t0);

float boxthickness = max(0, t1 - t0);

float3 entrypos = localcampos + (max(0,t0) * localcamvec);


return float4( entrypos, boxthickness );

在这里插入图片描述
深度已经正常,一切都好起来了…?
在这里插入图片描述

六、缩放修复

当你试图缩放cube,你会发现它破碎了
在这里插入图片描述
在这里插入图片描述
眼看着美好的事物在你的操作下支离破碎,这可太令人崩溃了,究其原因在于
在这里插入图片描述
TransformVector节点做了多余的非规格化计算,那么我们使用Custom手搓一个替代它吧:

  • 创建Custom,命名为TransFromVector_WorldToLocal
  • 添加输入CameraVector
    在这里插入图片描述
  • 代码如下
return normalize(mul(CameraVector,(float3x3)LWCToFloat(GetPrimitiveData(Parameters).WorldToLocal)));

在这里插入图片描述

Done

七、阶梯纹理修复

在这里插入图片描述
没有时间庆祝,接下来赶到战场的是阶梯问题。深度和缩放一旦被修复,图中画圈位置的阶梯状图形就会非常显眼。
这很明显这是由step产生的,因为我们的步(step)是预计算的,因此步与步之间就形成了这种阶梯纹理

我们可以力大砖飞!

我们可以力大砖飞 的通过增加MaxSteps数量,用更小的细分来解决,但总是有更好的办法

实际上我们只需要在进行额外一次采样就可以了
在这里插入图片描述

  • 使用frac取小数作为一小步FinalStepSize输入
  • 在Custom中的for后再进行一次采样

修改后代码如下:

//创建变量,从0开始累加沿相机方向步进过程中的总密度
float accumdens = 0;

//使用 MaxSteps 作为最大步数进行循环,每次循环执行以下操作
for (int i = 0; i < MaxSteps; i++)
{
	// 在当前步进位置进行纹理采样,采样的是 R 通道
    // PseudoVolumeTexture 函数用于伪体积纹理采样,函数需要的参数在括号内传递
    float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;
    // 将当前采样到的密度值累加到总密度中
    // 乘以步长是为了将采样密度与步进距离相匹配
    accumdens += cursample * StepSize;
    // 为下次循环更新射线位置,沿着相机方向步进
    CurPos += -LocalCamVec * StepSize;
}

//修复阶梯 额外在循环后再进行一次小步(FinalStepSize)的采样,累计到密度
CurPos -= -LocalCamVec * StepSize;
CurPos += -LocalCamVec * StepSize * FinalStepSize;
float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;
accumdens += cursample * StepSize * FinalStepSize;

//返回累计结果
return accumdens;

在这里插入图片描述
Done


到此一个最基本的体积纹理渲染就做好了,也就是所谓"无光照“模型
后续为其实现光照效果


### Unreal Engine 5 渲染技术概述 Unreal Engine 5 (UE5) 是一款强大的游戏引擎,其渲染功能相较于前代版本有了显著提升。以下是关于 UE5 中一些重要渲染技术和可能遇到的问题的相关说明。 #### Nanite 虚拟几何体 Nanite 是 UE5 的核心特性之一,它允许开发者导入具有极高多边形数量的模型而无需手动简化或 LOD 处理。通过将任何类型的网格资产表示为更高层次的中间格式[^2],Nanite 提供了一种高效的方式来处理复杂场景中的细节表现。这种改进不仅提升了工作流程效率,还使得创建高保真度环境变得更加容易。 然而,在实际应用过程中可能会面临如下挑战: - **性能瓶颈**:尽管 Nanite 可以自动管理可见性和细节级别,但在移动设备上运行时仍需注意优化资源使用情况; - **兼容性问题**:并非所有硬件都支持完整的 Nanite 功能集,因此需要针对不同平台调整设置。 #### Lumen 全动态全局光照 Lumen 是另一个标志性进步,旨在提供实时反射和间接照明效果而不依赖于预计算数据。相比传统方法而言,这种方法极大地增强了视觉真实感并减少了迭代时间成本。不过需要注意的是: - 如果项目中存在大量复杂的光源配置,则可能导致帧率下降;此时建议适当降低采样质量或者限制影响范围来缓解压力。 另外值得注意的一点是在 Android 设备上的 Vulkan 渲染器已经修复了许多呈现方面存在的缺陷[^1] ,这意味着移动端用户能够享受到更加稳定流畅的画面体验。 #### 自定义深度缓冲区 对于希望实现特定后期处理效果的应用程序来说,“Render Custom Depth”选项非常有用。它可以分别应用于静态网格物体以及骨骼动画对象,并位于渲染分类下[^3] 。启用该功能后即可获取额外的信息层用于进一步操作比如遮挡剔除等高级技巧当中去。 ```cpp // 启用自定义深度渲染的一个简单例子 UMaterialInterface* Material = ...; // 获取材质实例 Material->SetScalarParameterValue(FName("CustomDepth"), true); ``` 上述代码片段展示了如何通过编程方式控制某个材料是否参与自定义深度绘制过程。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值