当前位置: 首页 > news >正文

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

在前两篇文章中,我们分别拆解描述了实现原理,并进行了基础的着色器制作。在这一篇文章中,我们将为它实现光照效果
在这里插入图片描述


简单的概述

当光线射入体积时,随着光线射入距离的增加,体积中的介质会对光线产生反射和吸收作用,使其逐步损失能量。当路径上的能量降为零时,光线将无法再进入相机。如果光线仍有剩余能量,则这部分能量会进入相机。
这就是透射率的概念。
在这里插入图片描述

同时,为了降低复杂性,我们在这里不考虑光在介质内反弹并最终恰巧弹回相机的可能性。考虑这些情况会使问题变得过于复杂,会变得不幸,我们有比尔-朗伯定律。
在这里插入图片描述
核心原理就是在上一章中,用步长(step)为射线累计体积密度的同时,额外进行一次计算,计算光线在这一步时所剩余的能量。作为优化,我们只在边界内(紫色框)进行计算,并且当透射率低于某个阈值时,我们就不再继续计算(红色圆圈),如下图所示。
在这里插入图片描述

而且你会注意到这个光源是平行光,也就是太阳。如果你想要使用点光源,在计算光线路径时,你需要让黄色光线指向点,并且别忘记考虑点光源的自然衰减等特性。本章节我们将使用最简单的平行光。

完善Shader

在开始之前

在本节,着色器会变得逐渐复杂。因此在继续之前,我们有必要先对当前的工作做一些整理。

1.制作路由

在意大利面的复杂性继续增长之前,为了不必要的混乱,需要把一些面条制作成 路由 ,顾名思义,就是个“无线”的面条
在这里插入图片描述
我们先为StepSizeLocalCamVec制作路由
在这里插入图片描述
使用起来像这样
在这里插入图片描述
清爽多了

2.为Custom命名

后面会出现多个Custom,为避免混淆,需要给它们起名啦(之前忘了:| )
起名为RayMarching
在这里插入图片描述

修改 RayMarching 实现光影

1.光线步进计算光影

我们回到 RayMarching 。
之前我们的密度用了一个通道,而光照的颜色需要三个通道,总计四个。因此将输出类型改为四通道。
在这里插入图片描述

接下来我们为其增加5个输入,分别是

输入说明
LightVector平行光射入方向
ShadowSteps阴影的步数
ShadowStepSize步大小
ShadowDensity对阴影密度的额外控制
ShadowThreshold阈值,优化掉小于阈值的计算
Density需要将计算介质吸收的BeersLaw函数(布格-朗伯-比尔)移入内部

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

((InputName="Tex"),(InputName="XYFrames"),(InputName="NumFrames"),(InputName="MaxSteps"),(InputName="StepSize"),(InputName="LocalCamVec"),(InputName="CurPos"),(InputName="FinalStepSize"),(InputName="LightVector",Input=(Mask=1,MaskR=1,MaskG=1,MaskB=1)),(InputName="ShadowSteps"),(InputName="ShadowStepSize"),(InputName="ShadowDensity"),(InputName="ShadowThreshold"),(InputName="Density"))

可以看到它和相机方向的光线步进很像(其实它才是真正的光线步进不是吗)
现在样子如下:
在这里插入图片描述

现在修改RayMarching的Code,在for的内外加入了shadow部分,且使用新的结果作为返回:

在这里插入图片描述
上图中的代码如下:

// Code...代码呢?急急国王先别急。
// 下一步还有一个小修改,然后给这阶段完整的代码。
// 不然就成了纯凑字数

我们做出了很多修改,具体修改内容都标注在了注释上。
总的来说,我们把阴影的计算合并了进去,在每次采样密度时进行了一次光线的采样
在这里插入图片描述

注意:

  1. LocalCamVecStepSize 是刚才说的“路由”,别漏看现在有个乘法。
  2. RayMarching 的输出不要忘记改为4通道。
  3. 介质吸收的函数已经在 Custom 内实现(因为要写进 for)。

在这里插入图片描述
Done
现在阴影已经可以正确渲染了。目前,光照方向是手动输入的,稍后我们会使用场景中的阳光方向。但在此之前,我们先实现光源颜色。

2.光源颜色

为RayMarching增加一个输入

输入说明
LightColor光源颜色

计算阴影的同时已经计算了光能,因此我们可以在末尾直接乘以颜色:

lightenergy += exp(-shadowdist * ShadowDensity) * cursample * transmittance * LightColor;

修改后的代码:

