|
Unity Shader Water 真实感水体的制作心得
用了两周半的时间研究了一下基本的水体渲染。效果中并不涉及顶点的波移动,仅为法线贴图扰动,其中包括了海浪、平面反射、高光反射、折射等效果。适合用于PC端各类型游戏的水面效果,且源代码用的是最基本的顶点片元着色器和一些常见的shaderlab函数,也容易运用到其他引擎上。
此文是我一个学习笔记,分享出来希望大家一起学习,如果有大佬教导就更好了。
我在看了网上能找到的多部分水体的文章和论文后(文末会列出),结合一些实际游戏的效果图,其中代码会有所ctrlc/v和删减,实际效果还有许多欠缺,之后会继续学习。
首先要说我选择的是在世界空间下计算这些参数,即先在顶点着色器中计算切线空间到世界空间的变换矩阵,把他传递给片元着色器,再在片元着色器中把法线方向从切线空间变换到世界空间。 在计算中我会运用到这些基础向量:worldPos / lightDir / viewDir / halfDir / NdotL(法线点乘光源方向)/ NdotH(法线点乘半角向量)均为世界空间下 。
关于世界空间下法线的变换 详请翻阅《入门精要》P152。
这里贴上主要代码:- struct v2f
- {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- float4 TtoW0:TEXCOORD2;
- float4 TtoW1:TEXCOORD3;
- float4 TtoW2:TEXCOORD4;
- };
复制代码 //顶点着色器的输出结构体v2f,包含了切线空间到世界空间的变换矩阵。- v2f vert (appdata v)
- {
- v2f o;
- o.pos = UnityObjectToClipPos(v.vertex);
- o.uv = TRANSFORM_TEX(v.uv, _MainTex);
- float3 worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
- float3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
- fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
- fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
-
- o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
- o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
- o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
- return o;
- }
复制代码 顶点着色器中计算了世界空间下的顶点切线、副法线和法线的矢量表示。
//要用的向量- float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
- float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
- float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
- float3 halfDir = normalize(lightDir + viewDir);
- fixed3 tangentNormal1 = UnpackNormal(tex2D(_NormalTex , i.uv + offset)).rgb;
- fixed3 tangentNormal2 = UnpackNormal(tex2D(_NormalTex , i.uv - offset)).rgb;
- fixed3 tangentNormal = normalize(tangentNormal1 + tangentNormal2);
- tangentNormal.xy *= _NormalScale;
- tangentNormal.z = sqrt(1 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
- float3 worldNormal = normalize(half3(dot(i.TtoW0.xyz, tangentNormal), dot(i.TtoW1.xyz, tangentNormal), dot(i.TtoW2.xyz, tangentNormal)));
- float NdotH = max(0,dot(halfDir , worldNormal)); //BlinnPhong
- float NdotL = max(0,dot(worldNormal , lightDir)); // 漫反射
复制代码 这里的offset是一个法线扰动值,之后会讲解到。计算LightDir / viewDir 使用了UnityCG.cgine中常用的帮助函数,了解请翻阅《入门精要》P108,详细请查找源码 。使用前需要在前面声明:- #include "UnityCG.cginc"
复制代码 我们现在基本的向量都计算完了,有了这些向量我们就可以开始逐个计算各个元素了。我们先计算了A(ambient)D(diffuse)S(specular),- fixed3 diffuse = _LightColor0.rgb*col*saturate(dot(worldNormal , lightDir)) ;
- fixed3 specular = pow( NdotH , _Specular * 128.0) * _Gloss;
- float3 ambient = col*UNITY_LIGHTMODEL_AMBIENT.xyz;
复制代码 (_LightColor0是directional light的颜色)
使用_LightColor0 和UNITY_LIGHTMODEL_AMBIENT需要在前面声明- #include "Lighting.cginc"
复制代码 这是最基本的光照模型,我们可以看到以下效果。
///
之后我们加入法线偏移,这样水面就会动起来,原理是用一张法线贴图根据内置的_Time函数计算出一个可动的float2类型的偏移值,再在法线从切线空间转到世界空间的时候进行uv偏移,达到水面流动的效果。
//法线扰动- float4 offsetColor = (tex2D(_NormalTex, i.uv
- + float2(_WaveXSpeed*_Time.x,0))
- + tex2D(_NormalTex, float2(i.uv.y,i.uv.x)
- + float2(_WaveYSpeed*_Time.x,0)))/2;
-
- // float4 waveOffset = tex2D(_NormalTex ,i.uv + wave_offset);
- half2 offset = UnpackNormal(offsetColor).xy * _NormalRefract;//法线偏移程度可控之后offset被用于这里
-
- fixed3 tangentNormal1 = UnpackNormal(tex2D(_NormalTex , i.uv + offset)).rgb;
- fixed3 tangentNormal2 = UnpackNormal(tex2D(_NormalTex , i.uv - offset)).rgb;
复制代码 这样水面就动起来了。
///
关于岸边效果,我这里更改一下看了很多遍后的概念(这里的代码和源码比有所更改):
岸边效果的本质就是在视角空间下比较水面和地面的线性深度,水面是半透物体,地面时不透物体。所以本质就是比较视角空间下半透和不透物体的线性深度差值。半透物体的深度值 是NDC空间下的z值,ComputeScreenPos返回的是齐次裁剪空间下的屏幕坐标,除以w等于NDC空间下的屏幕坐标。
- float4 clipPos = UnityObjectToClipPos(v.vertex);
- float4 screenPos = ComputeScreenPos(clipPos);
- float4 screenPosNDC = screenPos / screenPos.w; //NDC下的屏幕坐标
复制代码不透物体的深度值 是通过直接获得unity相机内置的屏幕深度 CameraDepthTexture得到的,有四种方法:tex2Dproj的作用就是程序自动进行透视除法,效率比写出来的除要高,而这里我选择depth_2作为输出,因为他也是采样的NDC的xy坐标,和上文获取到的深度值在一个坐标空间里,所以理解起来比较方便。
- //详见 https://zhuanlan.zhihu.com/p/107627483
- float depth = tex2Dproj (_CameraDepthTexture,screenPos).r; //UNITY_PROJ_COORD:深度值 [0,1]
- float depth_1 = tex2D(_CameraDepthTexture,screenPos.xy / screenPos.w).r; //UNITY_PROJ_COORD:深度值 [0,1]
- float depth_2 = tex2D(_CameraDepthTexture,screenPosNDC.xy).r; //直接采样NDC下的xy坐标
- float depth_3 = SAMPLE_DEPTH_TEXTURE( _CameraDepthTexture, screenPosNDC.xy ); //SAMPLE_DEPTH_TEXTURE解决平台差异性问题
复制代码之后我们有了水面和地面的非线性深度,都是基于NDC坐标空间的。现在就是要把他们变为视角空间,用的是 LinearEyeDepth。得到不透物体和半透物体的线性深度值,然后做差。
- half s_depth = LinearEyeDepth(depth_2); //不透明物体的线性深度
- half t_depth = LinearEyeDepth(screenPosNDC.z); //半透明物体的线性深度
复制代码注:2021.3.17改,在看了大量文章后,有点明白了。但是目前这样的描述不知道是否准确,望指正。因为我看还有一种方法,就是关于ComputeScreenPos函数返回的值,我们知道这个得到的值其实不是字面意思齐次空间下屏幕的坐标值,他是个float4类型的值,根据输入值clipPos 我们知道他的输入值是裁剪空间下的顶点坐标,之后看过ComputeScreenPos的源码后发现计算完的zw值和输入的zw是相等的,没有变化。所以ComputeScreenPos得到的w就等于裁剪空间下的w,而根据投影矩阵,裁剪空间下的w就等于视角空间下的-z。所以直接获取screenPos的w就相当于直接获取到了物体在视角空间下的深度值,所以也可以这么写:
- half s_depth = LinearEyeDepth(depth_2); //不透明物体的线性深度
- half t_depth = screenPos.w; //半透明物体的线性深度
复制代码 阶段性展示
5.采样渐变贴图- fixed4 gradientColor = tex2D(_GradientTex , float2(sin(min(_Range.y , deltaDepth)/_Range.y),1));
复制代码 这样一来加上高光等效果就上档次了。
///
接着我们把海浪做了,海浪也是借鉴于那篇文章,不过有所减少。
//海浪- float3 n = tex2D(_NoiseTex , i.uv).rgb;
- fixed3 w = tex2D(_WaveTex , float2 ( sin( _Time.y+ min(_Range.x, deltaDepth)/_Range.x) , 1) ).rgb;
- float rz = 1 - (min(_Range.z , deltaDepth) / _Range.z );
复制代码 最后输出颜色时加上w*rz就可以产生这个效果。
///
再加入反射,这里我用了平面反射。po一个链接中有目前常见的所有反射效果
https://blog.csdn.net/puppet_master/article/details/80808486- sampler2D _ReflectionTex;
复制代码 这里的用proj的xy除以proj的z来得到视口空间的坐标。具体原理在上面的链接中,讲的很好!概括来说就是新建一个相机渲染反射的图像,然后我们把渲染好的图像进行偏移。- fixed3 reflectionCol = tex2D(_ReflectionTex, i.proj.xy/i.proj.w).rgb ;
复制代码 看一下效果
之后是折射,就是抓屏。我在最终效果中没有加入这个,因为我们前面生成的渐变色已经很好的把水下的样子渲染出来了,而且效果不错。不过如果是岸边比较浅的话还是用抓屏更好一点。直接上代码。- GrabPass{"_GrabTex"}
复制代码 //得到对应被抓取屏幕图像的采样坐标- o.scrPos = ComputeGrabScreenPos(o.pos);
- fixed3 refractColor = (tex2D( _GrabTex, i.scrPos.xy/i.scrPos.w).rgb );
复制代码 效果图
///
好了有了折射有了反射 我们就可以快乐的菲涅耳了。
不过我实验了很多菲涅耳公式的简单变形式,效果并不佳。而且用折射和反射的菲涅耳效果更不好,所以我最终抛弃了折射,直接用光照模型结果和反射做了菲涅耳,说实话效果同样一般,但正在继续改进中。- fixed fresnel = pow((1 - (dot(worldNormal,viewDir))),5);
- float3 finalCol = diffuse * gradientColor.rgb + specular + ambient;
- fixed3 f = lerp(finalCol,reflectionCol,saturate(fresnel))*atten;
复制代码 最终输出,Alpha用的是一个float值采样的深度值。- float Alpha = min(_Range.w, deltaDepth)/_Range.w; //透明度
- return float4((f + w * rz), Alpha);
复制代码 源文件中还加了法线融合和焦散的初始代码,但具体的并没有完美实现就不放效果了。
我想加入水波纹的交互但我不太会,有人有学习链接请评论出来,谢谢!
这是现阶段的输出效果。
以上是我二十天所学习到的水体渲染 学习笔记和总结。之后我会把有用的链接放在底下。
2020.8.10
如果大佬看到觉得哪里不好,改了的话一定告诉我。
这篇为学习笔记,如果代码雷同,那确实是我copy的(菜。
https://blog.csdn.net/u011076940/article/details/88018568/ unity水体渲染目录
https://www.jianshu.com/p/2b0e3f7f15b4 unity海洋水体渲染
https://github.com/QianMo/Game-Programmer-Study-Notes/tree/master/Content/%E3%80%8AGPU%20Gems%201%E3%80%8B%E5%85%A8%E4%B9%A6%E6%8F%90%E7%82%BC%E6%80%BB%E7%BB%93#%E4%B9%A6%E6%9C%AC%E9%85%8D%E5%A5%97%E8%B5%84%E6%BA%90%E4%B8%8E%E6%BA%90%E4%BB%A3%E7%A0%81%E4%B8%8B%E8%BD%BD GPU GEMS 第一章水体渲染 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|