zoukankan      html  css  js  c++  java
  • 如何做一个水面流动效果

    本文参考自教程,并加上自己的一些心得体会。所谓的水面流动,就是对给定的纹理贴图进行采样,使其随着时间流动,呈现有规则地沿着某一方向连续,但是又不完全有规律的效果。

    首先,我们用到的贴图是一张高度图,ag通道分别代表高度在x和y方向上的导数,b通道保存真实的高度数据。先让我们直接对贴图进行采样,我们用高度数据作为最后的颜色输出:

        		float3 UnpackDerivativeHeight (float4 textureData) 
                {
                    float3 dh = textureData.agb;
                    dh.xy = 2 * dh.xy - 1;
                    return dh;
                }
    
                float3 dh = UnpackDerivativeHeight(tex2D(_MainTex, i.uv));
                float4 texColor = dh.z * dh.z * _Color;
                float3 tangentNormal = normalize(float3(-dh.x, -dh.y, 1));
    

    接下来,我们希望它能沿着某一个方向动起来,为了实现这一效果,需要旋转整张贴图的uv,例如我们尝试着旋转45度:

                float2 RotateUV(float2 uv, float2 rotVec)
                {
                    rotVec = normalize(rotVec);
                    float2x2 mat = float2x2(rotVec.y, -rotVec.x, rotVec.x, rotVec.y);
                    return mul(mat, uv);
                }
    

    这里我们传入的参数并非是一个旋转的角度,而是一个向量,但实际上两者是等价的。已知旋转矩阵为:

    [egin{bmatrix} cos heta & -sin heta \ sin heta & cos heta end{bmatrix} ]

    又知道(sin^2 heta + cos^2 heta = 1),那么我们可以用一个归一化的向量(vec{v} = (sin heta, cos heta))来表示旋转。效果如下:

    接下来我们让它动起来,设置旋转向量为一个随时间变化的向量:

    			float2 uv = RotateUV(i.uv, float2(cos(_Time.y), sin(_Time.y)));
    

    仔细观察,会发现高光的效果不太对,高光并不是跟随着纹理旋转而旋转的,原因是我们在旋转高度贴图时,没有相应地对贴图描述的法线也进行旋转,那么我们把计算得到的旋转矩阵暴露出来:

                float2 RotateUV(float2 uv, float2 rotVec, out float2x2 mat)
                {
                    rotVec = normalize(rotVec);
                    mat = float2x2(rotVec.y, -rotVec.x, rotVec.x, rotVec.y);
                    return mul(mat, uv);
                }
                dh.xy = mul(rotMat, dh.xy);
    

    我们希望把旋转向量记录在一张单独的贴图中,这样就可以根据uv采样得到不同的旋转向量,然后让uv随着时间不断位移,从而获得沿着某个方向移动的效果。甚至,我们可以利用这张贴图附加一个速度信息,控制uv随着时间位移的速度:

    			   float3 flowVec = tex2D(_FlowMap, i.uv).rgb;
                    flowVec.xy = 2 * flowVec.xy - 1;
    			   flowVec.z *= _FlowStrength;
                    float2 uv = RotateUV(i.uv, flowVec.xy, rotMat);
                    uv.y = uv.y - flowVec.z * _Time.y;
    

    不过结果却是各种噪点在闪烁,令人失望。仔细分析下原因,是我们对flow map的采样太精细了,每个点都对应不同的旋转向量,导致这种不连续的效果。为了解决这个问题,首先我们将整个map进行分块处理,每块对应一个旋转向量:

                    float2 flowUV = floor(i.uv * _GridResolution) / _GridResolution;
                    float3 flowVec = tex2D(_FlowMap, flowUV).rgb;
    

    然后,让我们对块与块之间进行融合:

    			float3 FlowCell(float2 uv, float2 offset)
                {
                    float2x2 rotMat;
                    float2 flowUV = floor(uv * _GridResolution + offset) / _GridResolution;
                    float3 flowVec = tex2D(_FlowMap, flowUV).rgb;
                    flowVec.xy = 2 * flowVec.xy - 1;
    			   flowVec.z *= _FlowStrength;
                    float2 mainUV = RotateUV(uv, flowVec.xy, rotMat);
                    mainUV.y = mainUV.y - flowVec.z * _Time.y;
    
                    float3 dh = UnpackDerivativeHeight(tex2D(_MainTex, mainUV));
                    dh.xy = mul(rotMat, dh.xy);
    
                    return dh;
                }
    
    			float3 dhA = FlowCell(i.uv, float2(0, 0));
                 float3 dhB = FlowCell(i.uv, float2(1, 0));
                 float wA = frac(i.uv.x * _GridResolution);
                 float wB = 1 - wA;
                 float3 dh = dhA * wA + dhB * wB;
    
    

    虽然整体看上去看上去平滑了很多,但是块和块之间的间隙依旧十分明显。这些间隙的形成是由于不同块采样的flow map位置存在跳变,而发生跳变时融合的权重wA和wB总有一个是1。为了消除间隙,我们希望flow map采样发生跳变时,对应的融合权重为0。对于wA来说,我们希望当采样点到达块中央时,wA到达最大值1,而在边缘部分,则减小到最小值0。对于wB我们希望也满足这样的条件,但是这样就不满足融合的效果,因此我们需要将第二次采样的块偏移0.5个单位而不是1个单位:

     float3 FlowCell(float2 uv, float2 offset)
     {
     	offset *= 0.5;
     	...
     }
     
     float wB = abs(2 * frac(i.uv.x * _GridResolution) - 1);
     float wA = 1 - wB;
    

    可以看到,u方向的间隙已经消失了,如法炮制,再消除掉y方向上的间隙:

    float3 dhA = FlowCell(i.uv, float2(0, 0));
    float3 dhB = FlowCell(i.uv, float2(1, 0));
    float3 dhC = FlowCell(i.uv, float2(0, 1));
    float3 dhD = FlowCell(i.uv, float2(1, 1));
    
    float2 t = abs(2 * frac(i.uv * _GridResolution) - 1);
    float wA = (1 - t.x) * (1 - t.y);
    float wB = t.x * (1 - t.y);
    float wC = (1 - t.x) * t.y;
    float wD = t.x * t.y;
    float3 dh = dhA * wA + dhB * wB + dhC * wC + dhD * wD;
    

    经过调整之后,采样的权重最大时实际上是在块的中心,那么我们也希望采样的flow map位置也是每个块的中心,而现在是,带有偏移量的块采样的是其中心位置,但没有偏移的块采样的是其左下位置,因此我们需要额外进行一点处理:

    			float2 shift = 1 - offset;
                 shift *= 0.5;
                 float2 flowUV = (floor(uv * _GridResolution + offset) + shift) / _GridResolution;
    

    这里放shift放外面是因为我们只是想平移采样的位置,而不是整个块的位置。

    接下来,我们对流动的效果进行微调,例如根据flow map中b通道,即流动速度来调制高度信息和tiling信息,这也比较符合直观印象,流动越快之处,高度越高,tiling越大,流动纹理越密集。

                    float2 mainUV = RotateUV(uv, flowVec.xy, rotMat);
                    mainUV.y = mainUV.y - flowVec.z * _Time.y;
                    float tiling = flowVec.z * _TilingModulated + _Tiling;
                    mainUV *= tiling;
    
                    float3 dh = UnpackDerivativeHeight(tex2D(_MainTex, mainUV));
                    dh.xy = mul(rotMat, dh.xy);
                    dh *= flowVec.z * _HeightScaleModulated + _HeightScale;
    

    同时,为了避免采样出flow map的数据差异过小,我们可以手动为mainUV增加一些偏移来增加差异性:

                    float2 mainUV = RotateUV(uv + offset, flowVec.xy, rotMat);
    

    如果你觉得我的文章有帮助,欢迎关注我的微信公众号(大龄社畜的游戏开发之路)^ - ^

  • 相关阅读:
    nullnullConnecting with WiFi Direct 与WiFi直接连接
    nullnullUsing WiFi Direct for Service Discovery 直接使用WiFi服务发现
    nullnullSetting Up the Loader 设置装载机
    nullnullDefining and Launching the Query 定义和启动查询
    nullnullHandling the Results 处理结果
    装置输出喷泉装置(贪心问题)
    数据状态什么是事务?
    停止方法iOS CGD 任务开始与结束
    盘文件云存储——金山快盘
    函数标识符解决jQuery与其他库冲突的方法
  • 原文地址:https://www.cnblogs.com/back-to-the-past/p/13717917.html
Copyright © 2011-2022 走看看