UE引擎实现ShadowMap、体积光(C++)

前言

        整体上参考了YivanLee大佬的这两篇文:

虚幻4渲染编程(灯光篇)【第一卷:各种ShadowMap】

虚幻4渲染编程(灯光篇)【第二卷:体积光】

正文

1、ShadowMap

(1)创建工程

        先创建一个第三人称的C++工程,新增一个materials文件夹存放ShadowMap和体积光材质。

(2)获取光源位置及变换矩阵

        ShadowMap简单来说在光源位置放一个摄像机,保存这个摄像机渲染出来的深度纹理。对于想要显示阴影的材质,获取当前像素的世界空间坐标,变换到光源摄像机的裁剪空间,用像素的Z分量(深度)与深度纹理对应UV的深度值比较。如果像素的Z分量大于深度纹理的深度值,表示该像素处于阴影中。

        为了在虚幻引擎中实现上述效果,首先我们需要捕获光源摄像机的深度纹理,这里需要“场景捕获2D”组件,将其放置在场景中充当光源。

        之后,在内容浏览器中右键->材质和纹理->渲染目标,创建渲染目标用于保存光源摄像机渲染出的深度问题。

        之后回到光源摄像机,在其细节栏中添加刚才创建的渲染目标,捕获源选择场景深度。投射类型选择透视投影,这里实现的是点光源的阴影(阴影会在不同方向扭曲变形),如果想实现平行光的阴影需要将投射类型改成正交投影(后续会简单介绍实现方法)。

        至此,我们已经拿到了光源摄像机的深度纹理,接下来我们需要将像素的世界坐标转换到光源摄像机的裁剪空间坐标。这里需要用到OpenGL中MVP矩阵的相关知识。裁剪空间实际上是投影空间的子空间(即摄像机可见的部分),因此我们需要构造出光源摄像机的VP矩阵(View,projection)。 

        首先是View矩阵,参考LookAt矩阵的公式可知,我们需要获取光源摄像机的右向量,上向量,方向向量(这里说成前向量我觉得更好理解)以及摄像机位置。

        我们给光源摄像机(即场景捕获2DActor)添加C++组件,在其BeginPlay()中添加如下代码获取上述数据。其中向量ViewColX、ViewColY、ViewColZ、ViewColW为View矩阵每行的分量。可以看到,我们构造出来的View矩阵实际是LookAt矩阵的转置矩阵。原因后面会解释。另外,这里不要使用虚幻自带的函数计算View矩阵,这是因为虚幻引擎中X分量是前向量,而虚幻提供的透视投影矩阵函数是以Z分量为前向量计算的。因此我们需要自己构建出以Z分量为前向量的View矩阵。

