53

SIGGRAPH中海洋的研究学习-腾讯游戏学院

 5 years ago
source link: http://gad.qq.com/article/detail/289282
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
演示demo:



从海岛奇兵的海水一路改进过来,但总感觉还是不够好看。想来想去还是重新写一个新版海水。总体思路不再是优先考虑性能,而是先做效果,只要手机上还能支持,就先试试看。

打算先做Gerstner Wave。

数学部分知识如下:(来自https://zhuanlan.zhihu.com/p/31670275)
5c6a66925cd9d.png

实际实现的时候还是挺麻烦的。首先要自己创建一个网格,因为要做效果,这个网格的顶点数要多一点,我用的是程序动态生成,可以调整精细度。生成网格代码就不再赘述(因为又臭又长)。

波形公式有了,但具体用几个波进行叠加,怎么叠加却没有明确的说法。我查了好多资料,参考了UWA上面的一个项目https://lab.uwa4d.com/lab/5b55ee58d7f10a201fd760a9,最终决定分层随机叠加的方式。通过参数进行调节。以下代码让波长逐渐增加,同时生成了相位和角度。角度是用来控制波的方向。

   
public void GenerateWaveData(int componentsPerOctave, ref float[] wavelengths, ref float[] anglesDeg, ref float[] phases)
 { int totalComponents = NUM_OCTAVES * componentsPerOctave; if (wavelengths == null || wavelengths.Length != totalComponents) wavelengths = new float[totalComponents]; if (anglesDeg == null || anglesDeg.Length != totalComponents) anglesDeg = new float[totalComponents]; if (phases == null || phases.Length != totalComponents) phases = new float[totalComponents]; float minWavelength = Mathf.Pow(2f, SMALLEST_WL_POW_2); float invComponentsPerOctave = 1f / componentsPerOctave; for (int octave = 0; octave < NUM_OCTAVES; octave++) { for (int i = 0; i < componentsPerOctave; i++) { int index = octave * componentsPerOctave + i; float minWavelengthi = minWavelength + invComponentsPerOctave * minWavelength * i; float maxWavelengthi = Mathf.Min(minWavelengthi + invComponentsPerOctave * minWavelength, 2f * minWavelength); wavelengths[index] = Mathf.Lerp(minWavelengthi, maxWavelengthi, Random.value); float rnd; rnd = (i + Random.value) * invComponentsPerOctave; anglesDeg[index] = (2f * rnd - 1f) * _waveDirectionVariance; rnd = (i + Random.value) * invComponentsPerOctave; phases[index] = 2f * Mathf.PI * rnd; } minWavelength *= 2f; } }

光有相位和波长还不够,还需要振幅。根据一篇论文里的说法,海洋是可以根据相位和波长算出合理振幅的。论文地址如下:

https://hal.archives-ouvertes.fr/file/index/docid/307938/filename/frechot_realistic_simulation_of_ocean_surface_using_wave_spectra.pdf

我自己也没具体看,而是直接拿了结果:

    
public float GetAmplitude(float wavelength, float componentsPerOctave)
{
            float wl_pow2 = Mathf.Log(wavelength) / Mathf.Log(2f);
            wl_pow2 = Mathf.Clamp(wl_pow2, SMALLEST_WL_POW_2, SMALLEST_WL_POW_2 + NUM_OCTAVES - 1f);
            int index = (int)(wl_pow2 - SMALLEST_WL_POW_2);
            float wl_lo = Mathf.Pow(2f, Mathf.Floor(wl_pow2));
            float k_lo = 2f * Mathf.PI / wl_lo;
            float omega_lo = k_lo * ComputeWaveSpeed(wl_lo);
            float wl_hi = 2f * wl_lo;
            float k_hi = 2f * Mathf.PI / wl_hi;
            float omega_hi = k_hi * ComputeWaveSpeed(wl_hi);
            float domega = (omega_lo - omega_hi) / componentsPerOctave;
            float a_2 = 2f * Mathf.Pow(10f, _powerLog[index]) * domega;
            var a = Mathf.Sqrt(a_2);
            return a;
}

对于Gerstner Wave的处理,uwa那个项目还有一种非常神奇的做法,一般都是在顶点着色器里对多个波形叠加,通过增加顶点数来提高精度,而它直接用cb先在片段着色器里画出波形并且存到贴图中,然后再对贴图进行采样得到位置。毋庸置疑这种做法得到的波是非常平滑自然的,特别美妙。具体步骤如下:

1.创建好海面网格。可以是普通平面或者是回字形平面。后者更适合优化和平一点的视角。

2.用程序计算生成Gerstner Wave的一系列参数,传递给材质进行渲染。

3.渲染流程是通过commandbuff去做的。Gerstner Wave前面说了,是通过片段着色器去渲染,所以直接画一个四边形就行。

shader直接从UWA项目中抄录如下:

 
  //四边形uv是0-1,调整到[-0.5,0.5]之间。texelSize是生成的贴图的大小,i_res是缩放过的系数。假设是放大8倍的四边形,那么i_res就是32/size,也就是32*[-0.5,0.5], 就是[-16,16],而回字形海面刚好是4x4的格子,对应正确。再从中心进行偏移,就成功从uv转到世界坐标了(这里其实是回字形特有的算法,不必深究,只要知道是从uv得到世界坐标就好)。
    float2 LD_UVToWorld(in float2 i_uv, in float2 i_centerPos, in float i_res, in float i_texelSize)
    {
        return i_texelSize * i_res * (i_uv - 0.5) + i_centerPos;
    }
    float2 LD_0_UVToWorld(in float2 i_uv)
    {
        return LD_UVToWorld(i_uv, _LD_Pos_Scale_0.xy, _LD_Params_0.y, _LD_Params_0.x);
    }
    v2f vert( appdata_t v )
    {
        v2f o;
        o.vertex = float4(v.vertex.x, -v.vertex.y, 0., .5);
        float2 worldXZ = LD_0_UVToWorld(v.uv);
        o.worldPos_wt.xy = worldXZ;
        o.uv = v.uv;
        return o;
    }
    //GridSize是每个像素代表的长度,由外部传入
    float MinWavelengthForCurrentOrthoCamera()
    {
        return _GridSize * _TexelsPerWave;
    }
    //波速可以通过公式获得,具体参考下面链接地址
    float ComputeWaveSpeed(float wavelength, float g)
    {
        // wave speed of deep sea ocean waves: https://en.wikipedia.org/wiki/Wind_wave
        // https://en.wikipedia.org/wiki/Dispersion_(water_waves)#Wave_propagation_and_dispersion
        //float g = 9.81; float k = 2. * 3.141593 / wavelength; float cp = sqrt(g / k); return cp;
        const float one_over_2pi = 0.15915494;
        return sqrt(wavelength*g*one_over_2pi);
    }
    half4 frag (v2f i) : SV_Target
    {
        const half minWavelength = MinWavelengthForCurrentOrthoCamera();
        half3 result = (half3)0.;
        for (uint vi = 0; vi < BATCH_SIZE / 4; vi++)
        {
            [unroll]
            for (uint ei = 0; ei < 4; ei++)
            {
                if (_Wavelengths[vi][ei] == 0.)
                {
                    return half4(result, 0.);
                }
                half wt = 1;
    //按照求解公式,我们可以找到对应项,D是方向,点乘P,也就是位置,C和NowTime对应tφ,k就是频率,2π/波长,最后就是振幅A和Q,Q这里是_Chop,从外部传入,到这里,公式已经计算完毕,就可以得到最终的波形图了,存在rgbafloat的贴图中
                half C = ComputeWaveSpeed(_Wavelengths[vi][ei], _Gravity * _GravityScales[vi][ei]);
                half2 D = half2(cos(_Angles[vi][ei]), sin(_Angles[vi][ei]));
                half k = TWOPI / _Wavelengths[vi][ei];
                half x = dot(D, i.worldPos_wt.xy);
                half3 result_i = wt * _Amplitudes[vi][ei];
                result_i.y *= cos(k*(x + C * NowTime) + _Phases[vi][ei]);
                result_i.xz *= -_Chop * _ChopScales[vi][ei] * D * sin(k*(x + C * NowTime) + _Phases[vi][ei]);
                result += result_i;
            }
            }
            return half4(i.worldPos_wt.z * result, 0.);
    }

拿到这张波形图之后,就可以对我们前面生成的网格进行扰动了。在这之前,UWA项目里面对回字形的两层LOD进行了混合叠加处理,用来使过度更加自然。代码如下:

 
  //这里采样两次,也是回字形造成的
    void SampleDisplacements(in sampler2D i_dispSampler, in float2 i_uv, in float i_wt, inout float3 io_worldPos)
    {
        const half3 disp = tex2Dlod(i_dispSampler, float4(i_uv, 0., 0.)).xyz;
        io_worldPos += i_wt * disp;
    }
    half4 frag (v2f i) : SV_Target
    {
        const float2 worldPosXZ = LD_0_UVToWorld(i.uv);
        // sample the shape 1 texture at this world pos
        const float2 uv_1 = LD_1_WorldToUV(worldPosXZ);
        float3 result = 0.;
            SampleDisplacements(_LD_Sampler_AnimatedWaves_0, i.uv, 1.0, result);
        // waves to combine down from the next lod up the chain
        SampleDisplacements(_LD_Sampler_AnimatedWaves_1, uv_1, 1.0, result);
        return half4(result, 1.);
   }
好了,现在终于可以进入顶点着色器看怎么进行波形扰动了。

    
      
 void OnWillRenderObject()
{
            Camera.current.depthTextureMode |= DepthTextureMode.Depth;
            // per instance data
            if (_mpb == null)
            {
                _mpb = new MaterialPropertyBlock();
            }
            _rend.GetPropertyBlock(_mpb);
            float meshScaleLerp = 0f;
            float farNormalsWeight = 1f;
            _mpb.SetVector("_InstanceData", new Vector4(meshScaleLerp, farNormalsWeight, _lodIndex));
            //每个小格子的长度
            float squareSize = Mathf.Pow(2f, Mathf.Round(Mathf.Log(transform.lossyScale.x) / Mathf.Log(2f))) / _baseVertDensity;
            float mul = 1.875f; // fudge 1
            float pow = 1.4f; // fudge 2
            float normalScrollSpeed0 = Mathf.Pow(Mathf.Log(1f + 2f * squareSize) * mul, pow);
            float normalScrollSpeed1 = Mathf.Pow(Mathf.Log(1f + 4f * squareSize) * mul, pow);
            _mpb.SetVector("_GeomData", new Vector3(squareSize, normalScrollSpeed0, normalScrollSpeed1));
            // assign lod data to ocean shader
            var ldaws = Ocean.Instance._lodDataAnimWaves;
            ldaws.BindResultData(_lodIndex, 0, _mpb);
            if (_lodIndex + 1 < Ocean.Instance.CurrentLodCount)
            {
                ldaws.BindResultData(_lodIndex + 1, 1, _mpb);
            }
            _mpb.SetTexture(_reflectionTexId, Texture2D.blackTexture);
            _rend.SetPropertyBlock(_mpb);
 }
public void SetInstanceData(int lodIndex, int totalLodCount, float baseVertDensity)
{
            _lodIndex = lodIndex; _totalLodCount = totalLodCount; _baseVertDensity = baseVertDensity;
}

着色器代码如下:

  
 //这里有一个小技巧,就是以最小格子为单位进行移动,因为在顶点数有限的情况下,如果顶点移动不是跳跃式,那么中间插值会导致轻微闪烁。这里采用的是2倍最小格子,原因是三角形分布式2x2对称的,保持稳定性。后面是因为回字形缩放,要让边缘部分逐渐放大到两倍,和下一个lod完美对齐
    float ComputeLodAlpha(float3 i_worldPos, float i_meshScaleAlpha)
    {
        float2 offsetFromCenter = float2(abs(i_worldPos.x - _OceanCenterPosWorld.x), abs(i_worldPos.z - _OceanCenterPosWorld.z));
        float taxicab_norm = max(offsetFromCenter.x, offsetFromCenter.y);
        float lodAlpha = taxicab_norm / _LD_Pos_Scale_0.z - 1.0;
        const float BLACK_POINT = 0.15, WHITE_POINT = 0.85;
        lodAlpha = max((lodAlpha - BLACK_POINT) / (WHITE_POINT - BLACK_POINT), 0.);
        lodAlpha = min(lodAlpha, 1.);
        return lodAlpha;
    }
    void SnapAndTransitionVertLayout(float i_meshScaleAlpha, inout float3 io_worldPos, out float o_lodAlpha)
    {
        const float SQUARE_SIZE_2 = 2.0*_GeomData.x, SQUARE_SIZE_4 = 4.0*_GeomData.x;
        io_worldPos.xz -= frac(unity_ObjectToWorld._m03_m23 / SQUARE_SIZE_2) * SQUARE_SIZE_2;
        o_lodAlpha = ComputeLodAlpha(io_worldPos, i_meshScaleAlpha);
        float2 m = frac(io_worldPos.xz / SQUARE_SIZE_4); // this always returns positive
        float2 offset = m - 0.5;               
        const float minRadius = 0.26;
        if (abs(offset.x) < minRadius) io_worldPos.x += offset.x * o_lodAlpha * SQUARE_SIZE_4;
        if (abs(offset.y) < minRadius) io_worldPos.z += offset.y * o_lodAlpha * SQUARE_SIZE_4;
    }
    //采样偏移之后,还需要计算法线,通过xz两个方向,分别进行采样,相减再叉乘,就会得到法线,可以画图求解
    void SampleDisplacementsNormals(in sampler2D i_dispSampler, in float2 i_uv, in float i_wt, in float i_invRes, in float i_texelSize, inout float3 io_worldPos, inout half2 io_nxz)
    {
        const float4 uv = float4(i_uv, 0., 0.);
        const half3 disp = tex2Dlod(i_dispSampler, uv).xyz;
        io_worldPos += i_wt * disp;
        float3 n;
            {
            float3 dd = float3(i_invRes, 0.0, i_texelSize);
            half3 disp_x = dd.zyy + tex2Dlod(i_dispSampler, uv + dd.xyyy).xyz;
            half3 disp_z = dd.yyz + tex2Dlod(i_dispSampler, uv + dd.yxyy).xyz;
            n = normalize(cross(disp_z - disp, disp_x - disp));
        }
        io_nxz += i_wt * n.xz;
    }
    v2f vert( appdata_t v )
    {
        v2f o;
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);
        float lodAlpha;
        SnapAndTransitionVertLayout(_InstanceData.x, o.worldPos, lodAlpha);
        o.lodAlpha_worldXZUndisplaced_oceanDepth.x = lodAlpha;
        o.lodAlpha_worldXZUndisplaced_oceanDepth.yz = o.worldPos.xz;
        o.n_shadow = half4(0., 0., 0., 0.);
        o.foam_screenPos.x = 0.;
        o.lodAlpha_worldXZUndisplaced_oceanDepth.w = 0.;
        //根据权重,可以对两个lod分别采样混合
        float wt_0 = (1. - lodAlpha) * _LD_Params_0.z;
        float wt_1 = (1. - wt_0) * _LD_Params_1.z;
        // sample displacement textures, add results to current world pos / normal / foam
        const float2 worldXZBefore = o.worldPos.xz;
        if (wt_0 > 0.001)
        {
            const float2 uv_0 = LD_0_WorldToUV(worldXZBefore);
            SampleDisplacementsNormals(_LD_Sampler_AnimatedWaves_0, uv_0, wt_0, _LD_Params_0.w, _LD_Params_0.x, o.worldPos, o.n_shadow.xy);
            }
        if (wt_1 > 0.001)
        {
            const float2 uv_1 = LD_1_WorldToUV(worldXZBefore);
            SampleDisplacementsNormals(_LD_Sampler_AnimatedWaves_1, uv_1, wt_1, _LD_Params_1.w, _LD_Params_1.x, o.worldPos, o.n_shadow.xy);
        }
        // convert height above -1000m to depth below surface
        o.lodAlpha_worldXZUndisplaced_oceanDepth.w = DEPTH_BASELINE - o.lodAlpha_worldXZUndisplaced_oceanDepth.w;
        // foam can saturate
        o.foam_screenPos.x = saturate(o.foam_screenPos.x);
        // view-projection
        o.vertex = mul(UNITY_MATRIX_VP, float4(o.worldPos, 1.));
        UNITY_TRANSFER_FOG(o, o.vertex);           
        return o;
    }

