UE4Shader 环境设置
usf 和 ush 是 UE4 的 shader 文件格式,相当于 C++ 的 .cpp 和 .h 。那么这些文件该存在哪里我们才能在 customNode 节点里面引入使用呢?我这里先随便先弄 #include 一个 usf
可以看到由于我们存放到指定 Map 的位置,这些 Shader 文件是无法被正确 #include 的,所以接下来我们的操作就是要新建一个插件用于存储 Shader ,并且在插件导入的时候让 UE4 正确识别 Shader 文件的路径
新建插件
cpp 文件修改如下
#include "OpdaShaderPlugin.h"
#include <Interfaces\IPluginManager.h>
#define LOCTEXT_NAMESPACE "FOpdaShaderPluginModule"
void FOpdaShaderPluginModule::StartupModule()
{
// 这段代码将在模块加载到内存后执行; 具体的计时在每个模块的 .uplugin 文件中指定
/// @note 获取插件下的 Shader 文件夹
FString ShaderDirectory = FPaths::Combine(IPluginManager::Get().FindPlugin("OpdaShaderPlugin")->GetBaseDir()
, TEXT("Shaders"));
UE_LOG(LogLoad, Warning, TEXT("%s"),*ShaderDirectory);
/// @note 将这个文件夹添加到 Shader 的虚拟路径上
AddShaderSourceDirectoryMapping("/Opda", ShaderDirectory);
}
void FOpdaShaderPluginModule::ShutdownModule()
{
ResetAllShaderSourceDirectoryMappings();
// 这个函数可能会在关机期间被调用来清理你的模块。 对于支持动态重载的模块,我们在卸载模块之前调用这个函数。
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FOpdaShaderPluginModule, OpdaShaderPlugin)
在插件的 Build.cs 添加依赖模块
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class OpdaShaderPlugin : ModuleRules
{
public OpdaShaderPlugin(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"RenderCore",
"RHI",
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"Projects",
// ... add private dependencies that you statically link with here ...
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}
记得要在插件文件夹下新建 Shaders 文件夹,否则编译会报错
创建一个最简单的 usf 文件
COPYreturn Col;
非常完美
RayMarch
RayMarch 主要分为 3 个步骤:
- 构建距离场
- 渲染距离场
- SDF 渲染
RayMarch 主要通过摄像机发出屏幕射线根据与物体的距离进行渲染,适合制作云等需要大量顶点构建的物体。
构建距离场
构建一个简单的球体距离场,return 回一个正值则表面 pos 在球体外面,返回一个负值则证明在球体的里面
// 构建球体距离场
float sphere(float3 pos, float3 sphereCenter, float sphereRadius)
{
return distance(pos, sphereCenter) - sphereRadius;
}
其次场景中可能有多个不同物体的距离场,所以我们需要 SceneSDF 来统一管理距离场
// 将多个距离场合并,操作
float sceneSDF(float3 pos)
{
float sphere_1 = sphere(pos, float3(0.0f, 0.0f, 0.0f), 100.0f);
float sphere_2 = sphere(pos, float3(100.0f, 0.0f, 0.0f), 50.0f);
return min(sphere_1,sphere_2);
}
距离场渲染
首先我们需要摄像机位置,射线方向,最大步长数,最大距离,通过屏幕射线与物体求交得到场景的距离场
// 屏幕射线与物体表面求交
float2 RayMarch(float3 cameraPos, float3 direction, float MAX_Step, float MAX_Distance)
{
float d0 = 0.0f; // SDF 值
int hit = 0; // 用于判断是否撞击成功
for (int i = 0; i < MAX_Step; i++)
{
float3 pos = cameraPos + direction * d0; // 更新当前发出射线之后的位置
float distance = sceneSDF(pos); // 检测当前 pos 与场景的距离值
d0 += distance; // 累计 SDF
// 当距离值足够小的时候,就可以认为射线撞击到了物体
if (distance < 0.1f)
{
hit = 1;
break;
}
// 当距离值足够大但依然没有物体存在,那就可以打断循环
else if (distance > MAX_Distance)
{
hit = 0;
break;
}
}
// 返回 SDF
return float2(d0, hit);
}
返回的 d0 就代表着摄像机各个像素到物体的距离,我们可以利用 d0 来进行很多操作了。
SDF 渲染
物体的位置我们可以通过一下方法求得,通过摄像机 pos + 屏幕射线*d0 则可以得到物体的 pos
float2 d = FS.RayMarch(CameraPos,Direction,MAX_Step,MAX_Distance);
float3 pos = CameraPos + Direction * d.x;
接下来我们可以通过计算相邻 xyz 点的 SDF 差值来的到近似的法线方向
// 计算法线
float3 GetNormal(float3 pos)
{
//通过计算xyz三个方向的差值,归一化得到近似的法线方向
float D_value = 0.001f;
return normalize(float3(
sceneSDF(float3(pos.x + D_value, pos.y, pos.z)) - sceneSDF(float3(pos.x - D_value, pos.y, pos.z)),
sceneSDF(float3(pos.x, pos.y + D_value, pos.z)) - sceneSDF(float3(pos.x, pos.y - D_value, pos.z)),
sceneSDF(float3(pos.x, pos.y, pos.z + D_value)) - sceneSDF(float3(pos.x, pos.y, pos.z - D_value))));
}
得到法线之后,我们就可以进行漫反射+高光,但是我们可以将法线方向交给 UE4 材质,就可以享受 UE4 的光照效果了
最终效果
完整代码
首先新建一个 ush 和 usf 文件,ush 用于存放方法,
注意如果需要定义多个方法,我们需要将方法都放进一个
struct里面才能使用,否则会报错
ush 如下
#pragma once
struct FunctionStruct
{
//构建球体距离场
float sphere(float3 pos, float3 sphereCenter, float sphereRadius)
{
return distance(pos, sphereCenter) - sphereRadius;
}
//将多个距离场合并,操作
float sceneSDF(float3 pos)
{
float sphere_1 = sphere(pos, float3(0.0f, 0.0f, 0.0f), 100.0f);
float sphere_2 = sphere(pos, float3(100.0f, 0.0f, 0.0f), 50.0f);
return min(sphere_1, sphere_2);
}
//屏幕射线与物体表面求交
float2 RayMarch(float3 cameraPos, float3 direction, float MAX_Step, float MAX_Distance)
{
float d0 = 0.0f; //SDF值
int hit = 0; //用于判断是否撞击成功
for (int i = 0; i < MAX_Step; i++)
{
float3 pos = cameraPos + direction * d0; //更新当前发出射线之后的位置
float distance = sceneSDF(pos); //检测当前pos与场景的距离值
d0 += distance; //累计SDF
//当距离值足够小的时候,就可以认为射线撞击到了物体
if (distance < 0.1f)
{
hit = 1;
break;
}
//当距离值足够大但依然没有物体存在,那就可以打断循环
else if (distance > MAX_Distance)
{
hit = 0;
break;
}
}
//返回SDF
return float2(d0, hit);
}
//计算法线
float3 GetNormal(float3 pos)
{
//通过计算xyz三个方向的差值,归一化得到近似的法线方向
float D_value = 0.001f;
return normalize(float3(
sceneSDF(float3(pos.x + D_value, pos.y, pos.z)) - sceneSDF(float3(pos.x - D_value, pos.y, pos.z)),
sceneSDF(float3(pos.x, pos.y + D_value, pos.z)) - sceneSDF(float3(pos.x, pos.y - D_value, pos.z)),
sceneSDF(float3(pos.x, pos.y, pos.z + D_value)) - sceneSDF(float3(pos.x, pos.y, pos.z - D_value))));
}
//计算光照
float3 CalLight(float3 pos, float3 normal)
{
float3 lightPos = float3(1.0f, 1.0f, 0.0f);
float3 diffuse = dot(normal, normalize(lightPos)) / 2 + 0.5;
return diffuse;
}
};
usf 如下
#include "MyFirstShader.ush"
FunctionStruct FS;
float2 d = FS.RayMarch(CameraPos,Direction,MAX_Step,MAX_Distance);
float3 pos = CameraPos + Direction * d.x;
float3 normal = FS.GetNormal(pos);
return float4(normal,d.y);
材质中记得要把切线空间法线取消勾选,否则会出现奇怪效果