Unity URP真实感水体渲染工具(一)水的形态,折射,着色 ...
简介https://github.com/alen-cell/URP-UniversalWaterTool
写这篇文章主要想实现一个比较简单但是不失优雅效果较好的水的渲染工具,适用于URP管线,毕竟水体是个很美的东西~
[*]第一篇将讲水的形状和折射,以及深度着色;
[*]第二篇为水面的复杂一些的交互和反射
[*]第三章是其他一些杂七杂八的,想实现下泡沫效果,曲面细分,sss,再完善前面的内容,因文章太长,首先发一下关于水的形状和折射和简单的交互这篇
水的形状
水的形状有很多种算法,具体请参考这篇文章,真实感水体渲染技术总结 - 知乎 (zhihu.com)
这里实现一种用FlowMap叠加几何形态的波浪实现水面大波叠小波的方法
UDK效果
https://www.zhihu.com/video/1444983701705252868
FlowMap
首先,对于水的小波形态使用FlowMap制作:
不同的FlowMap会产生不同的Distortion形状
[*]将FlowMap的格式设为无压缩,取消SRGB勾选
[*]基本代码如下:
//waterShape
//将FlowMap映射到(-1,1)区间
float3 flowDir = SAMPLE_TEXTURE2D(_FlowMap,sampler_FlowMap,i.uv)*2.0-1.0;
float phase = frac(_Time.y);
float phase0 = frac(_Time.y*0.1*_FlowMapSpeed);
float phase1 = frac(_Time.y*0.1*_FlowMapSpeed + 0.5);
float2 tiling_uv = i.uv*_MainTex_ST.xy+_MainTex_ST.zw;
half3 tex0 = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,tiling_uv - flowDir.xy*phase0);
half3 tex1 = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,tiling_uv - flowDir.xy*phase1);
float flowLerp = abs((0.5-phase0)/0.5);
half3 flowColor = lerp(tex0,tex1,flowLerp);
[*]frac函数可以将不断累加的时间分解成同样高度的多个,再利用一个锯齿波函数让扭曲程度y在一个相位中从0变为1,再在1变为0;
[*]加上Noise
float noise = flowDir.a;
float phase = frac(_Time.y+noise);
[*]再添加对纹理的一个偏移和时间的偏移量,流动大小,等
//waterShape
float2 FlowMapUV = i.uv*_FlowMap_ST.xy+_FlowMap_ST.zw;
float4 flowDir = (SAMPLE_TEXTURE2D(_FlowMap,sampler_FlowMap,FlowMapUV)*2.0-1.0)*_FlowStrength;
float noise = flowDir.a*3;
float phase0 = frac(_Time.y*0.1*_FlowMapSpeed+_phaseOffset+noise);
float phase1 = frac(_Time.y*0.1*_FlowMapSpeed + 0.5+_phaseOffset+noise);
float2 tiling_uv = i.uv*_MainTex_ST.xy+_MainTex_ST.zw;
float2 flowUV = tiling_uv - flowDir.xy*phase0 + _UJump + _VJump;
float2 flowUV1 = tiling_uv - flowDir.xy*phase1;
half3 tex0 = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,flowUV);
half3 tex1 = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,flowUV1);
float flowLerp = abs((0.5-phase0)/0.5);
half3 flowColor = lerp(tex0,tex1,flowLerp);
大概效果
https://www.zhihu.com/video/1445042222824075264
将扭曲用于BumpMap后进行简单的着色,这里我采用的两张Bump放置在一个贴图中,根据不同大小Tiling采样,代码和调整效果如下:
// half3 normalA =UnpackNormal(SAMPLE_TEXTURE2D(_NormalTex,sampler_NormalTex,flowUV));
// half3 normalB =UnpackNormal(SAMPLE_TEXTURE2D(_NormalTex,sampler_NormalTex,flowUV1));
//half3 normalWS =lerp(normalA,normalB,flowLerp);
// half3x3 TBN = {i.tangentWS, i.BtangentWS, i.normalWS};
//useBumpMap
float3 BumpA = UnpackDerivativeHeight(SAMPLE_TEXTURE2D(_BumpTex,sampler_BumpTex,flowUV*_Tiling));
float3 BumpB = UnpackDerivativeHeight(SAMPLE_TEXTURE2D(_BumpTex,sampler_BumpTex,flowUV1*_TilingB));
float3 Bump = lerp(BumpA,BumpB,flowLerp);
//normalWS = normalize(float3(Bump.x+BumpB.xy,1));
float3 normalWS = i.normalWS +float3(Bump.x,0,Bump.y);
half3 spec = pow(saturate(dot(halfDir,normalWS)), _Glossiness)*mainLight.color*_SpeStrength;
half3 diffuse = dot(mainLightDir,normalWS)*_DepthGradient1;
half3 finalColor = spec + diffuse;
再一次大波叠小波
调整Bump高度之后
几何波形
波形有很多种
这里采用Gerstner波形
先实现一个简单的正弦波
VertexOutput VERT(VertexInput v)
{
VertexOutput o;
o.normalWS = TransformObjectToWorldNormal(v.normalOS);
#ifdef _ENABLE_GEOMETRY_WAVE_ON
float3 position = v.positionOS;
float k = 2*3.14/_WaveLength;
float f = k*(position.x - _WaveSpeed*_Time.y);
position.y =_Amplitube*sin(f);
float3 tangent = normalize(float3(1,k*_Amplitube*cos(f),0));
float3 normal = float3(-tangent.y,tangent.x,0);
o.normalWS = TransformObjectToWorldNormal(normal);
v.positionOS = position;
#endif
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
o.positionWS = TransformObjectToWorld(v.positionOS);
o.uv = v.uv;
return o;
}
10x10 quad
开始的平面是一个简单的10x10平面,这里为了追求更好的效果,替换成100x100的
相对的参数也需要调小10倍--1.0--0.01等
Grstner波形描绘的是在水面上下波动的同时,也会收到前后波动。在水面上一点接近峰值的时候,点会向着波峰移动,在波峰消散时,点会向着相反方向滑动
在x轴上采用加上Cos(f)可以模拟收到的力的周期
//修改函数和法线
float3 position = v.positionOS;
float k = 2*3.14/_WaveLength;
float f = k*(position.x - _WaveSpeed*_Time.y);
position.x+= _Amplitube*cos(f);
position.y =_Amplitube*sin(f);
float3 tangent = normalize(float3(1-k*_Amplitube*sin(f),k*_Amplitube*cos(f),0));
float3 normal = float3(-tangent.y,tangent.x,0);
o.normalWS = TransformObjectToWorldNormal(normal);
v.positionOS = position;
[*]将WaveSpeed与Wavelength关联起来
[*]加上方向向量,
float k = 2*3.14/_WaveLength;
float c = sqrt(9.8/k);
float2 d = normalize(_Direction);
float f = k*(dot(d,position.xz) - _WaveSpeed*_Time.y);//这里应将Wavespeed改为c,因为效果不太好所以采用手动调整
float a = _Steepness/k;
position.x+= d.x*(a*cos(f));
position.y =a*sin(f);
position.z+=d.y*(a*cos(f));
[*]重新计算切线
float3 tangent = float3(1-d.x*d.x*(_Steepness*sin(f)),
d.x*(_Steepness*cos(f)),
-d.x*d.y*(_Steepness*sin(f)));
float3 binormal = float3(
-d.x*d.y*(_Steepness*sin(f)),
d.y*(_Steepness*cos(f)),
1-d.y*d.y*(_Steepness*sin(f))
);
float3 normal = normalize(cross(binormal,tangent));
https://www.zhihu.com/video/1446170824763539456
[*]叠加三个波形,将波形函数拆开
float3 GerstnerWave(
float4 wave,float3 position,inout float3 tangent,inout float3 binormal
,float waveSpeed,float scale)
{
float steepness = wave.z/scale;
float wavelength = wave.w/scale;
float k = 2*3.14/wavelength;
float c = sqrt(9.8/k);
float2 d = normalize(wave.xy);
float f = k*(dot(d,position.xz) - (waveSpeed/scale)*_Time.y);
float a = steepness/k;
tangent += float3(1-d.x*d.x*(steepness*sin(f)),
d.x*(steepness*cos(f)),
-d.x*d.y*(steepness*sin(f)));
binormal += float3(
-d.x*d.y*(steepness*sin(f)),
d.y*(steepness*cos(f)),
1-d.y*d.y*(steepness*sin(f))
);
return float3(d.x*(a*cos(f)),a*sin(f),d.y*(a*cos(f))
);
}
#ifdef _ENABLE_GEOMETRY_WAVE_ON
float3 gridPoint = v.positionOS;
float3 tangent = float3(1,0,0);
float3 binormal = float3(0,0,1);
float3 position = gridPoint;
position += GerstnerWave(_WaveA,gridPoint,tangent,binormal,_WaveSpeed,_Scale);
position += GerstnerWave(_WaveB,gridPoint,tangent,binormal,_WaveSpeed,_Scale);
position += GerstnerWave(_WaveC,gridPoint,tangent,binormal,_WaveSpeed,_Scale);
float3 normal = normalize(cross(binormal,tangent));
o.normalWS = TransformObjectToWorldNormal(normal);
v.positionOS = position;
#endif
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
o.positionWS = TransformObjectToWorld(v.positionOS);
o.uv = v.uv;
return o;
[*]最终得到:
https://www.zhihu.com/video/1446511399925616640
水的着色
深度着色(后期还需要修正视角问题)
利用一张RampTex或者渐变颜色,
基本代码如下:
//在顶点着色器中计算 屏幕空间的的坐标
o.projPos = ComputeScreenPos(o.positionCS);
o.projPos.z = -mul(UNITY_MATRIX_MV,v.positionOS).z;
//在像素着色器中计算 半透明物体和场景深度插值进行着色
float3 downWardVector = TransformWorldToView(float4(0,1,0,1));
float sceneZ = LinearEyeDepth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture,UNITY_PROJ_COORD(i.projPos))),_ZBufferParams);
float WaterZ = i.projPos.z;
float3 Debug = normalize(dot(downWardVector,viewDirection));
float DepthDiff = (sceneZ - WaterZ);
DepthDiff /= _SceanDepth;
DepthDiff = clamp(0,0.99,DepthDiff);
//float3 DepthColor = lerp(_DepthGradient1,_DepthGradient2,DepthDiff);
float3 DepthColor = SAMPLE_TEXTURE2D(_rampTex,sampler_rampTex,(DepthDiff + 0.2)*4/5);
手动渐变色+融合Alpha透明度
float alpha = (0,1,DepthDiff);折射
这里我们用获取的_CameraOpaqueTexture进行简单的UV偏移,根据,Bump深度,UV深度和视角方向计算偏移的大小。
float2 screenPos = UNITY_PROJ_COORD(i.projPos);//除以w分量
float2 refractionUVOffset = Bump*0.01*_RefractionIntensity;
refractionUVOffset *= saturate((DepthDiff)/abs(_RefractionGradientRange)+0.001)*Debug;
float2 sceneRefractionUVs = float2(1,grabSign)*(screenPos)+float4(refractionUVOffset, refractionUVOffset).rg;;
float4 refractionColor = tex2D(_CameraOpaqueTexture, sceneRefractionUVs);
然后为了使偏移发生只在水面之下,我们需要检测深度差来控制偏移。
未剔除
修正过后
float2 sceneRefractionUVs = float2(1,grabSign)*(screenPos)+float4(refractionUVOffset, refractionUVOffset).rg;
float sceneZRefraction = LinearEyeDepth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, sceneRefractionUVs)), _ZBufferParams);
float DepthDiffRefraction = (sceneZRefraction - WaterZ);
if (DepthDiffRefraction < 0){
sceneRefractionUVs =float2(1,grabSign)*screenPos;
}
float4 refractionColor = tex2D(_CameraOpaqueTexture, sceneRefractionUVs);
当做到这里,单纯的混合还是不能达到好的效果,还需要根据当前UV重新采样深度,再进行混合
if (DepthDiffRefraction < 0){
sceneRefractionUVs =float2(1,grabSign)*screenPos;
}
//重新采样
sceneZRefraction = LinearEyeDepth(UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, sceneRefractionUVs)), _ZBufferParams);
DepthDiffRefraction = sceneZRefraction - WaterZ;
DepthDiffRefraction /= _SceanDepth;
DepthDiffRefraction = clamp(0,1,DepthDiffRefraction);
//DepthDiffRefraction /= _SceanDepth;//TODO 三阶颜色
float4 refractionColor = tex2D(_CameraOpaqueTexture, sceneRefractionUVs) ;
已经差不多修正好了,稍微有一点点瑕疵
物体周围还有一些细线,是因为采样深度没有修复的问题造成的。
需要重新采样。
float2 AlignWithGrabTexel(float2 uv){
return (floor(uv*_CameraDepthTexture_TexelSize.zw)+ 0.5)*abs(_CameraDepthTexture_TexelSize.xy);
}
........
sceneRefractionUVs = AlignWithGrabTexel(sceneRefractionUVs);
if (DepthDiffRefraction < 0){
sceneRefractionUVs =AlignWithGrabTexel(float2(1,grabSign)*screenPos);
}
水面交互(高度图生成)
https://www.zhihu.com/video/1450494172628848640
目标:先制作简单的一个交互,鼠标点击水面,水面会泛起涟漪。
包括三部分:触发器,和需要绘制RT的RenderFeature,Shader需要触发器的Shader和计算波动方程的Shader。
触发器部分:
发射CameraRay,如果Hit到便绘制到RT中
RenderFeature部分:
为了制作水面的交互,我们需要3张RT
当前一帧RT,上一帧的RT,和一个临时的RT。
简单的水面公式可以是
float3 e = float3(_CurrentRT_TexelSize.xy,0);
float2 uv =i.uv;
//获取上下左右四个值
float p10 = SAMPLE_TEXTURE2D(_CurrentRT, sampler_CurrentRT, uv - e.zy).x;//下
float p01 = SAMPLE_TEXTURE2D(_CurrentRT, sampler_CurrentRT, uv - e.xz).x;//左
float p21 = SAMPLE_TEXTURE2D(_CurrentRT, sampler_CurrentRT, uv + e.xz).x;//右
float p12 = SAMPLE_TEXTURE2D(_CurrentRT, sampler_CurrentRT, uv + e.zy).x;//上
//前一帧中心值
float p11 =SAMPLE_TEXTURE2D(_PrevRT, sampler_PrevRT, uv).x;
//计算结果
float d = (p10 + p01 + p21 + p12) / 2 - p11;
//衰减
d *= _Attenuation;计算的公式可以参考GDC nvidia 这篇:https://www.gdcvault.com/play/203/Fast-Water-Simulation-for-Games
c为波速,h为每一格的波动距离,t为 递进时间
代码:
https://github.com/alen-cell/URP-UniversalWaterTool
参考:
页:
[1]