【unity shader】水体渲染基础-基于texture distortion的流体流动材质

1.基于uv的texture distortion

当液体静止时,它在视觉上与固体没有太大区别。 但大多数时候,我们的性能不一定支持去实现特别复杂的水物理模拟, 需要的只是在常规的静态材料的表面上让其运动起来。我们可以对网格的 UV 坐标实现动态变化,从而让表面的纹理效果实现变形的动态变化。

1.1. uv实时变化

我们直接生成一个默认unlit shader,然后赋予其对应的纹理图片,并且写入随时间变化的uv更新函数。

//in frag shader
i.uv += _Time.y;

在这里插入图片描述
在这里插入图片描述
为了实现随机方向的uv变化,我们这里引入一张指示流动方向的贴图。

float2 flowDir = tex2D(_FlowMap, i.uv);
i.uv += _Time.y * flowDir;

在这里插入图片描述
在这里插入图片描述
可以看到随着时间推移,出现了uv挤压过度的情况,我们这里可以直接用一个frac或者正弦函数来限制uv随时间的变化范围。
在这里插入图片描述
tilling为4显得纹理太小了,我们把tilling调整回1。
在这里插入图片描述
但是在进行实时变形的过程中,纹理会完全恢复到未变形的状态,还是有点过于奇怪,我们通过对三角函数进行缩放和位移,使纹理一直保持在变形的状态。

//即使得时间参数一直为正数
i.uv += (sin(_Time.y)/2.0 + 1.0) * flowDir;

在这里插入图片描述
当我们使用frac函数来对time进行限制的时候,就会出现突然抖动,一跳又变成了平整的未变形纹理的情况。这是由于frac本身会带来取值从1-0的非连续变化带来的。
在这里插入图片描述

在这里插入图片描述

1.2. 实现循环衔接

既然frac函数存在不连续的问题,我们就得想办法掩盖一下(或者直接换成sin函数)。掩盖的办法就是通过黑色的淡入淡出,来掩盖住发生纹理颜色跳跃的瞬间。
即在frac的极大值和极小值时,都需要是黑色,且其需要在原有的周期内,实现从最小值到最大值的变化,再由最大值变为最小值,周期缩短一半(橙色线条)。
在这里插入图片描述

///flowDir 命名时改为Float3,但其实这边单独设一个小数也可以的
flowDir.z = 1.0 - abs(1.0 - 2* time_fac);
fixed4 col = tex2D(_MainTex, i.uv) * flowDir.z;

在这里插入图片描述
为了让贴图的变形更多样化,增加信息量,我们引入相关的噪音,放入到flowmap中作为a通道。从a通道中采样noise信息,混入时间计算当中。

float3 flowDir;
flowDir.xy = tex2D(_FlowMap, i.uv).rg;
float noise = tex2D(_FlowMap, i.uv).a;

float time_fac = frac(_Time.y + noise);

flowDir.z = 1.0 - abs(1.0 - 2* time_fac);
i.uv += time_fac * flowDir;

fixed4 col = tex2D(_MainTex, i.uv) * flowDir.z;

在这里插入图片描述
由于噪音的混入,使得渐变的黑色也能像波纹一样扩散。且本身引入噪音后,即便是不适用黑色进行渐入,非连续跳跃的问题也得到了有效缓解。
在这里插入图片描述

1.3. 混合纹理变形

当然通过黑色来进行淡入淡出,还是略显突兀一些,最好的方案肯定还是两种不同变形的纹理进行混合,通过权重进行区分。最好是当其中一个纹理的uv偏移为0时,另一个纹理的uv偏移为1,以确保能够全时刻都处在变形的状态。
在这里插入图片描述

首先我们要将原本放在frag shader里面计算uv偏移的部分打包成一个函数:
输入是变形前的uv,变形方向(采样自flowmap),时间参数和用于判断是否为第二个混合纹理的布尔参数。
返回值是三位浮点数,uvw,对应偏移后的uv以及对应的可见度参数。

扫描二维码关注公众号,回复: 15607242 查看本文章
float3 flowUVW(float2 uv, float2 flowVec, float3 time, bool Btag)
{
    
    
	//period对应偏移周期
	float phaseOffset = Btag? _Period: 0 ;
	time += phaseOffset;
	float3 uvw;
	uvw.xy = uv + time * flowVec;
	uvw.z = 1.0 - abs(1.0 - 2* time);
	return uvw;
}

