UE4---RayMarching笔记

image-20210102144520239

UE4Shader环境设置

usf和ush是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()
{
    //获取插件下的Shader文件夹
    FString ShaderDirectory = FPaths::Combine(IPluginManager::Get().FindPlugin("OpdaShaderPlugin")->GetBaseDir()
        , TEXT("Shaders"));
    UE_LOG(LogLoad, Warning, TEXT("%s"),*ShaderDirectory);
    //将这个文件夹添加到Shader的虚拟路径上
    AddShaderSourceDirectoryMapping("/Opda", ShaderDirectory);
    // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
}

void FOpdaShaderPluginModule::ShutdownModule()
{
    ResetAllShaderSourceDirectoryMappings();
    // This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,
    // we call this function before unloading the module.

}

#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文件

return Col;

非常完美

image-20201230201139323

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

    }

距离场渲染

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

完整代码

首先新建一个ush和usf文件,ush用于存放方法,注意如果需要定义多个方法,我们需要将方法都放进一个struct里面才能使用,否则会报错

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

    }
};
#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-20210102143853340

参考文章

知乎大佬:https://zhuanlan.zhihu.com/p/95547912

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2021 Opda
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信