做完以上步骤后,波形效果就出来了。
5c6a57fbdae24.png

 做完波形扰动后,就是要考虑开始着色,首先还是法线图一张,结合本身的法线进行基本的颜色显示。

  
 //法线贴图采样,uv通过两个魔数进行滚动。为了保证连续性,直接对下一个lod也进行一样的采样,但nstretch要翻倍,因为lod翻倍了,采样完毕后,把法线的值返回
    half2 SampleNormalMaps(float2 worldXZUndisplaced, float lodAlpha)
    {
        const float2 v0 = float2(0.94, 0.34), v1 = float2(-0.85, -0.53);
        const float geomSquareSize = _GeomData.x;
        float nstretch = _NormalsScale * geomSquareSize; // normals scaled with geometry
        const float spdmulL = _GeomData.y;
        half2 norm =
        UnpackNormal(tex2D(_Normals, (v0*NowTime*spdmulL + worldXZUndisplaced) / nstretch)).xy +
        UnpackNormal(tex2D(_Normals, (v1*NowTime*spdmulL + worldXZUndisplaced) / nstretch)).xy;
        // blend in next higher scale of normals to obtain continuity
        const float farNormalsWeight = _InstanceData.y;
        const half nblend = lodAlpha * farNormalsWeight;
        if (nblend > 0.001)
        {
            // next lod level
            nstretch *= 2.;
            const float spdmulH = _GeomData.z;
            norm = lerp(norm,
                UnpackNormal(tex2D(_Normals, (v0*NowTime*spdmulH + worldXZUndisplaced) / nstretch)).xy +
                UnpackNormal(tex2D(_Normals, (v1*NowTime*spdmulH + worldXZUndisplaced) / nstretch)).xy,
                nblend);
        }
        // approximate combine of normals. would be better if normals applied in local frame.
        return _NormalsStrength * norm;
    }
    //拿到法线后,和原始法线进行叠加混合
    float pixelZ = LinearEyeDepth(i.vertex.z);
    half3 screenPos = i.foam_screenPos.yzw;
    half2 uvDepth = screenPos.xy / screenPos.z;
    float sceneZ01 = tex2D(_CameraDepthTexture, uvDepth).x;
    float sceneZ = LinearEyeDepth(sceneZ01);
    float3 lightDir = WorldSpaceLightDir(i.worldPos);
    // Soft shadow, hard shadow
    fixed2 shadow = (fixed2)1.0;
    // Normal - geom + normal mapping
    half3 n_geom = normalize(half3(i.n_shadow.x, 1., i.n_shadow.y));
    if (underwater) n_geom = -n_geom;
    half3 n_pixel = n_geom;
    n_pixel.xz += (underwater ? -1. : 1.) * SampleNormalMaps(i.lodAlpha_worldXZUndisplaced_oceanDepth.yz, i.lodAlpha_worldXZUndisplaced_oceanDepth.x);
    n_pixel = normalize(n_pixel);
    half3 OceanEmission(in const half3 i_view, in const half3 i_n_pixel, in const float3 i_lightDir,in const half4 i_grabPos, in const float i_pixelZ, in const half2 i_uvDepth, in const float i_sceneZ, in const float i_sceneZ01,in const half3 i_bubbleCol, in sampler2D i_normals, in sampler2D i_cameraDepths, in const bool i_underwater, in const half3 i_scatterCol)
    {
        half3 col = i_scatterCol;
        // underwater bubbles reflect in light
        col += i_bubbleCol;
        return col;
    }
    
    
    先把天空盒的光线算进去,根据视线和法线,可以算出反射光线,采样天空盒,在用菲尼尔处理一下,菲尼尔用的是schlick 近似公式https://en.wikipedia.org/wiki/Schlick%27s_approximation
    
  
 void ApplyReflectionSky(half3 view, half3 n_pixel, half3 lightDir, half shadow, half4 i_screenPos, inout half3 col)
    {
        // Reflection
        half3 refl = reflect(-view, n_pixel);
        half3 skyColour;
        skyColour = texCUBE(_Skybox, refl).rgb;
        // Fresnel
        const float IOR_AIR = 1.0;
        const float IOR_WATER = 1.33;
        // reflectance at facing angle
        float R_0 = (IOR_AIR - IOR_WATER) / (IOR_AIR + IOR_WATER); R_0 *= R_0;
        // schlick's approximation
        float R_theta = R_0 + (1.0 - R_0) * pow(1.0 - max(dot(n_pixel, view), 0.), _FresnelPower);
        col = lerp(col, skyColour, R_theta);
    }