//创建变量,从0开始累加沿相机方向步进过程中的总密度
float accumdens = 0;//Shadow部分
//创建变量,透射率和光线的能量
float transmittance =1;
float3 lightenergy = 0;
//基本和相机方向步进一样,但这些都是常量,不需要写进for里
Density *= StepSize;
LightVector *= ShadowStepSize;
ShadowDensity *= ShadowStepSize;
//一个对数来计算阈值,用来判断光线是否还值得计算
float shadowthresh = -log(ShadowThreshold)/ShadowDensity;//使用 MaxSteps 作为最大步数进行循环,每次循环执行以下操作
for (int i = 0; i < MaxSteps; i++)
{// 在当前步进位置进行纹理采样,采样的是 R 通道// PseudoVolumeTexture 函数用于伪体积纹理采样,函数需要的参数在括号内传递float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;//Shadow部分if(cursample > 0.001)//如果采样位置没有密度,则跳过{float3 Lpos = CurPos;//Lpos将作为光线步进的起始位置float shadowdist = 0;//和之前的accumdens一样,积累阴影for(int s = 0; s < ShadowSteps; s++){Lpos += LightVector;//移动步进位置float Lsample = PseudoVolumeTexture(Tex, TexSampler, saturate(Lpos), XYFrames, NumFrames).r;//采样//判断是否在框内,不是则直接break退出forfloat3 shadowboxtest = floor( 0.5+ (abs(0.5-Lpos)));//float exitshadowbox = shadowboxtest.x + shadowboxtest.y + shadowboxtest.z;float exitshadowbox = dot(shadowboxtest,1);//简短的通道相加if(shadowdist > shadowthresh || exitshadowbox >= 1) break;shadowdist += Lsample;//累计}//更新样本和光能,算法是BeersLaw函数cursample = 1 -exp(-cursample * Density);lightenergy += exp(-shadowdist * ShadowDensity) * cursample * transmittance * LightColor;transmittance *= 1-cursample;     }// 将当前采样到的密度值累加到总密度中// 乘以步长是为了将采样密度与步进距离相匹配//accumdens += cursample * StepSize;// 为下次循环更新射线位置,沿着相机方向步进//CurPos += -LocalCamVec * StepSize;//将StepSize放custom外面了CurPos += -LocalCamVec;
}//修复阶梯,在循环后再进行一次额外采样
/* 目前先注释掉这些,这样我们不必每次修改后都改一次这里。等全部完成后,再重新编写这些内容。
CurPos -= -LocalCamVec * StepSize;
CurPos += -LocalCamVec * StepSize * FinalStepSize;
float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;
accumdens += cursample * StepSize * FinalStepSize;
*///返回累计结果
//return accumdens;
//现在返回
return float4(lightenergy, transmittance);

在这里插入图片描述

在这里插入图片描述
现在我们实现了"光"和"影"
Done

3.将光照方向和颜色与场景匹配

之前的光照方向和颜色都是手动输入的,如果你有特殊的效果实现,这样做刚好。但如果你希望它能够与场景完美融合,能够使用场景信息进行自动化当然是理想选择。

1.有天空大气时

在这里插入图片描述
如果你的场景中包含“SkyAtmosphere”,那么可以使用以下方法:

可以通过 SkyAtmosphereLightDirectionSkyAtmosphereLightIlluminance 分别获取“SkyAtmosphere”的光照方向和颜色。
请注意,SkyAtmosphereLightDirection 需要转换为本地空间。

在这里插入图片描述

2.未使用天空大气时

如果你未使用“SkyAtmosphere”,或者想摆脱对其的依赖,但仍需要Shader与场景融合,则可以使用自定义Custom来直接获取平行光的参数。

新建一个材质函数,命名为 DirectionalLight,并定义两个输出,分别是方向和颜色。为它们分别创建Custom节点,代码如下:

方向:

ResolvedView.DirectionalLightDirection

颜色:

ResolvedView.DirectionalLightColor

函数如下
在这里插入图片描述
在这里插入图片描述

注意:
1.代码可能会随版本变动,为确保未来的兼容性,最好是使用UE自带的
2.代码没有额外参数(如这里使用的)时,光源索引是0
3.同样需要转为本地空间

现在Shader可以自动匹配环境光照了
在这里插入图片描述

3.阴影颜色

现在我们将制作阴影颜色。在此之前,ShadowDensity 是由一个浮点数驱动的,它代表了介质对光的吸收。

现在我们将 ShadowDensity 从浮点值改为三通道的 RGB 颜色,这意味着我们可以针对吸收的波长进行更精细的控制。
在这里插入图片描述
介质对波长的吸收是什么关系?如何通过控制ShadowDensity 调整颜色?

介质对波长的吸收与波长的物理性质有关。一般来说,短波长(蓝色)光会比长波长(红色)光更容易被吸收。这种吸收可以通过调整 ShadowDensity 来控制。

ShadowDensity 现在是一个三通道的 RGB 颜色值,用来表示介质对不同波长的光的吸收程度。每个通道的数值越大,表示对该波长的光吸收越多。