//in frag shader
float2 flowVec;
flowVec = tex2D(_FlowMap, i.uv).rg;
float noise = tex2D(_FlowMap, i.uv).a;
float time_fac = frac(_Time.y + noise);

float3 uvwA = flowUVW(i.uv, flowVec, time_fac, false);
float3 uvwB = flowUVW(i.uv, flowVec, time_fac, true);

fixed4 colA = tex2D(_MainTex, uvwA.xy) * uvwA.z;
fixed4 colB = tex2D(_MainTex, uvwB.xy) * uvwB.z;

fixed4 col = colA + colB;

在这里插入图片描述
会出现黑色区域过多的问题,目测是因为uvw的z值没有处理好,并没有做好严格的偏移1/2个周期,导致累加的效果有问题。所以我们这里需要换成:

fixed4 colA = tex2D(_MainTex, uvwA.xy) * uvwA.z;
fixed4 colB = tex2D(_MainTex, uvwB.xy) * (1 - uvwA.z);

确保两个权重相加为1,调整后黑波纹的效果改善了。
当然这里也可以对time += phaseOffset做调整,传入前的时间参数不再进行取小数,而改用frac函数处理传入后的值,能够实现一样的效果。
在这里插入图片描述
在计算uv时,为更好表现两个uv的偏移程度,最好在uv初始化时也加入偏移值。

//in flowUVW function
uvw.xy = uv + time * flowVec + phaseOffset ;

1.4.通过uv jumping调整循环的周期

目前来说,我们的uv动画的周期,完全取决于_Time.y的固定周期。若是我们想要自定义地去调整uv动画的周期,则需要人为地添加相应的变量。
这里我们引入了jump参数,一个二维浮点数变量。
我们可以分别定义jump参数在u方向,v方向上的数值,通过这两个数值来控制循环周期。

_UJump ("u direction jump para", Range(-0.25, 0.25)) = 0.25
_VJump ("v direction jump para", Range(-0.25, 0.25)) = 0.25

//in frag shader
float2 jump = float2(_UJump, _VJump);

在flowUVW,我们新传入入jump参数,在初始化uvw的xy值后,通过jump参数对uvw.xy做一个叠加。

float3 flowUVW_jump(float2 uv, float2 flowVec, float2 jump, float3 time, bool Btag){
    
    
    float phaseOffset = Btag? _Period: 0 ;
    float progress = frac(time + phaseOffset);
    
    float3 uvw;
    uvw.xy = uv + progress * flowVec + phaseOffset;
    uvw.xy +=  (time - progress) * jump ;
    uvw.z = 1.0 - abs(1.0 - 2* progress);
    return uvw;
}

//in frag shader
float time_fac = _Time.y + noise;
float2 jump = float2(_UJump, _VJump);

float3 uvwA = flowUVW_jump(i.uv, flowVec, jump, time_fac, false);
float3 uvwB = flowUVW_jump(i.uv, flowVec, jump, time_fac, true);

可知我们得uv值的初始值是固定的,随着时间的变化,time值(即_Time.y + noise的值)逐渐增大,uvw.xy的值最终会以(time - progress) * jump为周期稳定循环,最终实现uv动画的可控周期。

在使得flowVec 为(0,0)时,我们能够比较明显地比较jump参数带来的周期差异:
左边是使用了jump为(0.25, 0)时的uv动画,右边为仅使用flowUVW的uv动画。在这里插入图片描述

2. 为uv动画添加其他参数

2.1. Tilling

用于调整贴图缩放的参数,典中典操作。

//in flowUVW
uvw.xy = uv + progress * flowVec + phaseOffset;
uvw.xy *= _Tilling;
//.....

在这里插入图片描述

2.2. 动画速度

显然这个是放到跟_Time.y乘在一起的,影响uv动画的速度。
在这里插入图片描述

2.3. 变形/流动强度

通过设置相关参数,放大/缩小从a通道采样到的变形方向。
在这里插入图片描述

2.4. 通过变形偏移控制uv动画的清晰度