ASceneCapture2D* owner = Cast<ASceneCapture2D>(GetOwner());
if (owner) {owner->CalcCamera(0, ViewInfo);FVector forwardV = owner->GetActorForwardVector(); 		FVector rightV = owner->GetActorRightVector()FVector upV = owner->GetActorUpVector();FVector loc = ViewInfo.Location;FVector inw = FVector(-FVector::DotProduct(rightV, loc), -FVector::DotProduct(upV, loc), -FVector::DotProduct(forwardV, loc));// 获取View矩阵列向量FLinearColor ViewColX = FLinearColor(rightV.X, upV.X, forwardV.X, 0);FLinearColor ViewColY = FLinearColor(rightV.Y, upV.Y, forwardV.Y, 0);FLinearColor ViewColZ = FLinearColor(rightV.Z, upV.Z, forwardV.Z, 0);FLinearColor ViewColW = FLinearColor(-FVector::DotProduct(rightV, loc), -FVector::DotProduct(upV, loc), -FVector::DotProduct(forwardV, loc), 1);
}

        接着,我们需要构造出投影矩阵,由于光源摄像机用的是透视投影,这里也需要构造透视投影矩阵。我们使用虚幻引擎自带的函数创建,代码如下。

	// 构建投影矩阵float FOV = ViewInfo.FOV;//float AspectRatio = ViewInfo.OrthoWidth/ ViewInfo.OffCenterProjectionOffset.X;float heigh = ViewInfo.OrthoWidth / ViewInfo.AspectRatio;float NearPlane = ViewInfo.OrthoNearClipPlane;float FarPlane = ViewInfo.OrthoFarClipPlane;// 注意:FOV要送入弧度float rad = FMath::DegreesToRadians(FOV / 2);ProjectionMatrix = FPerspectiveMatrix(rad, ViewInfo.OrthoWidth, heigh, NearPlane, FarPlane);// 构建投影矩阵行向量FLinearColor ProjectionMatrixColX = FLinearColor(ProjectionMatrix.M[0][0], ProjectionMatrix.M[0][1], ProjectionMatrix.M[0][2], ProjectionMatrix.M[0][3]);FLinearColor ProjectionMatrixColY = FLinearColor(ProjectionMatrix.M[1][0], ProjectionMatrix.M[1][1], ProjectionMatrix.M[1][2], ProjectionMatrix.M[1][3]);FLinearColor ProjectionMatrixColZ = FLinearColor(ProjectionMatrix.M[2][0], ProjectionMatrix.M[2][1], ProjectionMatrix.M[2][2], ProjectionMatrix.M[2][3]);FLinearColor ProjectionMatrixColW = FLinearColor(ProjectionMatrix.M[3][0], ProjectionMatrix.M[3][1], ProjectionMatrix.M[3][2], ProjectionMatrix.M[3][3]);

        可以看到这里没有对投影矩阵做转置,要明白其原因我们需要对比透视投影公式以及FPerspectiveMatrix函数源代码。可以看到虽然矩阵公式有所差异(其中的差异本人目前还没有完全理解),但FPerspectiveMatrix函数已经将投影矩阵转置了。

        透视矩阵公式来源:透视投影矩阵推导

        之后,再获取光源位置(可选,可以在后续的体积光中计算某个点的光强度)。

    FLinearColor lightPos = FLinearColor(ViewInfo.Location.X, ViewInfo.Location.Y, ViewInfo.Location.Z, 1);

        至此,光源摄像机的VP矩阵我们已经获取到了,接下来我们需要将这些矩阵传入ShadowMap的材质中。这里使用到虚幻引擎的材质参数集合,内容浏览器中右键->材质和纹理->材质参数集创建。

        之后双击刚创建的材质参数集进入详情页,创建所需的标量参数及向量参数。

        回到光源摄像机(即场景捕获2DActor)C++组件的BeginPlay()函数,获取刚才创建的材质参数集并将VP矩阵、光源位置等信息传入。代码如下。

UMaterialParameterCollection* ParameterCollection = LoadObject<UMaterialParameterCollection>(NULL, TEXT("MaterialParameterCollection'/Game/materials/matrixTransform.matrixTransform'"));
UMaterialParameterCollectionInstance* mpinst = GetWorld()->GetParameterCollectionInstance(ParameterCollection);
if (mpinst) {mpinst->SetVectorParameterValue(FName("viewXcol"), ViewColX);mpinst->SetVectorParameterValue(FName("viewYcol"), ViewColY);mpinst->SetVectorParameterValue(FName("viewZcol"), ViewColZ);mpinst->SetVectorParameterValue(FName("viewWcol"), ViewColW);mpinst->SetVectorParameterValue(FName("perspectiveXcol"), ProjectionMatrixColX);mpinst->SetVectorParameterValue(FName("perspectiveYcol"), ProjectionMatrixColY);mpinst->SetVectorParameterValue(FName("perspectiveZcol"), ProjectionMatrixColZ);mpinst->SetVectorParameterValue(FName("perspectiveWcol"), ProjectionMatrixColW);mpinst->SetVectorParameterValue(FName("lightPos"), lightPos);mpinst->SetScalarParameterValue(FName("zfar"), ViewInfo.OrthoFarClipPlane);mpinst->SetScalarParameterValue(FName("znear"), ViewInfo.OrthoNearClipPlane);
}

        至此,C++侧的准备工作完成,接下来是材质。