例如,ShadowDensity 值为 8, 16, 32,这意味着:

  • 对红色光的吸收是 8
  • 对绿色光的吸收是 16
  • 对蓝色光的吸收是 32

可以想象,当光线穿过介质时,蓝色光会被大量吸收,绿色光中等程度吸收,而红色光吸收最少。因此,更多的红色光最终会穿透介质进入相机,从而呈现出红色。

通过调整 ShadowDensity 的 RGB 值,你可以控制介质对不同波长光的吸收程度,从而改变最终的颜色表现。

在这里插入图片描述
Done

4.环境光照颜色

到目前为止,我们只处理了单个光源的散射效果。这种方法通常效果不佳,因为如果光源完全被遮挡,或者根本没有主光源,体积阴影区域就会显得很平淡。为了改善这一点,我们需要引入环境光。

但是,环境光照并不是简单地加一个代表环境光线的颜色就能搞定的。实际上,我们需要从垂直方向对介质采样三个额外的偏移样本。这样做可以帮助我们估计出环境光遮蔽的效果,从而让阴影区域显得更加柔和自然。

为RayMarching增加输入

输入说明
AmbientDensity环境光阴影密度
SkyColor光源颜色

在RayMarching里增加三次采样:

在这里插入图片描述

更新后全部代码如下:

//创建变量,从0开始累加沿相机方向步进过程中的总密度
float accumdens = 0;//Shadow部分
//创建变量,透射率和光线的能量
float transmittance =1;
float3 lightenergy = 0;
//基本和相机方向步进一样,但这些都是常量,不需要写进for里
Density *= StepSize;
LightVector *= ShadowStepSize;
ShadowDensity *= ShadowStepSize;
//一个对数来计算阈值,用来判断光线是否还值得计算
float shadowthresh = -log(ShadowThreshold)/ShadowDensity;//使用 MaxSteps 作为最大步数进行循环,每次循环执行以下操作
for (int i = 0; i < MaxSteps; i++)
{// 在当前步进位置进行纹理采样,采样的是 R 通道// PseudoVolumeTexture 函数用于伪体积纹理采样,函数需要的参数在括号内传递float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;//Shadow部分if(cursample > 0.001)//如果采样位置没有密度,则跳过{float3 Lpos = CurPos;//Lpos将作为光线步进的起始位置float shadowdist = 0;//和之前的accumdens一样,积累阴影for(int s = 0; s < ShadowSteps; s++){Lpos += LightVector;//移动步进位置float Lsample = PseudoVolumeTexture(Tex, TexSampler, saturate(Lpos), XYFrames, NumFrames).r;//采样//判断是否在框内,不是则直接break退出forfloat3 shadowboxtest = floor( 0.5+ (abs(0.5-Lpos)));//float exitshadowbox = shadowboxtest.x + shadowboxtest.y + shadowboxtest.z;float exitshadowbox = dot(shadowboxtest,1);//简短的通道相加if(shadowdist > shadowthresh || exitshadowbox >= 1) break;shadowdist += Lsample;//累计}//更新样本和光能,算法是BeersLaw函数cursample = 1 -exp(-cursample * Density);lightenergy += exp(-shadowdist * ShadowDensity) * cursample * transmittance * LightColor;transmittance *= 1-cursample;//环境光照部分shadowdist = 0;//重置一下阴影距离,继续利用它计算光照Lpos = CurPos + float3(0,0,0.025);//新位置float Lsample = PseudoVolumeTexture(Tex, TexSampler, saturate(Lpos), XYFrames, NumFrames).r;//采样shadowdist += Lsample;Lpos = CurPos + float3(0,0,0.05);Lsample = PseudoVolumeTexture(Tex, TexSampler, saturate(Lpos), XYFrames, NumFrames).r;//采样shadowdist += Lsample;Lpos = CurPos + float3(0,0,0.15);Lsample = PseudoVolumeTexture(Tex, TexSampler, saturate(Lpos), XYFrames, NumFrames).r;//采样shadowdist += Lsample;lightenergy += exp(-shadowdist * AmbientDensity) *cursample * SkyColor * transmittance;//累计到光}// 将当前采样到的密度值累加到总密度中// 乘以步长是为了将采样密度与步进距离相匹配//accumdens += cursample * StepSize;// 为下次循环更新射线位置,沿着相机方向步进//CurPos += -LocalCamVec * StepSize;//将StepSize放custom外面了CurPos += -LocalCamVec;
}//修复阶梯,在循环后再进行一次额外采样
/* 目前先注释掉这些,这样我们不必每次修改后都改一次这里。等全部完成后,再重新编写这些内容。
CurPos -= -LocalCamVec * StepSize;
CurPos += -LocalCamVec * StepSize * FinalStepSize;
float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;
accumdens += cursample * StepSize * FinalStepSize;
*///返回累计结果
//return accumdens;
//现在返回
return float4(lightenergy, transmittance);

