虚幻4渲染编程(环境模拟篇)【第七卷:海洋模拟-上-海洋模拟理论推导】

我的专栏目录:

小IVan:专题概述及目录​zhuanlan.zhihu.com图标

简介:

海洋这个要素在游戏里占比越来越多,开放世界的游戏要求角色能上天入地下水。目前游戏里做海洋的方法就那几种。(1)预烘焙法(2)Gerstner wave(3)FFT海洋。预烘焙法可以是实现烘焙好DisplacementMap或者是FFT的运算结果。Gerstner wave可以在GPU或者CPU上算。FFT的话就是拿海洋频率模型算出Displacement。


【1】基础环境搭建

在开始研究之前我们需要先搭建起我们的环境。我选择在ComputeShader种完成各种计算,然后在顶点着色器种直接Sample前面的ComputeShader的波形计算结果。不要把波形的计算塞到VertexShader里。把波形计算独立出来还有个好处就是我能把波形的结果储存起来拿给其它效果使用,比如制作浮力部分的时候我们就需要知道海面波形的信息,如果塞VertexShader里就拿不到这些信息了。

搭建ComputeShader的方法前面我的文章有提到,这里我就直接贴代码了。使用的引擎版本是4.21.0,如果引擎更新了新版本可能代码有一点区别。FFT部分我会给出4.22的代码。

如果不是在Unreal中实现或者不想做这么复杂,可以直接跳过这部分。