完成以上着色部分后,海水看上去是这样:
5c6a582e2d9d2.png

接下来,我们要看准方向加一个平行光,让海面亮起来

    高光用的是传统Phong模型就可以达到效果。
 
   skyColour += pow(max(0., dot(refl, lightDir)), _DirectionalLightFallOff) * _DirectionalLightBoost * _LightColor0 * shadow;
5c6a584a623cb.png


然而还是非常丑,主要还是因为光照太过简单,而海必须要考虑的就是散射。本来SSS也是一个大命题,可以看好几本书,所幸的是对于海来说,用近似次表面散射也可以得到好的效果,基础原理就是越看向太阳,就越亮

  
 #if _SUBSURFACESCATTERING_ON
        {
    // light
    // use the constant term (0th order) of SH stuff - this is the average. it seems to give the right kind of colour
            col *= half3(unity_SHAr.w, unity_SHAg.w, unity_SHAb.w);
    // Approximate subsurface scattering - add light when surface faces viewer. Use geometry normal - don't need high freqs.
            half towardsSun = pow(max(0., dot(i_lightDir, -i_view)), _SubSurfaceSunFallOff);
            col += (_SubSurfaceBase + _SubSurfaceSun * towardsSun) * _SubSurfaceColour.rgb * _LightColor0 * shadow;
        }
    #endif // _SUBSURFACESCATTERING_ON
