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

news2024/11/19 5:33:18

前言

        整体上参考了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.coloradmin.cn/o/1867378.html

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

相关文章

Golang | Leetcode Golang题解之第199题二叉树的右视图

题目&#xff1a; 题解&#xff1a; /** 102. 二叉树的递归遍历*/ func levelOrder(root *TreeNode) [][]int {arr : [][]int{}depth : 0var order func(root *TreeNode, depth int)order func(root *TreeNode, depth int) {if root nil {return}if len(arr) depth {arr a…

Open3D 显示带有强度的点云数据

目录 一、概述 1.1强度信息的意义 1.2应用场景 二、代码实现 三、实现效果 一、概述 在点云数据中&#xff0c;强度&#xff08;Intensity&#xff09;指的是激光雷达传感器在扫描环境时&#xff0c;每个点返回的反射强度值。这些强度值代表了激光脉冲返回的能量&#xff…

基于振弦采集仪的工程安全监测技术研究与应用

基于振弦采集仪的工程安全监测技术研究与应用 随着工程规模的不断扩大和复杂性的增加&#xff0c;工程安全监测变得越来越重要。工程安全监测的目的是保证工程的安全运行&#xff0c;预防事故的发生&#xff0c;保护人们的生命财产安全。其中&#xff0c;振弦采集仪作为一种重…

sys.stdout.write()方法——标准输出打印

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 语法参考 Sys.Stdout是sys模块中的标准输出对象&#xff0c;可以实现将数据向屏幕、文件等进行输出。Sys.Stdout通过write 方法实现数据的标准输出。…

Pinia的基本用法

