接下来是水体渲染基础的最后一篇,通过水面看到水下的物体,并呈现深度效果。
1. 搭建简单演示场景
我们直接搭一个小场景。
增加水面,赋予uv变形的水面材质,并增加透明度的设置。
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
LOD 100
Pass
{
//Tags {"LightMode" = "ForwardBase"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
//.......返回的color结果,添加一个控制透明度的参数
}
//注意FallBack也要注释掉
}
2. 基于雾效实现水深效果
水体会吸收光线,所以真实的水体并不是完全透明的。此外,水体对不同频率的光吸收率不同,蓝光被吸收最少。
故深度越深,水中的物体就会变成蓝色。
我们当然可以直接上一个全局雾,但这里我们最好还是使用仅面向水体的雾效计算。
这里开始,我们新增一个水下计算相关的cginc和一个返回水下片元颜色结果的函数。
新建cginc文件时我们需要注意,在windows文件夹下创建txt文件,并注意要修改文件后缀。
#include "LookingThroughWater.cginc"
float3 ColorBelowWater ()
{
//目前只返回黑色
return 0;
}
//返回值乘以ColorBelowWater()的结果,透明度调整为1
首先我们定义了最简单的水下片元颜色计算,得到了石油一般的流体
那么要计算深度雾,首先我们需要一个摄像机深度贴图。
// in xxx.cginc
sampler2D _CameraDepthTexture;
另外,在着色器计算时,我们需要获取对应的屏幕空间坐标。
在surface shader里面加入screenPos:
struct Input
{
float2 uv_MainTex;
float4 screenPos;
};
…
void surf (Input IN, inout SurfaceOutputStandard o)
{
…
o.Albedo = ColorBelowWater(IN.screenPos);
o.Alpha = 1;
}
在unlit shader里面,我们可以在片元着色器中增加VPOS语义,实现平面空间坐标的引入。
由于VPOS和SV_POSITION无法在同一个v2f结构中存在,所以我们必须删去原有的v2f中的SV_POSITION,并使之在顶点着色器的参数中通过out语义单独输出。
struct v2f
{
float2 uv : TEXCOORD0;
//......
// float4 vertex : SV_POSITION;
};
v2f vert (appdata_tan v, out float4 vertex : SV_POSITION)
{
v2f o;
vertex = UnityObjectToClipPos(v.vertex);
//.......
return o;
}
fixed4 frag (v2f i, UNITY_VPOS_TYPE screenPos : VPOS) : SV_Target
{
//....
}
同样的,我们也可以直接在进行计算,那样就无需使用VPOS,也不需要删去v2f结构体中的SV_POSITION定义。
v2f vert (appdata_tan v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.screenpos = ComputeScreenPos(o.vertex);
//.......
}
2.1 获取水下片元到水面的距离
逻辑不难,即是通过水底的片元深度-水面的片元深度,求解对应片元水体的厚度。
float3 ColorBelowWater (float4 screenPos)
{
float2 uv = screenPos.xy / screenPos.w;
float backgroundDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv));
float surfaceDepth = UNITY_Z_0_FAR_FROM_CLIPSPACE(screenPos.z);
float depthDifference = backgroundDepth - surfaceDepth;
//除以二十,拉开层次差别,这个20的常量,我们可以理解为最大深度
//所有常量,最好都根据实际搭的场景深度,进行灵活调整
return depthDifference/20;
}
注意这里片元着色器的返回值换成了纯ColorBelowWater() 的结果。
若此时我们得到了黑白颠倒的结果,则可能是深度贴图的v坐标是从上到下计算的。对这种情况,我们需要对v维度的uv进行取反。
//in xxxx.cginc
flaot4 _CameraDepthTexture_TexelSize;
float3 ColorBelowWater (float4 screenPos)
{
float2 uv = screenPos.xy / screenPos.w;
#if UNITY_UV_STARTS_AT_TOP
if (_CameraDepthTexture_TexelSize.y < 0) {
uv.y = 1 - uv.y;
}
#endif
//.........
}
2.2 获取水底渲染帧缓冲
解决了深度信息的计算,新的问题又来了,我们直接把深度信息乘算已有的结果的话,无法正确反映水下的颜色信息。
//in frag shader
return fixed4((col * _BaseColor + diffuse + specular)* ColorBelowWater(i.screenpos), _AlphaScale);
当然我们可以自作聪明地去调整alpha值来稀释黑色效果,但随之而来又会直接破坏深度效果。
所以我们需要将原本的水下渲染的颜色结果,和深度结算的结果进行差值混合。
因为原本的水体shader计算的只有水面的颜色结果,所以我们铁定是不可能在单个pass里面完成混合了。
我们单独增加一个GrabPass ,提前存储其他物体渲染的结果。由于透明物体渲染顺序本身就在非透明物体后,如果想面向非透明物体获取GrabPass,要注意渲染顺序问题。
根据unity文档的描述,grabpass只能抓取帧缓冲信息,拥有两种调用方法。在不提供目标贴图时,结果会被存储到_GrabTexture;而用户需要指定grabpass输出的暂存贴图时,需要通过双引号给出。
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
LOD 100
//增加一个GrabPass,将背景物体渲染的结果预先存储到_WaterBackground中,供后续颜色插值混合使用
GrabPass {"_WaterBackground"}
Pass
{
//水体渲染pass
}
}
//in xxx.cginc
float3 ColorBelowWater (float4 screenPos)
{
float2 uv = screenPos.xy / screenPos.w;
#if UNITY_UV_STARTS_AT_TOP
if (_CameraDepthTexture_TexelSize.y < 0) {
uv.y = 1 - uv.y;
}
#endif
float3 backgroundColor = tex2D(_WaterBackground, uv).rgb;
return backgroundColor;
}
可以看到在满alpha的情况下,也有了透视的效果。
2.3 完成背景渲染和水深的插值混合
在属性中新增两个雾效相关的参数:
_WaterFogColor ("Water Fog Color", Color) = (0, 0, 0, 0)
_WaterFogDensity ("Water Fog Density", Range(0, 2)) = 0.1
根据水底颜色(雾颜色),背景颜色进行差值混合,插值因子是雾浓度和深度的结合。
//in xxx.cginc
// update ColorBelowWater( )
float3 backgroundColor = tex2D(_WaterBackground, uv).rgb;
float fogFactor = exp2(-_WaterFogDensity * depthDifference);
return lerp(_WaterFogColor, backgroundColor, fogFactor);
现在,我们可以通过_WaterFogDensity 来控制水的散射效果,实现水体的深度差别。
然后对alphascale值做调整,显然现在我们不需要一个控制输出颜色透明度的参数,而是需要一个控制水体是否为透明深度渲染的参数。
原本的alphascale主要用于控制片元着色器颜色计算结果的透明度。
现在我们将其调整,让其影响fogFactor最终的计算结果,并使片元着色器的返回的w值固定为1
//in .cginc ColorBelowWater
return lerp(_WaterFogColor, backgroundColor, fogFactor * _AlphaScale);
与调整_WaterFogDensity 的效果不同,调节_AlphaScale主要影响是否进行深度混合效果的计算。
3.实现水下物体的扭曲
有生活经验的朋友们都知道,水下的物体在有水波时,会出现扭曲现象,参考下图中水下鱼类的边缘,随着水波出现了一定程度的扭曲。
实现的逻辑不复杂,即是让水下的部分的采样uv,沿水波的法线方向(x,z方向)做偏移即可。
//额外新增了参数_RefractionStrength,用于控制采样水下颜色结果的uv的偏移程度
float3 ColorBelowWater (float4 screenPos, float3 worldNormal)
{
float2 uvoffset = worldNormal.xz * _RefractionStrength;
float2 uv = (screenPos.xy + uvoffset) / screenPos.w;
//.........
}
水下扭曲现象从无到有的对比
3.1 修正水上物体的误偏移
看上面的代码就知道,由于uv偏移是全局性的计算,所以会导致很多明明没有位于水下的物体,其对应的水面也产生了扭曲颜色的情况。
修正的方法很简单,就是我们经判断后,只对水下的片元做uv偏移即可,对于水上的uv,则沿用原本的uv,并注意需要重算depthDifference。
float3 ColorBelowWater (float4 screenPos, float3 worldNormal)
{
//......新增一个originUV,用于存储偏移前的uv
if(depthDifference < 0)
{
uv = originUV;
#if UNITY_UV_STARTS_AT_TOP
if (_CameraDepthTexture_TexelSize.y < 0) {
uv.y = 1 - uv.y;
}
#endif
//使用偏移前的uv采样颜色缓冲,会导致偏移后的uv采样的深度差,与颜色不匹配
//这里同时重采样backgroundDepth,再算一次depthDifference
backgroundDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv));
depthDifference = backgroundDepth - surfaceDepth;
}
float3 backgroundColor = tex2D(_WaterBackground, uv).rgb;
float fogFactor = exp2(-_WaterFogDensity * depthDifference);
return lerp(_WaterFogColor, backgroundColor, fogFactor * _AlphaScale);
}
4.实现水面波动
调整好水下折射效果后,我们仍然觉得水面的效果明明如此的波涛汹涌,但是水平面仍然明镜止水般镇定,显然不太符合常识。
最后在前面的渲染基础上,我们在顶点着色器内对flowmap进行采样,求解其向量长度,并用于太高vertex的位置(y方向)。
当然,这里使用的是100x100的plane,此外,在顶点着色器无法使用tex2D进行贴图采样,我们需要使用tex2Dlod来采样lowmap,并提供四位浮点数的uv。
我们给uv的第三,第四位置为0。
文章来源:https://www.toymoban.com/news/detail-839835.html
v2f vert (appdata_tan v)
{
v2f o;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
float2 flowVec;
flowVec = tex2Dlod(_FlowMap, float4(o.uv + _Time.y * _Speed, 0.0, 0.0)).rg;
flowVec = flowVec * 2 -1;
o.vertex = UnityObjectToClipPos(v.vertex + float3(0.0, length(flowVec) * _HeightScale, 0.0));
o.screenpos = ComputeScreenPos(o.vertex);
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
float3 worldBiTangent = cross(worldNormal, worldTangent) * v.tangent.w;
o.t2w_0 = float4(worldTangent.x,worldBiTangent.x,worldNormal.x, worldPos.x);
o.t2w_1 = float4(worldTangent.y,worldBiTangent.y,worldNormal.y, worldPos.y);
o.t2w_2 = float4(worldTangent.z,worldBiTangent.z,worldNormal.z, worldPos.z);
return o;
}
这样水体与浸没水体的物体边缘的交互会随着时间产生一定波动,能够显得更加真实一些。
文章来源地址https://www.toymoban.com/news/detail-839835.html
到了这里,关于【unity shader】水体渲染基础-水下透视效果的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!