(3)创建材质

        内容浏览器右键->材质创建shadowMap材质,并将其加载到需要显示阴影的Actor组件上(如地面)。然后进入材质详情面板。将上一小节创建的材质参数集拖到详情面板中即可获取材质参数集的数据。

        获取像素的世界坐标,通过Transform3x3Matrix节点将世界坐标依次变换到视口空间(View)、透视投影空间(Projection)。

        这里我们进入Transform3x3Matrix节点看下它的实现(如下图)。这里考虑3X3矩阵的情况(不考虑W分量),设输入向量三个分量R,G,B。用于变换的矩阵行分量X(X1, X2, X3),Y(Y1, Y2, Y3),Z(Z1, Z2, Z3)。正常的矩阵乘法有:

\begin{bmatrix} X1 & X2 & X3\\ Y1 & Y2 & Y3\\ Z1 & Z2 & Z3 \end{bmatrix} *\begin{bmatrix} R\\ G\\ B \end{bmatrix} = \begin{bmatrix} R*X1 + G*X2 + B*X3\\ R*Y1 + G*Y2 + B*Y3\\ R*Z1 + G*Z2 + B*Z3 \end{bmatrix}  

        而该节点实现的矩阵乘法则是:

\begin{bmatrix} X1 & X2 & X3\\ Y1 & Y2 & Y3\\ Z1 & Z2 & Z3 \end{bmatrix} *\begin{bmatrix} R\\ G\\ B \end{bmatrix} = \begin{bmatrix} R*X1 + G*Y1 + B*Z1\\ R*X2+ G*Y2 + B*Z2\\ R*X3 + G*Y3 + B*Z3 \end{bmatrix}

        可以看到,变换矩阵是先转置在于输入向量相乘的。这也是为什么我们在第二小节需要将VP矩阵转置再送到材质参数集里。

        像素的世界坐标经过VP矩阵变换后,得到了其在透视投影空间中的坐标。根据透视除法公式,我们给X,Y分量除以View空间下像素坐标的Z分量(通过透视投影矩阵公式可知透视投影空间下的W分量等于View空间下的Z分量),将摄像机可见部分的X、Y坐标限制在(-1, 1)之间。之后再将其压到(0, 1)之间作为UV去采样渲染目标的深度纹理(渲染目标也是通过拖入材质详情中使用),通过除2(乘0.5)加0.5实现(-1, 1)到(0, 1)。注意虚幻的UV左上角是(0, 0),右下角是(1, 1),而投影空间中心为(0, 0),右是X正方向,上是Y正方向,因此V分量需要取反(用1去减)。


        通过UV获取到对应位置的深度之后,将其与投影空间下的Z值进行比较(这里需要加一点点偏移,不然会出现明暗条纹)。如果深度值小于投影空间下的Z值,说明该像素位于阴影中,渲染成黑色,反之为白色。

        之后将输出值送给“自发光颜色”,大功告成。注意,这里插入的if是我用来处理X,Y不在(-1, 1)范围的情况的,这里就不额外介绍了。

(4)效果展示

(5)正交投影

        这里在简单介绍下利用正交投影实现平行光阴影。首先将“场景捕获2D”组件的投射类型改为正交。C++侧通过函数FOrthoMatrix获取正交投影矩阵,送入材质参数集的方式不变。在材质中,获取UV的方式改为:

        这里不用乘0.5再加0.5了,直接加0.5即可。原因在于FOrthoMatrix函数获取的矩阵,对比正交矩阵公式可知该函数返回的矩阵长度就是1,不需要再除以2了。

        正交矩阵推导可参考:【计算机图形学基础】投影矩阵

2、体积光

(1)基本思路

        通过后处理的方式,使用RayMarching算法,计算每个屏幕像素的光强度,再与屏幕纹理叠加。

