赞
踩
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;
这样水面就动起来了。
///
接着我开始做根据水面深度来采样一张渐变贴图,达到水底是深色的,岸边是浅色的这一效果。具体文章出自
博客上讲的很清楚,我这里补充一下更细致的概念。
1.直接获得unity相机内置的屏幕深度图:链接详解
sampler2D _CameraDepthTexture;
2.用ComputeScreenPos函数返回一个float4的值,输入的参数时经过MVP变换的后在裁剪空间的顶点坐标,透视投影中z的分量范围是[-Near,Far],w值为[Near,Far];正交投影中z为[-1,1],w恒为1。
o.proj = ComputeScreenPos(o.pos); //返回齐次坐标系下的屏幕坐标值
COMPUTE_EYEDEPTH(o.proj.z); //计算深度
4.SAMPLE_DEPTH_TEXTURE_PROJ为unity提供的宏,LinearEyeDepth负责把深度纹理的采样结果转换到视角空间下的深度值。
float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture , i.proj);
half m_depth = LinearEyeDepth(depth);
half deltaDepth = m_depth - i.proj.z;
阶段性展示
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一个链接中有目前常见的所有反射效果
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
工程
提取码:1963
如果大佬看到觉得哪里不好,改了的话一定告诉我。
这篇为学习笔记,如果代码雷同,那确实是我copy的(菜。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。