目前来看,uv动画的清晰度多少有点捉鸡,这是由uv偏移程度,和当前纹理的权重决定的。
显然,当uvw.z=1时,当前计算的颜色越清晰。(无论是a颜色还是b颜色,只要有其中一方的uvw.z为1时,另一方则一定为0)
当uvw.z=1时, progress值为1/2。即time + phaseoffset值为xxxxxx.5。
所以我们需要在uvw.z=1的前提下,添加一个_FlowOffset,通过和progress相互抵消,去抹平相应的uv偏移,使得uv不受flowVec引入的各向异性变形的影响。

//in flowUVW, 
uvw.xy = uv + (progress + _FlowOffset) * flowVec + phaseOffset;
uvw.xy *= _Tilling;

使用_FlowOffset(左)和不使用_FlowOffset(右)的对比。
在这里插入图片描述
当变形强度增大时,清晰化的效果会更明显。
在这里插入图片描述
在这里插入图片描述

2.5. 替换颜色贴图

接下来我们使用真正的液体颜色贴图,以及对应的法线贴图。
在这里插入图片描述
在这里插入图片描述
同样的,我们需要各自用uvwA和uvwB对变换后的法线贴图进行采样,并对采样后的法线和进行标准化。

//in frag shader
//前置步骤:计算TBN矩阵,传递TBN矩阵,获取worldPos,执行uvwA,uvwB的计算等
float4 packedBumpA = tex2D(_NormalTex, uvwA.xy);
float4 packedBumpB = tex2D(_NormalTex, uvwB.xy);

float3 tangentSpaceNormalA = UnpackNormalWithScale(packedBumpA, _BumpScale);
float3 tangentSpaceNormalB = UnpackNormalWithScale(packedBumpB, _BumpScale);

float3 worldNormalA = normalize(mul(tangent2World, tangentSpaceNormalA));
float3 worldNormalB = normalize(mul(tangent2World, tangentSpaceNormalB));
float3 worldNormal = normalize(worldNormalA + worldNormalB);
//后续步骤:bling-phong三部曲

在这里插入图片描述

2.6.尝试不同的噪音组合

我用SD也做了一点自制噪音贴图,主要是voronoi和bnw混合,以及voronoi和plasma混合,大家也可以自己做一些尝试。
如果是老版本的SD,没有voronoi节点的话,可以用tile generator加上distance来做一个简易的voronoi。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.7. 通过derivative map增加法线细节

由于图像压缩的机制,常规的法线贴图往往会有一定程度的细节丢失。derivative map主要是将法线的x,y方向的偏导数(即切线和副切线的部分值)存储在a,g通道,而不需要经过再次转换。
我们直接通过a,g通道的值组成两个切线,并通过叉乘的方式反求法线。
在这里插入图片描述
同样的,我们写一个解包函数,把从图像中读取到的值恢复到(-1,1)的区间。

float3 UnpackDerivativeHeight(float4 textureData){
    
    
    float3 dh = textureData.agb;
    dh.xy = dh.xy * 2 - 1;
    return dh;
}

在片元着色器中,我们全部删掉切线空间转换相关的内容,但是需要保留worldPos来做光照计算。其余的换成综合两个采样的法线贴图求解worldNormal的部分。

注意这里和catlikecoding大佬的教程不同,由于derivative map展开的是沿z方向的法线在xy方向的偏导数,但是我们没有再去求TBN矩阵来做转换,而是直接使用相应的值。需要还原出来的法线是沿y方向的,所以需要调整下相应的组成分布来求解正确的法线方向。

//in frag shader
float3 dhA = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwA.xy)) * uvwA.z;
float3 dhB = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwB.xy)) * uvwB.z;

float2 worldNormalXY = -(dhA.xy + dhB.xy);
float3 worldNormal = normalize(float3(worldNormalXY.x, 1, worldNormalXY.y));
//正常计算bling-phong

右边为使用derivative map的uv流体动画。
在这里插入图片描述

2.8.控制浪高

通过增加可调的_HeightScale参数,影响采样后的权重计算。

float3 dhA = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwA.xy)) * (uvwA.z * _HeightScale);
float3 dhB = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwB.xy)) * (uvwB.z * _HeightScale);

在这里插入图片描述
更近一步的,我们能够通过波流动速度来影响浪高。

float finalHeightScale = length(flowVec) * _Flow2HeightScale + _HeightScale;

float3 dhA = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwA.xy)) * (uvwA.z * finalHeightScale);
float3 dhB = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwB.xy)) * (uvwB.z * finalHeightScale);

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/misaka12807/article/details/131503536