5c6a5867a3342.png



一下子就好看不少,更加通透,有一种散射的小感觉了。不过这样还远远不够,我们放入一个地形,就会发现地形和海衔接的部分还完全没有考虑。

波形需要处理,随着地形的阻挡,波应该要逐步减弱,这个可以通过深度计算去处理。

在合适的位置放一个摄像机,往地形拍摄,将深度写入图中。

 
  v2f vert( appdata_t v )
    {
        v2f o;
        o.vertex = UnityObjectToClipPos( v.vertex );
        float altitude = mul(unity_ObjectToWorld, v.vertex).y;
        o.depth = altitude - (_OceanCenterPosWorld.y - depthMax);
        return o;
    }
    float frag (v2f i) : SV_Target
    {
        return i.depth;
    }

生成这张图后,我们要回到波形生成的地方,根据深度值,重新调整波的振幅。

   
拿到深度,并且把depth还原成离水面的距离,如果depth很小,那么波长就要变小
    const half depth = depthMax - tex2D(_LD_Sampler_SeaFloorDepth_0, i.uv).x;
    half wt = 1;
    half depth_wt = saturate(depth / (0.5 * _Wavelengths[vi][ei]));
    wt *= .1 + .9 * depth_wt;
这样子处理之后,岸边的波浪就小下去了。