(2)创建后处理材质

        在虚幻引擎中,要使用后处理材质,首先需要一个后处理体积Actor作为载体。创建方式如下图。后处理材质贴在该体积上,玩家摄像机进入该体积时后处理材质生效。这里可以将该体积直接作为玩家角色的子Actor,使得后处理材质一直生效。

        新建一个材质,材质域选择后期处理,这样该材质就可以贴到后期处理体积上使用啦。后期处理简单来说就是对渲染流程生成的一张张屏幕大小的图片进行处理,也可以理解是图像处理。

        这里要使用材质里的custom节点(如下图),这是一个允许我们自己写HLSL代码的节点。输入参数及输出类型需要在细节一栏手动配置。这里的输入参数不需要定义类型,在代码中可以直接通过其变量名使用。

        这个节点虽然支持我们自己写代码,但是不能直接定义函数。这里有一个坑,我们可以查看当前材质的着色器代码。

        找到我们自定义的代码,可以发现我们的代码是放在一个预先定义好的函数里,函数内不能再定义函数。难道我们就不能在custom节点里定义函数了吗?其实是可以的,具体方法在第三小节介绍。

(3)实现RayMarching算法

        RayMarching算法的原理网上有很多讲解,这里主要讲在虚幻引擎的材质中如何实现RayMarching算法。首先我们拿到像素点对应的世界坐标,以摄像机位置为起点,摄像机位置到该世界坐标的方向为步进方向。通过custom节点实现步进算法,输出该像素点的光强度,最后再与场景纹理叠加。custom节点代码、以及细节配置如下:

struct MB {float3 transform(float3 inp, float3 x, float3 y, float3 z, float3 w){float3 outx = inp.x * x;float3 outy = inp.y * y;float3 outz = inp.z * z;float3 outxy = outx + outy;float3 outzw = outz + w;return outxy + outzw;}
}BaseModel;float lindensity = 0.0f;
float lengthperstep = 10;
float lightinsperlit = 1500;
float lightinsperunlit = 6000;
// pos为步进中的坐标,以摄像机的位置为起点
float3 pos = cameraPos;
for (int i = 0; i < (int)maxLength; i++)
{// 坐标转换到光源摄像机View空间float3 posInView = BaseModel.transform(pos, ViewXcol.xyz, ViewYcol.xyz, ViewZcol.xyz, ViewWcol.xyz);// 坐标转换到光源摄像机透视投影空间float3 posInPer = BaseModel.transform(posInView, PerXcol.xyz, PerYcol.xyz, PerZcol.xyz, PerWcol.xyz);// 透视除法posInPer.x = posInPer.x / posInView.z;posInPer.y = posInPer.y / posInView.z;float2 uv;uv.x = (posInPer.x * 0.5 + 0.5);uv.y = 1 - (posInPer.y * 0.5 + 0.5) ;if (uv.x > 1 || uv.y > 1 || uv.x < 0 || uv.y < 0 || posInPer.z < 0) {// 该坐标不在光源摄像机视口范围,不处理pos = pos + (lengthperstep * lightVecNor);continue;}// 光源摄像机深度纹理采样float depth = Texture2DSample(DtextureMap, DtextureMapSampler, uv) + 1.5;if (depth > posInPer.z) {// 该坐标在光源内,加一点光强度lindensity +=(lightinsperlit / (distance(pos, lightPos)*distance(pos, lightPos)));}else {// 该坐标在阴影内,减一点光强度,这里是为了让暗的部分更突出lindensity -= (lightinsperunlit / (distance(pos, lightPos)*distance(pos, lightPos)));}// lightVecNor为摄像机位置到像素坐标方向的单位向量pos = pos + (lengthperstep * lightVecNor);
}
return lindensity;

        对于第二小节定义函数的问题,在custom的代码中,我们可以定义一个结构体,在结构体内定义函数。通过结构体对象我们就可以调用函数啦。这里的custom节点看着吓人,其实算法本身不复杂,麻烦的部分是将材质节点Transform3x3Matrix代码化(代码中的transform函数)。

