unity 随机数_【学习笔记】Unity 基于GPU FFT海洋的实现-实践篇

f4a077aa061abbabfa54172eca8b3d3d.png

本文是继【学习笔记】Unity 基于GPU FFT海洋的实现-理论篇 的实现。

这是最后得到的结果

5bff9688d67bb52e5eeeb579b42bdf83.png
https://www.zhihu.com/video/1188154326164643840

对于实现的话就比较简单了,只需要照着公式抄一遍就可以了。在本次实现中我们将使用Compute Shader在计算频谱和FFT。为了理清思路和便于debug,整个工程代码写得比较野蛮==其中很多计算是可以放到一起的,也创建了很多的RenderTexture(实际并用不了那么多),同时还使用了比较大的数据类型等。

首先我们需要先生成高斯随机数

//计算高斯随机变量
[numthreads(8, 8, 1)]
void ComputeGaussianRandom(uint3 id: SV_DispatchThreadID)
{
    float2 g = gaussian(id.xy);

    GaussianRandomRT[id.xy] = float4(g, 0, 0);
}

//计算高斯随机数
float2 gaussian(float2 id)
{
    //均匀分布随机数
    rngState = wangHash(id.y * N + id.x);
    float x1 = rand();
    float x2 = rand();

    x1 = max(1e-6f, x1);
    x2 = max(1e-6f, x2);
    //计算两个相互独立的高斯随机数
    float g1 = sqrt(-2.0f * log(x1)) * cos(2.0f * PI * x2);
    float g2 = sqrt(-2.0f * log(x1)) * sin(2.0f * PI * x2);

    return float2(g1, g2);
}
//随机种子
uint wangHash(uint seed)
{
    seed = (seed ^ 61) ^(seed >> 16);
    seed *= 9;
    seed = seed ^(seed >> 4);
    seed *= 0x27d4eb2d;
    seed = seed ^(seed >> 15);
    return seed;
}
//计算均匀分布随机数[0,1)
float rand()
{
    // Xorshift算法
    rngState ^= (rngState << 13);
    rngState ^= (rngState >> 17);
    rngState ^= (rngState << 5);
    return rngState / 4294967296.0f;;
}

我们将使用wangHash来生成随机数种子,然后使用Xorshift算法来生成均匀分布的随机数。可以参考这里Quick And Easy GPU Random Numbers In D3D11 ,然后对得到均匀分布的随机数,通过Box-Muller转换,将得到高斯随机数

是两个相互独立的均匀分布的随机数,
是两个相互独立的高斯随机数。可参考这里GPU Gems 3 :Chapter 37 。

随机数只需要计算一次就好了,然后我们在计算高度频谱

//生成高度频谱
[numthreads(8, 8, 1)]
void CreateHeightSpectrum(uint3 id: SV_DispatchThreadID)
{
    float2 k = float2(2.0f * PI * id.x / N - PI, 2.0f * PI * id.y / N - PI);

    float2 gaussian = GaussianRandomRT[id.xy].xy;

    float2 hTilde0 = gaussian * sqrt(abs(phillips(k) * DonelanBannerDirectionalSpreading(k)) / 2.0f);
    float2 hTilde0Conj = gaussian * sqrt(abs(phillips(-k) * DonelanBannerDirectionalSpreading(-k)) / 2.0f);
    hTilde0Conj.y *= -1.0f;

    float omegat = dispersion(k) * Time;
    float c = cos(omegat);
    float s = sin(omegat);
    
    float2 h1 = complexMultiply(hTilde0, float2(c, s));
    float2 h2 = complexMultiply(hTilde0Conj, float2(c, -s));

    float2 HTilde = h1 + h2;

    HeightSpectrumRT[id.xy] = float4(HTilde, 0, 0);
}

phillips谱

//计算phillips谱
float phillips(float2 k)
{
    float kLength = length(k);
    kLength = max(0.001f, kLength);
    // kLength = 1;
    float kLength2 = kLength * kLength;
    float kLength4 = kLength2 * kLength2;

    float windLength = length(WindAndSeed.xy);
    float  l = windLength * windLength / G;
    float l2 = l * l;

    float damping = 0.001f;
    float L2 = l2 * damping * damping;

    //phillips谱
    return  A * exp(-1.0f / (kLength2 * l2)) / kLength4 * exp(-kLength2 * L2);
}

Donelan-Banner方向拓展

