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

【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退出for
            float3 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退出for
            float3 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;//累计
        }

        //更新样本和光能,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;

   
    }

    CurPos += -LocalCamVec;
}
return float4(lightenergy, transmittance);

蓝图

在这里插入图片描述

结果

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

在这里插入图片描述

画饼

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


http://www.kler.cn/a/321464.html

相关文章:

  • 《Probing the 3D Awareness of Visual Foundation Models》论文解析——多视图一致性
  • linux,一、部署LNMP环境二、配置动静分离三、地址重写四、编写systemd Unit文件
  • 云安全之云计算基础
  • 基于STM32设计的森林火灾监测系统(华为云IOT)_263
  • 「人眼视觉不再是视频消费的唯一形式」丨智能编解码和 AI 视频生成专场回顾@RTE2024
  • React 中如何解析字符串中的 html 结构
  • 代码随想录打卡Day39
  • 【devops】devops-ansible模块介绍
  • 卷积神经网络-迁移学习
  • Spire.PDF for .NET【页面设置】演示:对PDF 文件进行分页
  • 【ASE】第一课_双面着色器
  • 增量式编码器实现原理
  • 使用python爬取豆瓣网站?如何简单的爬取豆瓣网站?
  • FPGA中系统门数和逻辑门数的理解
  • 智视臂传-AI视觉触感未来丨OPENAIGC开发者大赛高校组AI创作力奖
  • 计算机毕业设计 基于Hadoop的智慧校园数据共享平台的设计与实现 Python 数据分析 可视化大屏 附源码 文档
  • 性能设计模式
  • 1.6 判定表
  • 【C++与数据结构】搜索二叉树(BinarySearchTree)
  • 数据仓库-数据质量规范
  • 问:聊聊JAVA中的共享锁和独占锁?
  • 了解针对基座大语言模型(类似 ChatGPT 的架构,Decoder-only)的重头预训练和微调训练
  • 前端Vue 基础学习1
  • 暗黑破坏神4新资料片憎恶之躯即将上线,第六赛季暗黑破坏神4搬砖如何获得最大收益?
  • 响应式的几种解决方案——媒体查询、flex、grid、多列布局、瀑布流和数据可视化屏幕的缩放处理
  • 极狐GitLab 17.4 重点功能解读【三】