(4)效果展示

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/35883.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【浦语开源】深入探索:大模型全链路开源组件 InternLM Lagent,打造灵笔Demo实战指南

一、准备工作&#xff1a; 1、环境配置&#xff1a; pip、conda换源&#xff1a; pip临时换源&#xff1a; pip install -i https://mirrors.cernet.edu.cn/pypi/web/simple some-package# 这里的“https://mirrors.cernet.edu.cn/pypi/web/simple”是所换的源&#xff0c;…

C++设计模式---模版方法模式

1、介绍 概念&#xff1a; 模板方法模式&#xff08;Template Method Pattern&#xff09;【行为型模式】&#xff0c;定义一个操作中算法的骨架&#xff0c;而将一些步骤延迟到子类中&#xff0c;模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 …

AI绘画Stable Diffusion人物背景替换实操教程,让创意无限延伸

大家好&#xff0c;我是画画的小强 Stable Diffusion以其强大的能力可以实现人物背景的更换。本文将带你深入了解如何利用Stable Diffusion中的Inpaint Anything插件快速且精准地实现人物背景的替换&#xff0c;从而让你的图片焕发新生。 前期准备 本文会使用到Inpaint Anyt…

密码学及其应用——专用名词(法语版)

1. 密码学概念 1. cryptographie - 密码学 2. cryptosystme - 密码系统 3. cryptographie symtrique - 对称密码学 4. cryptographie asymtrique - 非对称密码学 5. cryptographie quantique - 量子密码学 6. stganographie - 隐写术 2. 加密与解密 1. cryptage - 加密 2. dc…

如何优化Spring Boot应用的启动时间

如何优化Spring Boot应用的启动时间 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将讨论如何优化Spring Boot应用的启动时间&#xff0c;提升应用的性…

代码随想录算法训练营第六天|242.有效的字母异位词、349. 两个数组的交集、202. 快乐数、1. 两数之和

打卡Day6 1.哈希表理论基础2.242.有效的字母异位词2.349. 两个数组的交集3.202. 快乐数4.1. 两数之和 1.哈希表理论基础 文档讲解&#xff1a; 代码随想录 当遇到要快速判断一个元素是否出现在集合中时&#xff0c;要考虑哈希法。但是哈希法牺牲了空间换取了时间&#xff0c;…

观星观景大屏呈现 实时拍摄长焦定格 当当狸智能天文望远镜TW2来啦

《宇宙的奇迹》中有这样一句话&#xff1a;“我们与那些遥远星系息息相关&#xff0c;无论它们是如何与我们天各一方&#xff0c;那些经过数十亿年旅行到达地球的光线&#xff0c;终究会把我们联系在一起”。 想象一下—— 等到繁星低垂&#xff0c;月光皎洁之时&#xff0c;…

随机森林回归原理详解及Python代码示例

随机森林回归原理详解 随机森林回归&#xff08;Random Forest Regression&#xff09;是一种集成学习方法&#xff0c;通过构建多个决策树并将它们的预测结果进行平均&#xff0c;来提高模型的稳定性和预测准确性。它通过引入随机性来增强模型的泛化能力&#xff0c;有效减轻了…

【SQL Server数据库】存储过程的使用

目录 一、要求 1&#xff0e;创建一个存储过程 Proc_Course&#xff0c;查看“0108”号课程的选修情况&#xff0c;包括学生学号、姓名和成绩&#xff0c;然后执行该过程 2&#xff0e;创建一个存储过程 Proc_SC1&#xff0c;通过学生学号来查询学生选修情况&#xff0c;默认…

Spring Cloud Gateway 跨域配置和跨服务请求跟踪