5c6a58a465e6b.png


 边缘硬切很难看,首先要处理透明问题,透明的基本原则是深度越浅越透明,深度越深越不透明。

  
 const half2 uvBackground = i_grabPos.xy / i_grabPos.w;
    //根据法线方向折射处理
    half2 uvBackgroundRefract = uvBackground + _RefractionStrength * i_n_pixel.xz;
    half3 sceneColour;
    half3 alpha = 0.;
    float depthFogDistance;
    //从深度贴图获得深度,并和顶点的深度作比较,如果顶点深度大于背景深度,那么就把距离算出来
    否则说明水在物体下面
    const half2 uvDepthRefract = i_uvDepth + _RefractionStrength * i_n_pixel.xz;
    const float sceneZRefract = LinearEyeDepth(tex2D(i_cameraDepths, uvDepthRefract).x);
    // Compute depth fog alpha based on refracted position if it landed on an underwater surface, or on unrefracted depth otherwise
    if (sceneZRefract > i_pixelZ)
    {
        depthFogDistance = sceneZRefract - i_pixelZ;
    }
    else
    {
        depthFogDistance = i_sceneZ - i_pixelZ;
        uvBackgroundRefract = uvBackground;
    }
    sceneColour = tex2D(_BackgroundTexture, uvBackgroundRefract).rgb;
    //对透明度根据距离进行处理
    alpha = 1. - exp(-_DepthFogDensity.xyz * depthFogDistance);
    // blend from water colour to the scene colour
    col = lerp(sceneColour, col, alpha);

