自学内容网 自学内容网

【UE5】将2D切片图渲染为体积纹理,最终实现使用RT实时绘制体积纹理【第二篇-着色器制作】

在上一篇文章中,我们已经理顺了实现流程。
接下来,我们将在UE5中,从头开始一步一步地构建一次流程。
通过这种方法,我们可以借助一个熟悉的开发环境,使那些对着色器不太熟悉的朋友们更好地理解着色器的工作原理。

这篇文章中,我们将使用这样一个纹理,点此下载

在这里插入图片描述
我喜欢先实现着色器,因此先用这个贴图作为占位
这是一个体积纹理的二维展开,最终我们会用RT纹理来代替这种静态的贴图,在runtime用RT进行实时的绘制

创建材质

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

制作基础体积着色器

一、光线步进

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


在这里插入图片描述
首先,我们先创建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。
其基本原理是,让屏幕上的每个像素沿着相机的朝向发射射线。这条射线从起点到终点的过程被称为"步进(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

在这里插入图片描述

为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 );

计算过程上一篇有介绍,这里不赘述,效果如下

在这里插入图片描述

Done

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

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

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

三、相机进入内部

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

我们可以力大砖飞!

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

创建法线向内的Box模型

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

进入建模模式

在这里插入图片描述

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

创建Box

在这里插入图片描述

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

在这里插入图片描述

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

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

四、介质光吸收

在这里插入图片描述
将输出连入不透明度,再加上一个颜色。此时能更好的看到它的密度看上去非常“扁平”。
为此要使用布格-朗伯-比尔定律,来更好的计算介质对光的吸收
它的公式在上一篇中有介绍,这里不赘述,这里我们直接使用自带的函数BeersLaw
在这里插入图片描述
它将返回有多少光被材质吸收
在这里插入图片描述

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

五、体积深度

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

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

1.禁用深度测试

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

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手搓一个使用LWC精度的替代节点吧:

  • 创建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
到此一个最基本的体积纹理渲染就做好了


待续
就上三天班又放假?
等回来就开始在下一篇进行【制作进阶体积着色器】,继续为其实现光照等效果
放假回来我们继续,下班



原文地址:https://blog.csdn.net/Utwelve/article/details/142375909

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!