//Donelan-Banner方向拓展
float DonelanBannerDirectionalSpreading(float2 k)
{
    float betaS;
    float omegap = 0.855f * G / length(WindAndSeed.xy);
    float ratio = dispersion(k) / omegap;

    if (ratio < 0.95f)
    {
        betaS = 2.61f * pow(ratio, 1.3f);
    }
    if(ratio >= 0.95f && ratio < 1.6f)
    {
        betaS = 2.28f * pow(ratio, -1.3f);
    }
    if(ratio > 1.6f)
    {
        float epsilon = -0.4f + 0.8393f * exp(-0.567f * log(ratio * ratio));
        betaS = pow(10, epsilon);
    }
    float theta = atan2(k.y, k.x) - atan2(WindAndSeed.y, WindAndSeed.x);

    return betaS / max(1e-7f, 2.0f * tanh(betaS * PI) * pow(cosh(betaS * theta), 2));
}
float dispersion(float2 k)
{
    return sqrt(G * length(k));
}

这里并没有什么好说的,基本就是照着公式抄==。Donelan-Banner方向拓展公式为

5b984b2486cc0564989139c476124ec2.png

图截取自Empirical Directional Wave Spectra for Computer Graphics,

角频率,
是波相对于风的角度,
是峰值频率
,
是重力加速度,
是平均风速。

得到了高度频谱,就可以使用他来计算我们的偏移频谱

//生成偏移频谱
[numthreads(8, 8, 1)]
void CreateDisplaceSpectrum(uint3 id: SV_DispatchThreadID)
{
    float2 k = float2(2 * PI * id.x / N - PI, 2 * PI * id.y / N - PI);
    k /= max(0.001f, length(k));
    float2 HTilde = HeightSpectrumRT[id.xy].xy;

    float2 KxHTilde = complexMultiply(float2(0, -k.x), HTilde);
    float2 kzHTilde = complexMultiply(float2(0, -k.y), HTilde);

    DisplaceXSpectrumRT[id.xy] = float4(KxHTilde, 0, 0);
    DisplaceZSpectrumRT[id.xy] = float4(kzHTilde, 0, 0);
}

至此就得到了我们想要的所有频谱,然后分别来对他们进行FFT就可以了

//横向FFT计算,只针对第m-1阶段,最后一阶段需要特殊处理
[numthreads(8, 8, 1)]
void FFTHorizontal(uint3 id: SV_DispatchThreadID)
{
    int2 idxs = id.xy;
    idxs.x = floor(id.x / (Ns * 2.0f)) * Ns + id.x % Ns;
    float angle = 2.0f * PI * (id.x / (Ns * 2.0f));
    float2 w = float2(cos(angle), sin(angle));

    float2 x0 = InputRT[idxs].xy;
    float2 x1 = InputRT[int2(idxs.x + N * 0.5f, idxs.y)].xy;

    float2 output = x0 + float2(w.x * x1.x - w.y * x1.y, w.x * x1.y + w.y * x1.x);
    OutputRT[id.xy] = float4(output, 0, 0);
}

这里只截取了一个,对于最后一个阶段和纵向FFT,代码大同小异,其实也可以写到一起。

当FFT计算完后,就可以生成我们的偏移纹理,这里使用了几个参数来控制他的偏移程度。

//生成偏移纹理
[numthreads(8, 8, 1)]
void TextureGenerationDisplace(uint3 id: SV_DispatchThreadID)
{
    float y = length(HeightSpectrumRT[id.xy].xy) / (N * N) * HeightScale;//高度
    float x = length(DisplaceXSpectrumRT[id.xy].xy) / (N * N) * Lambda;//x轴偏移
    float z = length(DisplaceZSpectrumRT[id.xy].xy) / (N * N) * Lambda;//z轴偏移
    
    HeightSpectrumRT[id.xy] = float4(y, y, y, 0);
    DisplaceXSpectrumRT[id.xy] = float4(x, x, x, 0);
    DisplaceZSpectrumRT[id.xy] = float4(z, z, z, 0);
    DisplaceRT[id.xy] = float4(x, y, z, 0);
}

最后根据偏移纹理,来计算法线和泡沫,计算方法就和我们上一节所讲的那样。