5c6a590127aff.png

透明和扰动都有了,但是边缘部分的切边还是很明显。一般这种时候就需要泡沫来帮忙了。 以前我曾经用两层泡沫图叠加的方式去做,效果一般般。而且根据深度去产生泡沫也并不正确,在海浪的波峰也是有可能产生泡沫的,泡沫产生的原因主要是因为运动撕裂程度大。在海洋统计学里可以用雅克比行列式(完全看不懂)去做,这里也可以模仿。

   
//这是大猫知乎上关于雅克比行列式求解过程
    for (int i = 0; i < resolution; i++)
    {
        for (int j = 0; j < resolution; j++)
        {
            int index = i * resolution + j;
            Vector2 dDdx = Vector2.zero;
            Vector2 dDdy = Vector2.zero;
                    //ddx就是将改点的偏移减去x轴一个像素的偏移,ddy对应y轴
                    if (i != resolution - 1)
                {
                dDdx = 0.5f * (hds[index] - hds[index + resolution]);
            }
            if (j != resolution - 1)
            {
                dDdy = 0.5f * (hds[index] - hds[index + 1]);
            }
                    //这是行列式的值,后面应该是调整的值
            float jacobian = (1 + dDdx.x) * (1 + dDdy.y) - dDdx.y * dDdy.x;
            Vector2 noise = new Vector2(Mathf.Abs(normals[index].x), Mathf.Abs(normals[index].z)) * 0.3f;
            float turb = Mathf.Max(1f - jacobian + noise.magnitude, 0f);
            float xx = 1f + 3f * Mathf.SmoothStep(1.2f, 1.8f, turb);
            xx = Mathf.Min(turb, 1.0f);
            xx = Mathf.SmoothStep(0f, 1f, turb);
            colors[index] = new Color(xx, xx, xx, xx);
        }
    }
    half frag(v2f i) : SV_Target
    {
        float4 uv = float4(i.uv_uv_lastframe.xy, 0., 0.);
        float4 uv_lastframe = float4(i.uv_uv_lastframe.zw, 0., 0.);
        // #if _FLOW_ON
        half4 velocity = half4(tex2Dlod(_LD_Sampler_Flow_1, uv).xy, 0., 0.);
        half foam = tex2Dlod(_LD_Sampler_Foam_0, uv_lastframe
                    - ((_SimDeltaTime * _LD_Params_0.w) * velocity)
                    ).x;
        half2 r = abs(uv_lastframe.xy - 0.5);
        if (max(r.x, r.y) > 0.5 - _LD_Params_0.w)
        {
            // no border wrap mode for RTs in unity it seems, so make any off-texture reads 0 manually
            foam = 0.;
        }
        // fade
        foam *= max(0.0, 1.0 - _FoamFadeRate * _SimDeltaTime);
        // sample displacement texture and generate foam from it
        const float3 dd = float3(_LD_Params_1.w, 0.0, _LD_Params_1.x);
        half3 s = tex2Dlod(_LD_Sampler_AnimatedWaves_1, uv).xyz;
        half3 sx = tex2Dlod(_LD_Sampler_AnimatedWaves_1, uv + dd.xyyy).xyz;
        half3 sz = tex2Dlod(_LD_Sampler_AnimatedWaves_1, uv + dd.yxyy).xyz;
        float3 disp = s.xyz;
        float3 disp_x = dd.zyy + sx.xyz;
        float3 disp_z = dd.yyz + sz.xyz;
        // The determinant of the displacement Jacobian is a good measure for turbulence:
        // > 1: Stretch
        // < 1: Squash
        // < 0: Overlap
            //把两边偏移相减,这里是直接算行列式,没有+1的操作,算出foam后,还要根据深度去加强foam
        float4 du = float4(disp_x.xz, disp_z.xz) - disp.xzxz;
        float det = (du.x * du.w - du.y * du.z) / (_LD_Params_1.x * _LD_Params_1.x);
        foam += 5. * _SimDeltaTime * _WaveFoamStrength * saturate(_WaveFoamCoverage - det);
        // add foam in shallow water. use the displaced position to ensure we add foam where world objects are.
        float4 uv_1_displaced = float4(LD_1_WorldToUV(i.worldXZ + disp.xz), 0., 1.);
        float signedOceanDepth = depthMax - tex2Dlod(_LD_Sampler_SeaFloorDepth_1, uv_1_displaced).x + disp.y;
        foam += _ShorelineFoamStrength * _SimDeltaTime * saturate(1. - signedOceanDepth / _ShorelineFoamMaxDepth);
        return foam;
    }

