【转载】UE4 —— RayMarching 笔记

868 阅读4分钟

原文链接:《UE4---RayMarching笔记》 | 作者:Opda

UE4Shader 环境设置

usfush 是 UE4 的 shader 文件格式,相当于 C++ 的 .cpp 和 .h 。那么这些文件该存在哪里我们才能在 customNode 节点里面引入使用呢?我这里先随便先弄 #include 一个 usf

image-20201230175543818

可以看到由于我们存放到指定 Map 的位置,这些 Shader 文件是无法被正确 #include 的,所以接下来我们的操作就是要新建一个插件用于存储 Shader ,并且在插件导入的时候让 UE4 正确识别 Shader 文件的路径

新建插件

image-20201230175958026

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 文件夹,否则编译会报错

image-20201230193701738

创建一个最简单的 usf 文件

COPYreturn Col;

非常完美

image-20201230201139323

RayMarch

RayMarch 主要分为 3 个步骤:

  1. 构建距离场
  2. 渲染距离场
  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);

}

距离场渲染

image-20210102141752621

首先我们需要摄像机位置射线方向最大步长数最大距离,通过屏幕射线与物体求交得到场景的距离场

img

// 屏幕射线与物体表面求交
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 的光照效果了

最终效果

image-20210102143521881

完整代码

首先新建一个 ushusf 文件,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);

材质中记得要把切线空间法线取消勾选,否则会出现奇怪效果

image.png

参考文章

RayMarch in UE4[第一章-概念与基础场景构建](傻瓜向,看不懂自杀) | 白狸奴