Pinia的安装和引入 1.安装Pinia npm install pinia2. 在vue项目的main.js文件中引入pinia import { createApp } from vue import { createPinia } from pinia import App from ./App.vueconst pinia createPinia() const app createApp(App)app.use(pinia) app.mount(#ap…

LabVIEW电梯钢丝绳实时监测系统

电梯作为现代高层建筑中不可或缺的交通工具&#xff0c;其安全性直接影响到乘客的生命财产安全。电梯钢丝绳作为承载乘客与货物的关键部件&#xff0c;其健康状况尤为重要。传统的钢丝绳检测方法大多依赖于定期检查&#xff0c;无法实现实时监控&#xff0c;存在一定的安全隐患…

LabVIEW技术交流-布尔灯仿真数码管

问题来源 闲来无事&#xff0c;逛论坛问答&#xff0c;看到这样一个问题&#xff0c;觉得有意思&#xff0c;就自己尝试下。 这个功能其实是不难的&#xff0c;就是显示不同的数值时&#xff0c;对相应的布尔灯进行真假值操作就行了。但是我又想到了更有趣的玩法&#xff0c;能…

【unity笔记】七、Mirror插件使用

一、简介 Mirror 是一个用于 Unity 的开源多人游戏网络框架&#xff0c;它提供了一套简单高效的网络同步机制&#xff0c;特别适用于中小型多人游戏的开发。以下是 Mirror 插件的一些关键特点和组件介绍&#xff1a; 简单高效&#xff1a;Mirror 以其简洁的 API 和高效的网络…

前端:Nuxt2 + Vuetify2

想要开发一个网站&#xff0c;并且支持SEO搜索&#xff0c;当然离不开我们的 Nuxt &#xff0c;那通过本篇文章让我们一起了解一下。如果构建一个Nuxt项目 安装 Nuxt&#xff0c;创建项目 安装nuxt2&#xff0c; 需要node v16&#xff0c;大家记得查看自己的node版本。构建脚…

安全技术和防火墙(iptables)

安全技术 入侵检测系统&#xff1a;特点是不阻断网络访问&#xff0c;主要是提供报警和事后监督&#xff0c;不主动介入&#xff0c;类似于监控。 入侵防御系统&#xff1a;透明模式工作&#xff0c;对数据包&#xff0c;网络监控&#xff0c;服务攻击&#xff0c;木马&#…

实时显示用户输入PySide6实例

如何用 PySide6 实现QLabel 实时显示用户在 QLineEdit 内输入的内容&#xff1f; 示例代码&#xff1a; # QLineEdit 用户输入内容&#xff0c;QLabel 即时显示用户输入训练from PySide6.QtWidgets import (QApplication, QWidget,QLabel, QLineEdit, QVBoxLayout)class MyWi…

Python | Leetcode Python题解之第200题岛屿数量

题目&#xff1a; 题解&#xff1a; class Solution:def dfs(self, grid, r, c):grid[r][c] 0nr, nc len(grid), len(grid[0])for x, y in [(r - 1, c), (r 1, c), (r, c - 1), (r, c 1)]:if 0 < x < nr and 0 < y < nc and grid[x][y] "1":self.d…

喂饭级AI神器!免代码一键绘制图表,文本数据秒变惊艳视觉盛宴!

由于目前的AI生成图表工具存在以下几个方面的问题&#xff1a; 大多AI图表平台是纯英文&#xff0c;对国内用户来说不够友好&#xff1b;部分平台在生成图表前仍需选择图表类型、配置项&#xff0c;操作繁琐&#xff1b;他们仍需一份规整的数据表格&#xff0c;需要人为对数据…

Qt Quick Effect Maker 工具使用介绍

一、介绍 随着 Qt 版本的不断升级,越来越多的功能被加入 Qt,一些新的工具也随之应运而生,Qt Quick Effect Maker 工具是 Qt 6.5 之后才新添加的工具,之前的名字应该是叫做 Qt shader tool 这个模块。 以下是官方的释义:Qt Quick Effect Maker是一个用于为Qt Quick创建自定…

3.PyQt6常用基本控件

目录 常用控件 1.文本类控件 1.QLable标签控件 1.设置标签文本 2.设置标签文本和对齐方式 3.换行显示 4.添加超链接 5.为标签设置图片 6.获取标签文本 2.QLineEdit单行文本控件 3.QTextEdit多行富文本控件 4.QPlainTextEdit纯文本控件 5.QSpinBox整数数字选择控件 …

C++ | Leetcode C++题解之第200题岛屿数量

题目&#xff1a; 题解&#xff1a; class Solution { private:void dfs(vector<vector<char>>& grid, int r, int c) {int nr grid.size();int nc grid[0].size();grid[r][c] 0;if (r - 1 > 0 && grid[r-1][c] 1) dfs(grid, r - 1, c);if (r …

小白上手AIGC-基于PAI-DSW部署Stable Diffusion文生图Lora模型

小白上手AIGC-基于PAI-DSW部署Stable Diffusion文生图Lora模型 前言资源准备开启体验服务创建工作空间 部署服务创建DSW实例安装Diffusers启动WebUI 写在最后 前言 在上一篇博文小白上手AIGC-基于FC部署stable-diffusion 中&#xff0c;说到基于函数计算应用模板部署AIGC文生图…

Java之线程相关应用实现

后台线程 一个进程中只有后台进程运行&#xff0c;该进程将会结束。 新创建的线程默认为前台线程&#xff0c;Java中只要有一个前台线程运行&#xff0c;就不会结束程序&#xff0c;如果只有后台线程运行&#xff0c;程序就会结束&#xff0c;可以在线程对象启动前执行setDae…

工业AIoT竞赛流程

不要点到重置&#xff01;&#xff01;&#xff01;要刷新虚拟机就点重启 xshell连接虚拟机&#xff1a;ssh rootPublic IP 环境构建 vim /etc/hosts 按 i 进入插入模式&#xff0c;加内网ip和主机名&#xff0c;按esc&#xff0c;按 : &#xff0c;按wq 三个虚拟机都这样配 …

HQChart使用教程30-K线图如何对接第3方数据41-分钟K线叠加股票增量更新

HQChart使用教程30-K线图如何对接第3方数据40-日K叠加股票增量更新 叠加股票叠加分钟K线更新Request 字段说明Data.symbol 协议截图返回json数据结构overlaydata HQChart代码地址交流 叠加股票 示例地址:https://jones2000.github.io/HQChart/webhqchart.demo/samples/kline_i…