文章目录 引言I Spring Cloud Gateway 跨域配置1.1 网关统一处理:配置文件-推荐1.2 网关统一处理:配置类方式1.3 微服务处理,网关侧不用处理CORS。1.4 子服务依赖配置1.5 网关服务的依赖配置II 跨服务请求日志跟踪2.1 feign 依赖配置2.2 feign子模块将请求头中的参数,全部作…

Linux系统安装和卸载nginx

&#x1f4d6;Linux系统安装和卸载nginx ✅下载✅安装✅启动nginx✅安装成系统服务✅常见问题&#xff1a;80端口被占用了✅卸载✅目录结构 以下介绍的是以源码编译安装方式&#xff1a; ✅下载 官方地址&#xff1a;https://nginx.org/en/download.html 123云盘地址&#x…

pytest的搜索路径和导入模式

搜索路径 指定路径下面所有的 test_*.py or *_test.py 文件 中的所有以test前缀的方法。或者Test前缀类下的所有test 前缀的方法。不管是不是staticmethod and classmethods 官方文档 https://docs.pytest.org/en/8.2.x/explanation/goodpractices.html#test-discovery base…

基于springboot、vue影院管理系统

设计技术&#xff1a; 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatisvue 工具&#xff1a;IDEA、Maven、Navicat 主要功能&#xff1a; 影城管理系统的主要使用者分为管理员和用户&#xff0c; 实现功能包括管理员&#xff1a; 首页…

如何在 Node.js 中使用文件系统

前言&#xff1a;Web 应用程序并不总是需要写入文件系统&#xff0c;但 Node.js 提供了一个全面的应用程序编程接口 (API) 来实现这一点。如果您要输出调试日志、将文件传输到服务器或从服务器传输文件&#xff0c;或者创建命令行工具&#xff0c;那么它可能是必不可少的。 值得…

ELK集群设置密码

一、软件安装清单 elasticsearch7.17.22logstash7.17.22kibana:7.17.22filebeat7.17.22elasticsearch-head:5 二、配置 生成证书 进入elasticsearch容器 bin/elasticsearch-certutil cert -out /usr/share/elasticsearch/config/elastic-certificates.p12 -pass将证书拷贝…

HTML DOM 修改 HTML 内容

HTML DOM 修改 HTML 内容 HTML DOM(文档对象模型)是 HTML 和 XML 文档的编程接口。它提供了对文档的结构化表示,并定义了一种方式来访问和操作文档的内容、结构和样式。在网页开发中,使用 HTML DOM 可以动态地修改 HTML 元素的内容、属性和样式。 基本概念 在 HTML DOM …

在Ubuntu上安装和配置配置服务器防火墙(CSF)的方法

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 简介 Config Server Firewall&#xff08;CSF&#xff09;是大多数 Linux 发行版和基于 Linux 的 VPS 的免费高级防火墙。除了基本的防…

qt for android 工程添加AndroidManifest.xml 文件

1.选择左边图形栏目中的Projects&#xff0c;在Build steps下的Build Android APK中Details 2.点击Create Templates&#xff0c;并勾选 此时在工程下面会多出一个文件夹android 3.将这个android的中所有文件加入工程中&#xff0c;编辑.pro 4.通过QT 图形化编辑设置属性&#…

JAVA【案例5-5】二月天

【二月天】 1、案例描述 二月是一个有趣的月份&#xff0c;平年的二月有28天&#xff0c;闰年的二月由29天。闰年每四年一次&#xff0c;在判断闰年时&#xff0c;可以使用年份除于4&#xff0c;如果能够整除&#xff0c;则该年是闰年。 本案例要求编写一个程序&#xff0c;…

项目前端遇到的相关问题及解决办法

一.在IDEA中修改html文件后,网页却不更新,即使重启服务,页面也不更新 由于本地静态网页缓存,导致浏览器不访问服务器的新资源,解决办法如下: 打开网页后,按“F12”调出“开发人员工具”。长按浏览器左上角的“刷新”按钮直至弹出对话框,选择“清空缓存并进行硬刷新”。…