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;…

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

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

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

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

Linux系统安装和卸载nginx

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

基于springboot、vue影院管理系统

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

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将证书拷贝…

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;…

python e怎么表示

exp()方法返回x的指数&#xff0c;ex。 语法 以下是 exp() 方法的语法: import math math.exp( x ) 注意&#xff1a;exp()是不能直接访问的&#xff0c;需要导入 math 模块&#xff0c;通过静态对象调用该方法。 参数 x -- 数值表达式。 返回值 返回x的指数&#xff0c;…

01背包问题求解

来源于 https://kamacoder.com/problempage.php?pid1046 使用动态规划&#xff0c;五步走 1.定义状态数组和具体状态含义&#xff1a; dp是个二维数组&#xff0c;第一维代表物品索引&#xff0c;第二维代表背包空间状态。 dp[i][j]是指物品i 在背包空间j 的情况下所能放的…

【redis】redis安装

1、安装前准备 1.1环境准备 VMware安装 参考博文&#xff1a;【VMware】VMware虚拟机安装_配置_使用教程_选择虚拟机配置选项,设置dvd镜像为 点击启动虚拟机-CSDN博客 安装centOS的linux操作系统 xshell xftp 参考博文&#xff1a;【Linux】Xshell和Xftp简介_安装_VMwar…

最新版Git安装指南使用指南

首先&#xff0c;访问Git的官方网站https://git-scm.com下载适用于您操作系统的安装包。您也可以选择使用阿里云镜像来加速下载过程。 也可以用国内地址下载https://pan.quark.cn/s/0293d76e58bchttps://pan.quark.cn/s/0293d76e58bc安装过程 在这里插入图片描述 2、点击“…

vue3 Cesium 离线地图

源码&#xff1a;cesium-demo: Cesium示例工程&#xff0c;基于vue3 1、vite-plugin-cesium 是一个专门为 Vite 构建工具定制的插件&#xff0c;用于在 Vite 项目中轻松使用 Cesium 库。它简化了在 Vite 项目中集成 Cesium 的过程。 npm i cesium vite-plugin-cesium vite -D…

[leetcode]k-th-smallest-in-lexicographical-order 字典序的第K小数字

. - 力扣&#xff08;LeetCode&#xff09; class Solution { public:int getSteps(int curr, long n) {int steps 0;long first curr;long last curr;while (first < n) {steps min(last, n) - first 1;first first * 10;last last * 10 9;}return steps;}int find…

WEB界面上使用ChatGPT

&#xff08;作者&#xff1a;陈玓玏&#xff09; 开源项目&#xff0c;欢迎star哦&#xff0c;https://github.com/tencentmusic/cube-studio 随着大模型不断发展&#xff0c;现在无论写代码&#xff0c;做设计&#xff0c;甚至老师备课、评卷都可以通过AI大模型来实现了&…

开发小技巧Tips-----在Idea中配置nacos/redis等

背景&#xff1a; 进入了一个新的项目开发&#xff0c;领导为了加快开发速度&#xff08;加快调试的速度&#xff09;&#xff0c;让我们在本地启动服务&#xff0c;然后给了我一堆数据就走了。坏了坏了&#xff0c;啥意思啊&#xff0c;自己开发的时候本地就是直接点击一下run…

在vscode 中ssh连接虚拟ubuntu,不能使用code打开文件

这是参考别人的文章&#xff1a;https://blog.csdn.net/weixin_44465434/article/details/130035032找到vscode的版本信息&#xff0c;提交后面是需要的打开home/(用户)/.bashrc&#xff0c;添加环境变量 export PATH"~/.vscode-server/bin/5437499feb04f7a586f677b155b03…

江协科技51单片机学习- p16 矩阵键盘

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

LeetCode 算法:验证二叉搜索树 c++

原题链接&#x1f517;&#xff1a;验证二叉搜索树 难度&#xff1a;中等⭐️⭐️ 题目 给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&#xff1a; 节点的左 子树 只包含 小于 当前节点的数。节点的右子树只包含 大于…

J1939与CAN标准报文的区别

J1939报文:J1939是在CAN2.0B(扩展CAN)的基础上,对仲裁场部分的29位ID的重新解释,其它部分完全一样。 29位ID分为:3位的优先级、8位的PF(帧格式)、8位的PS(帧扩展)、8位的SA(源地址)、1位的DP(Data Page数据页)、1位的保留位。 其中1位的DP、8位的PF、8位的PS组成…