拿到生成的foam贴图后就可以开始渲染。按照ppt里的说法,泡沫分成两层,顶部是白色泡沫,下面是褪色的海浪。具体的数学公式我没有查到,非常遗憾,只能有一个大概解释。

   
void SampleFoam(in sampler2D i_oceanFoamSampler, float2 i_uv, in float i_wt, inout half io_foam)
    {
        io_foam += i_wt * tex2Dlod(i_oceanFoamSampler, float4(i_uv, 0., 0.)).x;
    }
    half WhiteFoamTexture(half i_foam, float2 i_worldXZUndisplaced)
    {
    //这里负责白色泡沫
        half ft = lerp(
            tex2D(_FoamTexture, (1.25*i_worldXZUndisplaced + NowTime / 10.) / _FoamScale).r,
            tex2D(_FoamTexture, (3.00*i_worldXZUndisplaced - NowTime / 10.) / _FoamScale).r,
            0.5);
        // black point fade
        i_foam = saturate(1. - i_foam);
        return smoothstep(i_foam, i_foam + _WaveFoamFeather, ft);
    }
    void ComputeFoam(half i_foam, float2 i_worldXZUndisplaced, float2 i_worldXZ, half3 i_n, float i_pixelZ, float i_sceneZ, half3 i_view, float3 i_lightDir, half i_shadow, out half3 o_bubbleCol, out half4 o_whiteFoamCol)
    {
        half foamAmount = i_foam;
        //海岸线衰减
        foamAmount *= saturate((i_sceneZ - i_pixelZ) / _ShorelineFoamMinDepth);
        // Additive underwater foam - use same foam texture but add mip bias to blur for free
            //这里进行了偏移,类似于模糊处理
        float2 foamUVBubbles = (lerp(i_worldXZUndisplaced, i_worldXZ, 0.05) + 0.5 * NowTime * _WindDirXZ) / _FoamScale + 0.125 * i_n.xz;
        half bubbleFoamTexValue = tex2Dlod(_FoamTexture, float4(.74 * foamUVBubbles - _FoamBubbleParallax * i_view.xz / i_view.y, 0., 5.)).r;
        o_bubbleCol = (half3)bubbleFoamTexValue * _FoamBubbleColor.rgb * saturate(i_foam * _WaveFoamBubblesCoverage) * AmbientLight();
        // White foam on top, with black-point fading
        half whiteFoam = WhiteFoamTexture(foamAmount, i_worldXZUndisplaced);
        o_whiteFoamCol.rgb = _FoamWhiteColor.rgb * (AmbientLight() +    _WaveFoamLightScale * _LightColor0 * i_shadow);
        o_whiteFoamCol.a = _FoamWhiteColor.a * whiteFoam;
    }
