我的专栏目录:
小IVan:专题概述及目录海洋这个要素在游戏里占比越来越多,开放世界的游戏要求角色能上天入地下水。目前游戏里做海洋的方法就那几种。(1)预烘焙法(2)Gerstner wave(3)FFT海洋。预烘焙法可以是实现烘焙好DisplacementMap或者是FFT的运算结果。Gerstner wave可以在GPU或者CPU上算。FFT的话就是拿海洋频率模型算出Displacement。
在开始研究之前我们需要先搭建起我们的环境。我选择在ComputeShader种完成各种计算,然后在顶点着色器种直接Sample前面的ComputeShader的波形计算结果。不要把波形的计算塞到VertexShader里。把波形计算独立出来还有个好处就是我能把波形的结果储存起来拿给其它效果使用,比如制作浮力部分的时候我们就需要知道海面波形的信息,如果塞VertexShader里就拿不到这些信息了。
搭建ComputeShader的方法前面我的文章有提到,这里我就直接贴代码了。使用的引擎版本是4.21.0,如果引擎更新了新版本可能代码有一点区别。FFT部分我会给出4.22的代码。
如果不是在Unreal中实现或者不想做这么复杂,可以直接跳过这部分。
SDHOcean.build.cs
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class SDHOcean : ModuleRules
{
public SDHOcean(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",
"CoreUObject",
"Engine",
"RHI",
"Engine",
"RenderCore",
"ShaderCore",
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"UnrealEd",
"Projects",
// ... add private dependencies that you statically link with here ...
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}
SDHOcean.h
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
class FSDHOceanModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
SDHOcean.cpp
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
#include "SDHOcean.h"
#define LOCTEXT_NAMESPACE "FSDHOceanModule"
void FSDHOceanModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("SDHOcean"))->GetBaseDir(), TEXT("Shaders"));
AddShaderSourceDirectoryMapping(TEXT("/Plugin/SDHOcean"), PluginShaderDir);
}
void FSDHOceanModule::ShutdownModule()
{
// 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(FSDHOceanModule, SDHOcean)
Ocean.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Runtime/Engine/Classes/Components/ActorComponent.h"
#include "Engine/Classes/Engine/TextureRenderTarget2D.h"
#include "Ocean.generated.h"
typedef TRefCountPtr FTexture2DRHIRef;
typedef TRefCountPtr FUnorderedAccessViewRHIRef;
typedef TRefCountPtr FStructuredBufferRHIRef;
class FRHITexture;
class FRHIUnorderedAccessView;
class FRHICommandListImmediate;
USTRUCT(BlueprintType)
struct FOceanBasicStructData_GameThread
{
GENERATED_USTRUCT_BODY()
FOceanBasicStructData_GameThread(){}
UPROPERTY(BlueprintReadWrite, EditAnywhere)
FVector4 OceanTime_GameThread;
};
UCLASS(hidecategories = (Object, LOD, Physics, Collision), editinlinenew, meta = (BlueprintSpawnableComponent), ClassGroup = Rendering, DisplayName = "OceanRenderComp")
class SDHOCEAN_API UOceanRenderComponent : public UActorComponent
{
GENERATED_BODY()
public:
UOceanRenderComponent(const FObjectInitializer& ObjectInitializer);
//~ Begin UActorComponent Interface.
virtual void OnRegister() override;
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "OceanComponent")
UTextureRenderTarget2D* OutputRenderTarget2D;
//UniformData for Ocean render
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "OceanComponent")
FOceanBasicStructData_GameThread OceanUniformDataBuffer;
int32 TargetSize;
ETextureRenderTargetFormat RenderTargetFormat;
private:
//Render ocean render thread
void OceanCalculating_GameThread();
void OceanCalculating_RenderThread
(
FRHICommandListImmediate& RHICmdList,
ERHIFeatureLevel::Type FeatureLevel,
FRHITexture* OutputRenderTarget,
int32 SurfaceSize,
const FOceanBasicStructData_GameThread& OceanUniformData
);
FTexture2DRHIRef OutputTexture;
FUnorderedAccessViewRHIRef OutputTextureUAV;
};
Ocean.cpp
#include "SDHOcean/Public/Ocean.h"
#include "ShaderCore/Public/GlobalShader.h"
#include "Classes/Engine/World.h"
#include "Public/GlobalShader.h"
#include "Public/PipelineStateCache.h"
#include "Public/RHIStaticStates.h"
#include "Public/SceneUtils.h"
#include "Public/SceneInterface.h"
#include "Public/ShaderParameterUtils.h"
#include "Public/Logging/MessageLog.h"
#include "Public/Internationalization/Internationalization.h"
#include "Public/StaticBoundShaderState.h"
#include "RHI/Public/RHICommandList.h"
#include "RHI/Public/RHIResources.h"
#include "Engine/Classes/Kismet/KismetRenderingLibrary.h"
#include "Runtime/Engine/Classes/Kismet/GameplayStatics.h"
#define LOCTEXT_NAMESPACE "SDHOcean"
BEGIN_UNIFORM_BUFFER_STRUCT(FOceanBasicStructData, )
UNIFORM_MEMBER(FVector4, OceanTime)
END_UNIFORM_BUFFER_STRUCT(FOceanBasicStructData)
IMPLEMENT_UNIFORM_BUFFER_STRUCT(FOceanBasicStructData, TEXT("OceanBasicStructData"))
class FOceeanCSShader : public FGlobalShader
{
DECLARE_SHADER_TYPE(FOceeanCSShader, Global)
public:
FOceeanCSShader() {}
FOceeanCSShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
//TODO Bind pramerter here
OutputBufferSurface.Bind(Initializer.ParameterMap, TEXT("OutputBufferSurface"));
SurfaceClearColor.Bind(Initializer.ParameterMap, TEXT("SurfaceClearColor"));
}
//----------------------------------------------------//
static bool ShouldCache(EShaderPlatform PlateForm)
{
return IsFeatureLevelSupported(PlateForm, ERHIFeatureLevel::SM5);
}
//----------------------------------------------------//
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
}
//----------------------------------------------------//
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
//Define micro here
//OutEnvironment.SetDefine(TEXT("TEST_MICRO"), 1);
}
//----------------------------------------------------//
void SetSurface(FRHICommandList& RHICmdList,
FUnorderedAccessViewRHIRef& OutputUAV,
const FLinearColor ClearColor
)
{
//set the UAV
FComputeShaderRHIParamRef ComputeShaderRHI = GetComputeShader();
if (OutputBufferSurface.IsBound())
RHICmdList.SetUAVParameter(ComputeShaderRHI, OutputBufferSurface.GetBaseIndex(), OutputUAV);
if (SurfaceClearColor.IsBound())
//RHICmdList.SetShaderParameter(GetComputeShader(), SurfaceClearColor.GetBufferIndex(), SurfaceClearColor.GetBaseIndex(), SurfaceClearColor.GetNumBytes(), ClearColor);
SetShaderValue(RHICmdList, GetComputeShader(), SurfaceClearColor, ClearColor);
}
void SetOceanUniformBuffer(FRHICommandList& RHICmdList, const FOceanBasicStructData_GameThread& OceanStructData)
{
FOceanBasicStructData UniformData;
UniformData.OceanTime = OceanStructData.OceanTime_GameThread;
SetUniformBufferParameterImmediate(RHICmdList, GetComputeShader(), GetUniformBufferParameter(), UniformData);
}
void UnBindBuffers(FRHICommandList& RHICmdList)
{
FComputeShaderRHIParamRef ComputeShaderRHI = GetComputeShader();
if (OutputBufferSurface.IsBound())
RHICmdList.SetUAVParameter(ComputeShaderRHI, OutputBufferSurface.GetBaseIndex(), FUnorderedAccessViewRHIRef());
}
virtual bool Serialize(FArchive& Ar) override
{
bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
//Serrilize something here
Ar << OutputBufferSurface << SurfaceClearColor;
return bShaderHasOutdatedParameters;
}
private:
FShaderResourceParameter OutputBufferSurface;
FShaderParameter SurfaceClearColor;
};
IMPLEMENT_SHADER_TYPE(, FOceeanCSShader, TEXT("/Plugin/SDHOcean/Ocean.usf"), TEXT("OceanMainCS"), SF_Compute)
void UOceanRenderComponent::OceanCalculating_RenderThread
(
FRHICommandListImmediate& RHICmdList,
ERHIFeatureLevel::Type FeatureLevel,
FRHITexture* OutputRenderTarget,
int32 SurfaceSize,
const FOceanBasicStructData_GameThread& OceanUniformData
)
{
check(IsInRenderingThread());
check(OutputRenderTarget);
TShaderMapRefOceanComputeShader(GetGlobalShaderMap(FeatureLevel));
RHICmdList.SetComputeShader(OceanComputeShader->GetComputeShader());
if (OutputTexture.IsValid() == false)
{
if (OutputTexture.IsValid())
OutputTexture->Release();
if (OutputTextureUAV.IsValid())
OutputTextureUAV->Release();
FRHIResourceCreateInfo CreateInfo;
OutputTexture = RHICreateTexture2D(SurfaceSize, SurfaceSize, PF_FloatRGBA, 1, 1, TexCreate_ShaderResource | TexCreate_UAV, CreateInfo);
OutputTextureUAV = RHICreateUnorderedAccessView(OutputTexture);
}
OceanComputeShader->SetSurface(RHICmdList, OutputTextureUAV, FLinearColor(1,1,1,1));
OceanComputeShader->SetOceanUniformBuffer(RHICmdList ,OceanUniformData);
DispatchComputeShader(RHICmdList, *OceanComputeShader, SurfaceSize / 32, SurfaceSize / 32, 1);
OceanComputeShader->UnBindBuffers(RHICmdList);
RHICmdList.CopyToResolveTarget(OutputTexture, OutputRenderTarget, FResolveParams());
//FRHICopyTextureInfo copyinfo(SurfaceSize, SurfaceSize);
//RHICmdList.CopyTexture(OutputTexture, OutputRenderTarget, copyinfo);
}
UOceanRenderComponent::UOceanRenderComponent(const FObjectInitializer& ObjectInitializer)
:Super(ObjectInitializer)
{
PrimaryComponentTick.bCanEverTick = true;
bTickInEditor = true;
bAutoActivate = true;
RenderTargetFormat = RTF_RGBA32f;
}
void UOceanRenderComponent::OnRegister()
{
Super::OnRegister();
}
void UOceanRenderComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
//Tick render the ocean
OceanCalculating_GameThread();
}
void UOceanRenderComponent::OceanCalculating_GameThread()
{
UWorld* world = GetWorld();
ERHIFeatureLevel::Type FeatureLevel = world->Scene->GetFeatureLevel();
checkf(FeatureLevel == ERHIFeatureLevel::SM5, TEXT("Only surpport SM5"));
if (OutputRenderTarget2D == nullptr) return;
//Using front RT to render,back buffer store last frame imformation
UKismetRenderingLibrary::ClearRenderTarget2D(world, OutputRenderTarget2D);
FTextureReferenceRHIRef OutputRenderTargetTextureRHI = OutputRenderTarget2D->TextureReference.TextureReferenceRHI;
checkf(OutputRenderTargetTextureRHI != nullptr, TEXT("Can't get render target %d texture"));
FRHITexture* RenderTargetTextureRef = OutputRenderTargetTextureRHI->GetTextureReference()->GetReferencedTexture();
TargetSize = OutputRenderTarget2D->SizeX;
//Update the uniform buffer
OceanUniformDataBuffer.OceanTime_GameThread.X = UGameplayStatics::GetRealTimeSeconds(GetWorld());
ENQUEUE_RENDER_COMMAND(OceanRenderCommand)
(
[FeatureLevel, RenderTargetTextureRef, this](FRHICommandListImmediate& RHICmdList)
{
OceanCalculating_RenderThread
(
RHICmdList,
FeatureLevel,
RenderTargetTextureRef,
this->TargetSize,
this->OceanUniformDataBuffer
);
}
);
}
#undef LOCTEXT_NAMESPACE
Ocean.usf
#include "/Engine/Private/Common.ush"
RWTexture2D OutputBufferSurface;
float4 SurfaceClearColor;
struct OceanUniform
{
float Time;
};
void InitOceanUniform(out OceanUniform uniformval)
{
uniformval.Time = OceanBasicStructData.OceanTime.x;
}
[numthreads(32, 32, 1)]
void OceanMainCS(uint3 ThreadId : SV_DispatchThreadID)
{
//Set up some variables we are going to need
//The size of outputsurface and input surface is same
float sizeX, sizeY;
OutputBufferSurface.GetDimensions(sizeX, sizeY);
OceanUniform OceanUniformVal;
InitOceanUniform(OceanUniformVal);
float2 iResolution = float2(sizeX, sizeY);
float2 UV = (ThreadId.xy / iResolution.xy) * 50.0f;
float4 Output = float4(1.0f, 1.0f, 1.0f, 1.0f);
Output.xyz = float3(sin(UV.x + OceanUniformVal.Time), 0, 0);
OutputBufferSurface[ThreadId.xy] = Output;
}
我这里做了个UniformBuffer然后拿到系统的时间,把系统的时间变量塞到我的Compute shader中。
直接把值采出来连到VertexPositionOffset上就可以把我们的ComputeShader的结果传到顶点着色器了。
在Gerstner Waves之前,先使用正玄波变形的方法模拟。对正玄波进行变形属于经验性的方法,把正弦波变形让它的形状更接近水浪。
下面先来制作正玄波水面
于是乎我们可以得到如下效果
有了基础的波形后,剩下就是让海面变化更丰富。想让海面变丰富那就是多叠几层波
Output.z = 0.5 * pow((sin(WaveLength * Speed + dot(direction, UV * 0.8) * WaveLength) + 1) / 2, 2.5f);
Output.z += 0.2 * pow((sin(WaveLength * Speed * 0.8f + dot(float2(0.8, 0.1), UV * 0.9) * WaveLength) + 1) / 2, 2.5f);
Output.z += 0.15 * sin(WaveLength * Speed * 1.2f + dot(float2(-0.8, -0.1), UV) * WaveLength * 1.3f);
Output.z += 0.1 * sin(WaveLength * Speed * 1.2f + dot(float2(0.6, -0.5), UV) * WaveLength * 1.5f);
Output.z += 0.1 * sin(WaveLength * Speed * 0.5f + dot(float2(0.5, -0.1), UV) * WaveLength * 1.5f);
Output.y = 0.5 * pow((cos(WaveLength * Speed + dot(direction, UV * 0.8) * WaveLength) + 1) / 2, 2.5f);
Output.y += 0.2 * pow((cos(WaveLength * Speed * 0.8f + dot(float2(0.8, 0.1), UV * 0.9) * WaveLength) + 1) / 2, 2.5f);
Output.y += 0.15 * cos(WaveLength * Speed * 1.2f + dot(float2(-0.8, -0.1), UV) * WaveLength * 1.3f);
Output.y += 0.1 * cos(WaveLength * Speed * 1.2f + dot(float2(0.6, -0.5), UV) * WaveLength * 1.5f);
Output.y += 0.1 * cos(WaveLength * Speed * 0.5f + dot(float2(0.5, -0.1), UV) * WaveLength * 1.5f);
Output.z = 0.5 * pow((cos(WaveLength * Speed + dot(direction, UV * 0.8) * WaveLength) + 1) / 2, 2.5f);
Output.z += 0.2 * pow((cos(WaveLength * Speed * 0.8f + dot(float2(0.8, 0.1), UV * 0.9) * WaveLength) + 1) / 2, 2.5f);
Output.z += 0.15 * cos(WaveLength * Speed * 1.2f + dot(float2(-0.8, -0.1), UV) * WaveLength * 1.3f);
Output.z += 0.1 * cos(WaveLength * Speed * 1.2f + dot(float2(0.6, -0.5), UV) * WaveLength * 1.5f);
Output.z += 0.1 * cos(WaveLength * Speed * 0.5f + dot(float2(0.5, -0.1), UV) * WaveLength * 1.5f);
OutputBufferSurface[ThreadId.xy] = Output;
可以看到正玄波水面波浪比较平,无法模拟出水波的波峰陡峭的特点。因此我们需要使用新的模拟模型:Gerstner Wave。
PositionOffset:
Normal:
Gerstner Wave是周期重力波欧拉方程的解, 其描述的是拥有无限深度且不可压缩的流体表面的波形 。
代码如下:
想要更好的效果可以优化下参数和多叠几层波,反正Computeshader里算这种东西很快的啦。
FFT海面的核心思路是我们通过一系列测量得到真实海面的波的频率然后把这些频率通过FFT变换到时域然后计算出置换贴图。下面来公式推导
设波的高度为水平方向的位置和时间的关系 。在水平方向上 根据Tessendorf J.2001的论文我们可以得到如下公式
其中 代表波正在运动的二维水平方向, 。
, 。
是水平方向的Domain Resolution。 是竖直方向的Domain Resolution
所以 的范围是:
The fft process generates the height field at discrete points
下面我做一系列化简
m和n的可以在整形范围【16,2048】之间取值
需要使用一个海洋统计学计算模型Phillips spectrum,这个模型的原始数据是多年观测的来,它是一个经验公式。
: global wave amplitude
: normalized wave direction vector
: the magnitude of
: normalized wind direction vector
: wind speed
: gravitational constant
为了具有随机性,我们使用高斯分布来获取随机波普,根据上面的公式
波高场的傅立叶振幅可以产生为:
式中,ξr和ξi是高斯随机数发生器的普通独立绘图。
给定一个离散关系 ,则当时间为 时,波场的傅里叶振幅为:
式中 项为波普参数,使用这个式子可以执行FFT来计算结果。
(1)首先我们需要一个gauss分布
按照上面的公式可以渲染得到下面一张图:
(2)然后需要用高斯分布生成一个Phillips spectrum
会得到如下效果
把它和gauss分布结合后得到如下效果:
然后现在我们有了海面的频谱图。下一步需要进行IFFT变换,但是在做变换前我们需要一个OmegaTexture做蝶形变换
(3)然后做IFFT变换生成Displacement,然后生成高度图和normal
最后把这些生成的图弄到渲染管线种作为渲染资源渲染海面即可。下面就根据这上面的步骤来生成我们的海面。
下一卷我将给出具体FFT实现。
Enjoy it。
【参考资料】
【1】Ocean Shader with Gerstner Waves
【2】音速键盘猫:Shader相册第6期 --- 实时水面模拟与渲染(一)
【3】https://labs.karmaninteractive.com/ocean-simulation-pt-1-introduction-df134a47150
【4】Ocean simulation part one: using the discrete Fourier transform
【5】Ocean simulation part two: using the fast Fourier transform
【6】https://www.slideshare.net/Codemotion/an-introduction-to-realistic-ocean-rendering-through-fft-fabio-suriano-codemotion-rome-2017
【7】海洋模擬 FFT算法實現--基於GPU的基2快速傅里葉變換 2維FFT算法實現--基於GPU的基2快速二維傅里葉變換 【pbrt】使用openFrameworks調用pbrt
【8】白霂凡:一小时学会快速傅里叶变换(Fast Fourier Transform)
【9】海面模拟以及渲染(计算着色器、FFT、Reflection Matrix)
【10】wubugui/Jerry-Tessendorf-2004
【11】http://evasion.imag.fr/~Fabrice.Neyret/images/fluids-nuages/waves/Jonathan/articlesCG/waterslides2001.pdf
【12】https://dsqiu.iteye.com/blog/1636299
【13】https://www.bilibili.com/video/av19141078?t=1053
傅里叶变换基础教程(如果对傅里叶变换完全不清楚的建议按顺序看完下面的链接)
【14】Heinrich:傅里叶分析之掐死教程(完整版)更新于2014.06.06
【15】https://www.bilibili.com/video/av34364399/?spm_id_from=333.788.videocard.0
【16】https://www.bilibili.com/video/av34556069/?spm_id_from=333.788.videocard.0
【17】https://www.bilibili.com/video/av34845617/?spm_id_from=333.788.videocard.0
【18】https://www.bilibili.com/video/av35047004/?spm_id_from=333.788.videocard.0
【19】https://www.bilibili.com/video/av35810587/?spm_id_from=333.788.videocard.0
【20】https://www.bilibili.com/video/av36343956/?spm_id_from=333.788.videocard.0