在这里插入图片描述
在这里插入图片描述
现在我们的体积有柔和的环境光啦
Done

将环境光的颜色匹配

同样的,如果你有特殊的效果实现,就继续使用SkyColor作为输入。如果你希望它能够与场景完美融合,就做如下步骤:

1.有天空大气时

使用SkyAtmosphereDistantLightScatteredLuminance取得环境光
在这里插入图片描述

2.没有天空大气时
1.SkyLightEnvMapSample

可以使用SkyLightEnvMapSample,沿垂直方向(0,0,-1)采样

在这里插入图片描述

2.ResolvedView.SkyLightColor

创建Custom,并使用

ResolvedView.SkyLightColor

获取天光光源颜色
在这里插入图片描述
要注意它获取的是“光源颜色”
在这里插入图片描述

关于匹配场景颜色的Tip:
预览窗没有“SkyAtmosphere”
因此不依赖“SkyAtmosphere”的方案,可以在材质的预览窗口中预览
在这里插入图片描述
当场景有“SkyAtmosphere”,则最好使用依赖“SkyAtmosphere”的方案,能更贴合场景实际的效果

本章总结

代码

float accumdens = 0;//Shadow部分
float transmittance =1;
float3 lightenergy = 0;Density *= StepSize;
LightVector *= ShadowStepSize;
ShadowDensity *= ShadowStepSize;float shadowthresh = -log(ShadowThreshold)/ShadowDensity;//光线步进
for (int i = 0; i < MaxSteps; i++)
{float cursample = PseudoVolumeTexture(Tex, TexSampler, saturate(CurPos), XYFrames, NumFrames).r;//Shadow部分if(cursample > 0.001){float3 Lpos = CurPos;float shadowdist = 0;for(int s = 0; s < ShadowSteps; s++){Lpos += LightVector;float Lsample = PseudoVolumeTexture(Tex, TexSampler, saturate(Lpos), XYFrames, NumFrames).r;//采样float3 shadowboxtest = floor( 0.5+ (abs(0.5-Lpos)));float exitshadowbox = dot(shadowboxtest,1);//三通道求和if(shadowdist > shadowthresh || exitshadowbox >= 1) break;shadowdist += Lsample;//累计}//更新样本和光能,BeersLawcursample = 1 -exp(-cursample * Density);lightenergy += exp(-shadowdist * ShadowDensity) * cursample * transmittance * LightColor;transmittance *= 1-cursample;//环境光照shadowdist = 0;Lpos = CurPos + float3(0,0,0.025);//新位置float Lsample = PseudoVolumeTexture(Tex, TexSampler, saturate(Lpos), XYFrames, NumFrames).r;shadowdist += Lsample;Lpos = CurPos + float3(0,0,0.05);Lsample = PseudoVolumeTexture(Tex, TexSampler, saturate(Lpos), XYFrames, NumFrames).r;shadowdist += Lsample;Lpos = CurPos + float3(0,0,0.15);Lsample = PseudoVolumeTexture(Tex, TexSampler, saturate(Lpos), XYFrames, NumFrames).r;shadowdist += Lsample;lightenergy += exp(-shadowdist * AmbientDensity) *cursample * SkyColor * transmittance;}CurPos += -LocalCamVec;
}
return float4(lightenergy, transmittance);

蓝图

在这里插入图片描述

结果

Done
这章我们完成了Shader的光照部分,先看看成果:

在这里插入图片描述

画饼

下章我们继续制作“阴影投射”的部分


http://www.mrgr.cn/news/35054.html

相关文章:

  • 共享wifi公司哪家正规合法?具体流程全公开!
  • 什么是绩效改进计划?
  • drools规则引擎
  • Linux入门学习:深刻理解计算机硬件与OS体系
  • golang strings api接口
  • Python项目的质量保证
  • 内容生态短缺,Rokid AR眼镜面临市场淘汰赛
  • 责任链模式优化 文章发布的接口(长度验证,敏感词验证,图片验证等环节) 代码,示例
  • 数据结构---线性表之顺序表
  • 9.24-k8s服务发布
  • 基于C语言开发(控制台)通讯录管理程序
  • Java之路--搞定数据类型及运算符已如探囊取物
  • golang学习笔记11-模块化与包管理【重要】
  • 32岁前端干了8年,是继续做前端开发,还是转其它工作_ui设计师转开发
  • pwd 命令:显示当前工作目录
  • 嵌入式工程师需要拥有哪些能力?
  • 第二讲 数据结构
  • 百度C++一面-面经总结
  • 制药污水处理设备流程说明介绍
  • 2024年最强网络安全学习路线,详细到直接上清华的教材!