v2-4ab738bee9f2f0f9000ffab4845f0bb3_b.jpg
v2-dc8f6793cb3b3573ec7cc7995691837d_b.jpg

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<class FRHITexture2D> FTexture2DRHIRef;
typedef TRefCountPtr<class FRHIUnorderedAccessView> FUnorderedAccessViewRHIRef;
typedef TRefCountPtr<class FRHIStructuredBuffer> 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<FOceanBasicStructData>(), 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);

	TShaderMapRef<FOceeanCSShader>OceanComputeShader(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<float4> 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中。

v2-88095020cb6b0dcb4dc54a777b64f4d6_b.jpg

直接把值采出来连到VertexPositionOffset上就可以把我们的ComputeShader的结果传到顶点着色器了。


【2】Gerstner Wave

在Gerstner Waves之前,先使用正玄波变形的方法模拟。对正玄波进行变形属于经验性的方法,把正弦波变形让它的形状更接近水浪。

v2-6bf09c6030306e6c2fe96cb6a6d8a05f_b.jpg

F(x)=2(\frac{sin(x) + 1}{2})^{k}

下面先来制作正玄波水面

v2-eab451ce66d2e7623ff74d70491b97f1_b.jpg

于是乎我们可以得到如下效果

v2-de748c163f619f706230575047d8bffd_b.gif

有了基础的波形后,剩下就是让海面变化更丰富。想让海面变丰富那就是多叠几层波

v2-a64dcccdc118251d2c0f0494999dbe18_b.gif
    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。

v2-2b7f8d03ba20f38a7e97f1cffee377d1_b.gif

PositionOffset:

v2-411a49066c4f0a9ce6795db806513106_b.jpg

Normal:

v2-40f398d827736048f87be5ac4cc3e4db_b.jpg

Gerstner Wave是周期重力波欧拉方程的解, 其描述的是拥有无限深度且不可压缩的流体表面的波形 。

v2-dbb661495703ac0400f9ea6bdbee7bf4_b.gif

代码如下:

v2-6c4496f082f6e44454acf849fc2dca4e_b.jpg

想要更好的效果可以优化下参数和多叠几层波,反正Computeshader里算这种东西很快的啦。


【3】FFT海面理论推导

FFT海面的核心思路是我们通过一系列测量得到真实海面的波的频率然后把这些频率通过FFT变换到时域然后计算出置换贴图。下面来公式推导

设波的高度为水平方向的位置和时间的关系 h(X,t) 。在水平方向上 X= (x,z) 根据Tessendorf J.2001的论文我们可以得到如下公式

h(X,t)=\sum_{K}^{}{x}\tilde{h}(K,t)e^{iK\cdot X}

其中 K 代表波正在运动的二维水平方向, K = (k_{x}, K_{y})

k_{x} = 2\pi/L_{x} , k_{y} = 2\pi m/L_{y}

n 是水平方向的Domain Resolution。 m 是竖直方向的Domain Resolution

所以 K 的范围是:

K=(2\pi/L_{x},2\pi m/L_{y})

The fft process generates the height field at discrete points

X = (nL_{x}/N, mL_{z}/N)

-N/2 \leq n, m < N/2

下面我做一系列化简

0\leq n,m<N-1

n = n - N/2

m = m - N/2

m和n的可以在整形范围【16,2048】之间取值

K = (\frac{2\pi n - \pi N}{L}, \frac{2\pi m - \pi N}{L})

X=((n-\frac{N}{2})L/N,(m-\frac{N}{2})L/N)

-N/2 \leq k, l < N -1

X = (kL/N , lL/N)

\Rightarrow h(k,l,t) = \sum_{n=0}^{N-1}{\sum_{m=0}^{N-1}}\tilde{h}(n,m,t)e^{\frac{i(2\pi nk-\pi Nk)}{N}}e^{\frac{i(2\pi nl - \pi Nl)}{N}}

\Rightarrow h(k,l,t) = \sum_{n=0}^{N-1}{\sum_{m=0}^{N-1}}\tilde{h}(n,m,t)e^{\frac{i2\pi nk}{N}}e^{-i\pi k}e^{-i\pi l}

e^{-i\pi} = 1

\Rightarrow h(k,l,t) = \frac{1}{N \cdot N}\sum_{n=0}^{N-1}{[\sum_{m=0}^{N-1}}\tilde{h}(n,m,t)e^{\frac{i2\pi nk}{N}}]e^{\frac{i2\pi nk}{N}}

h_{0} 需要使用一个海洋统计学计算模型Phillips spectrum,这个模型的原始数据是多年观测的来,它是一个经验公式。

P_{h}(K)=a \frac{e^{-\frac{1}{(kL)^2}}}{k^4}\left| \tilde{K}\cdot \tilde{W} \right|^2

L = \frac{v^2}{g}

a : global wave amplitude

\tilde{K} : normalized wave direction vector

k : the magnitude of

\tilde{W} : normalized wind direction vector

v : wind speed

g : gravitational constant

为了具有随机性,我们使用高斯分布来获取随机波普,根据上面的公式

波高场的傅立叶振幅可以产生为:

\tilde{h}_{0}(K)=\frac{1}{\sqrt{2}}(\xi _{r} + i\xi _{i})\sqrt{P_{h}(K)}

式中,ξr和ξi是高斯随机数发生器的普通独立绘图。

给定一个离散关系 w_{k} ,则当时间为 t 时,波场的傅里叶振幅为:

\tilde{h(k,t)} = \tilde{h_{0}}(k)exp\left\{ iω(k)t \right\} + \tilde{h_{0}^{*}}(−k)exp\left\{  -iω(k)t \right\}

\tilde{h_{0}^{*}} = \tilde{h}(-k,t)

式中 h_{0} 项为波普参数,使用这个式子可以执行FFT来计算结果。


【4】FFT海面实现步骤

(1)首先我们需要一个gauss分布

v2-285fa4882daaa08fedfb1aaf9df3fe5e_b.png
v2-4c86e21af99ce52b1d72a21f4715ad53_b.jpg

按照上面的公式可以渲染得到下面一张图:

v2-af31311d8161c849722fd2f163dc2eeb_b.jpg

(2)然后需要用高斯分布生成一个Phillips spectrum

v2-fd3284db8fb3bcc1ee55c958e35707b5_b.jpg
v2-396a7a0ffb2d0a0f5d7357a614e95a22_b.jpg

会得到如下效果

v2-afba77cea1beb7c45028aa6cda969655_b.jpg


把它和gauss分布结合后得到如下效果:

v2-de7986523352cdd9a3bea15e493c3b0b_b.jpg

然后现在我们有了海面的频谱图。下一步需要进行IFFT变换,但是在做变换前我们需要一个OmegaTexture做蝶形变换

v2-2f50dc53c4a1ff3d97d517db9133bcab_b.jpg
v2-a9407198f16802a5ff92dcd890503d78_b.png



(3)然后做IFFT变换生成Displacement,然后生成高度图和normal

v2-f1a1ec427b83e1c33c44ae0d4bc64c56_b.jpg

最后把这些生成的图弄到渲染管线种作为渲染资源渲染海面即可。下面就根据这上面的步骤来生成我们的海面。

下一卷我将给出具体FFT实现。

Enjoy it。


Next:

YivanLee:虚幻4渲染编程(环境模拟篇)【第八卷:海洋模拟-中-在UE中实现FFT海洋】​zhuanlan.zhihu.com图标

【参考资料】

【1】Ocean Shader with Gerstner Waves

【2】音速键盘猫:Shader相册第6期 --- 实时水面模拟与渲染(一)

【3】labs.karmaninteractive.com

【4】Ocean simulation part one: using the discrete Fourier transform

【5】Ocean simulation part two: using the fast Fourier transform

【6】slideshare.net/Codemoti

【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】evasion.imag.fr/~Fabric

【12】dsqiu.iteye.com/blog/16

【13】bilibili.com/video/av19

傅里叶变换基础教程(如果对傅里叶变换完全不清楚的建议按顺序看完下面的链接)

【14】Heinrich:傅里叶分析之掐死教程(完整版)更新于2014.06.06

【15】bilibili.com/video/av34

【16】bilibili.com/video/av34

【17】bilibili.com/video/av34

【18】bilibili.com/video/av35

【19】bilibili.com/video/av35

【20】bilibili.com/video/av36

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cpongo11

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值