maltadirk 发表于 2021-4-28 09:42

Unity Shader 水体渲染

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:深度值
float depth_1 = tex2D(_CameraDepthTexture,screenPos.xy / screenPos.w).r;//UNITY_PROJ_COORD:深度值
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 第一章水体渲染

zt3ff3n 发表于 2021-4-28 09:45

可以可以参考一下

Ylisar 发表于 2021-4-28 09:54

好滴!

redhat9i 发表于 2021-4-28 09:58

你就在腾讯实习TA了?你是技术出身还是美术啊

XGundam05 发表于 2021-4-28 09:59

搜嘎。。图形程序嘛?

IT圈老男孩1 发表于 2021-4-28 10:01

技术美术哈 不是图程

IT圈老男孩1 发表于 2021-4-28 10:09

厉害厉害~~~

JamesB 发表于 2021-4-28 10:19

加油!

acecase 发表于 2021-4-28 10:27

优秀

RecursiveFrog 发表于 2021-4-28 10:34

大家都喜欢各大平台用一个网名一个头像的吗哈哈哈哈
页: [1] 2 3
查看完整版本: Unity Shader 水体渲染