//生成法线和泡沫纹理
[numthreads(8, 8, 1)]
void TextureGenerationNormalBubbles(uint3 id: SV_DispatchThreadID)
{
    //计算法线
    float uintLength = OceanLength / (N - 1.0f);//两点间单位长度
    //获取当前点,周围4个点的uv坐标
    uint2 uvX1 = uint2((id.x - 1.0f + N) % N, id.y);
    uint2 uvX2 = uint2((id.x + 1.0f + N) % N, id.y);
    uint2 uvZ1 = uint2(id.x, (id.y - 1.0f + N) % N);
    uint2 uvZ2 = uint2(id.x, (id.y + 1.0f + N) % N);

    //以当前点为中心,获取周围4个点的偏移值
    float3 x1D = DisplaceRT[uvX1].xyz;//在x轴 第一个点的偏移值
    float3 x2D = DisplaceRT[uvX2].xyz;//在x轴 第二个点的偏移值
    float3 z1D = DisplaceRT[uvZ1].xyz;//在z轴 第一个点的偏移值
    float3 z2D = DisplaceRT[uvZ2].xyz;//在z轴 第二个点的偏移值

    //以当前点为原点,构建周围4个点的坐标
    float3 x1 = float3(x1D.x - uintLength, x1D.yz);//在x轴 第一个点的坐标
    float3 x2 = float3(x2D.x + uintLength, x2D.yz);//在x轴 第二个点的坐标
    float3 z1 = float3(z1D.xy, z1D.z - uintLength);//在z轴 第一个点的坐标
    float3 z2 = float3(z1D.xy, z1D.z + uintLength);//在z轴 第二个点的坐标

    //计算两个切向量
    float3 tangentX = x2 - x1;
    float3 tangentZ = z2 - z1;

    //计算法线
    float3 normal = normalize(cross(tangentZ, tangentX));


    //计算泡沫
    float3 ddx = x2D - x1D;
    float3 ddz = z2D - z1D;
    //雅可比行列式
    float jacobian = (1.0f + ddx.x) * (1.0f + ddz.z) - ddx.z * ddz.x;

    jacobian = saturate(max(0, BubblesThreshold - saturate(jacobian)) * BubblesScale);

    NormalRT[id.xy] = float4(normal, 0);
    BubblesRT[id.xy] = float4(jacobian, jacobian, jacobian, 0);
}

这样我们就有了所有的数据,接下来进行渲染就可以了。在顶点着色器根据偏移纹理 进行顶点偏移。片源着色器就进行了简单的灯光计算,如果想要更真实的物理效果可以参考这篇论文Real-time Realistic Ocean Lighting using Seamless Transitions from Geometry to BRDF

            v2f vert(appdata v)
            {
                v2f o;
                o.uv = TRANSFORM_TEX(v.uv, _Displace);
                float4 displcae = tex2Dlod(_Displace, float4(o.uv, 0, 0));
                v.vertex += float4(displcae.xyz, 0);
                o.pos = UnityObjectToClipPos(v.vertex);
                
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                fixed3 normal = UnityObjectToWorldNormal(tex2D(_Normal, i.uv).rgb);
                fixed bubbles = tex2D(_Bubbles, i.uv).r;
                
                fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
                fixed3 reflectDir = reflect(-viewDir, normal);               
                // reflectDir *= sign(reflectDir.y);
                
                //采样反射探头
                half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectDir, 0);
                half3 sky = DecodeHDR(rgbm, unity_SpecCube0_HDR);
                
                //菲涅尔
                fixed fresnel = saturate(_FresnelScale + (1 - _FresnelScale) * pow(1 - dot(normal, viewDir), 5));
                
                half facing = saturate(dot(viewDir, normal));                
                fixed3 oceanColor = lerp(_OceanColorShallow, _OceanColorDeep, facing);
                
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;
                //泡沫颜色
                fixed3 bubblesDiffuse = _BubblesColor.rbg * _LightColor0.rgb * saturate(dot(lightDir, normal));
                //海洋颜色
                fixed3 oceanDiffuse = oceanColor * _LightColor0.rgb * saturate(dot(lightDir, normal));
                fixed3 halfDir = normalize(lightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(normal, halfDir)), _Gloss);
                
                fixed3 diffuse = lerp(oceanDiffuse, bubblesDiffuse, bubbles);
                
                fixed3 col = ambient + lerp(diffuse, sky, fresnel) + specular ;
                
                return fixed4(col, 1);
            }

逻辑代码并没有粘,因为他实在是太简单了.....

源码已经上传到了Github上,如果有什么理解或错误的地方,望大佬告诉我.....

Straw1997/FFTOcean​github.com
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值