这样处理后,浪花效果还不错。
5c6a58e87dae4.png

但是边缘效果依然丑陋,主要是海岸线和海洋中心差别还是挺大的。想了下还是希望走类似于下图这样的波浪。

5c6a59428ad2e.png

 这里我做了简化处理,没有做多层,只做了简单的一层。

5c6a5954de1d8.png

 然后开始继续优化效果,首先是散射的问题,在视角增高后,海水看起来很暗,其实是因为没有正确散射引起的,视角增高的时候,会增强散射效果。

col += pow(saturate(0.5 + 2.0 * waveHeight / _SubSurfaceHeightMax), _SubSurfaceHeightPower) * _SubSurfaceCrestColour.rgb;


这还不够,浅水的地方海的散射会更强。

 
  void SampleSeaFloorHeightAboveBaseline(in sampler2D i_oceanDepthSampler, float2 i_uv, in float i_wt, inout half io_oceanDepth)
    {
        io_oceanDepth += i_wt * (tex2Dlod(i_oceanDepthSampler, float4(i_uv, 0., 0.)).x);
    }
    #if _SUBSURFACESHALLOWCOLOUR_ON
        float shallowness = pow(1. - saturate(depth / _SubSurfaceDepthMax), _SubSurfaceDepthPower);
        half3 shallowCol = _SubSurfaceShallowCol;
        col = lerp(col, shallowCol, shallowness);
    #endif
5c6a592e5ebe1.png
 最后是焦散

  
 void ApplyCaustics(in const half3 i_view, in const half3 i_lightDir, in const float i_sceneZ, in sampler2D i_normals, inout half3 io_sceneColour)
    {
        // could sample from the screen space shadow texture to attenuate this..
        // underwater caustics - dedicated to P
        float3 camForward = mul((float3x3)unity_CameraToWorld, float3(0., 0., 1.));
        float3 scenePos = _WorldSpaceCameraPos - i_view * i_sceneZ / dot(camForward, -i_view);
        const float2 scenePosUV = LD_1_WorldToUV(scenePos.xz);
        half3 disp = 0.;
        // this gives height at displaced position, not exactly at query position.. but it helps. i cant pass this from vert shader
        // because i dont know it at scene pos.
                                        SampleDisplacements(_LD_Sampler_AnimatedWaves_1, scenePosUV, 1.0, disp);
        half waterHeight = _OceanCenterPosWorld.y + disp.y;
        half sceneDepth = waterHeight - scenePos.y;
        half bias = abs(sceneDepth - _CausticsFocalDepth) / _CausticsDepthOfField;
        // project along light dir, but multiply by a fudge factor reduce the angle bit - compensates for fact that in real life
        // caustics come from many directions and don't exhibit such a strong directonality
        float2 surfacePosXZ = scenePos.xz + i_lightDir.xz * sceneDepth / (4.*i_lightDir.y);
        half2 causticN = _CausticsDistortionStrength * UnpackNormal(tex2D(i_normals, surfacePosXZ / _CausticsDistortionScale)).xy;
        half4 cuv1 = half4((surfacePosXZ / _CausticsTextureScale + 1.3 *causticN + half2(0.044*NowTime + 17.16, -0.169*NowTime)), 0., bias);
        half4 cuv2 = half4((1.37*surfacePosXZ / _CausticsTextureScale + 1.77*causticN + half2(0.248*NowTime, 0.117*NowTime)), 0., bias);
        half causticsStrength = _CausticsStrength;
        io_sceneColour *= 1. + causticsStrength *
            (0.5*tex2Dbias(_CausticsTexture, cuv1).x + 0.5*tex2Dbias(_CausticsTexture, cuv2).x - _CausticsTextureAverage);
    }
5c6a5974b3867.png

全部效果叠加有点闪烁,自己简化了代码,没有严格按照文档的做法,所以我自己修改了边界条件,修复了这个问题。其次,由于没有缩放考虑,摄像机拉高的时候海面有很多噪点,我通过线性减少扰动和浅滩散射来处理。暂时就处理到这里。

 

总结一下,完整的演讲中的海水远远比我这个复杂,而且即便是实现其中的这么一小部分,我也有大量的细节没有理解清楚,或者没有找到对应的公式。再自己复原效果的过程中,大量简化了一些实现,勉强达到了可以看的效果,不过由于为了让每个参数明显,海面看上去稍显油腻或者说卡通了一点。在手机上跑几乎是不可能了,也难以简化到那个程度。等我再补补数学,再来继续搞这个海水吧。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK