【Unity Shader】Plane实现风格化水

2023-10-21 02:20

本文主要是介绍【Unity Shader】Plane实现风格化水,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

写在前面

长文警告!!!!!

很久没更新博客了,,这次是要做一个风格化水效果,是基于Plane着色实现水面效果。

项目:Unity 2017.4.40f1 Build-in,因此实现过程会跟URP有些出入(例如获取相机深度图等等),但思路都是一样的。


前期准备

效果拆解

以《RIME》

和《原神》为例:

想实现的是二者融合的感觉,总结一下包含的基本效果:

  • 随着深浅变化的水颜色:浅水域湖蓝色,深水域天蓝色
  • 水面反射:反射天空盒
  • 水面折射:即折射带来的扭曲效果,类似上面RIME第一张图里那种水底的扭曲
  • 水表面波纹
  • 水底扰动:浅水域水底会有扰动效果?
  • 岸边的浮沫:《原神》没有岸边浮沫,那就参考RIME的来

实现一个基本的水效果之外,有时间的话还会加上人物和水的交互涟漪效果。

模型准备

由于时间原因,先Plane搭建最简单的沙滩+水面,沙滩给个纹理:

渲染路径

还是选择前向渲染。

为什么要在这提一句路径呢?因为看了挺多关于水渲染的文章,很多人是在日本大佬那篇文章的基础上进行完善的,而他Camera的Rendering Path设置的是延迟渲染,Gbuffer的话根本不需要考虑深度问题。所以Shader也没有设置Queue

如果选择前向渲染,一定要注意在Shader里规范好渲染顺序:

不然会出现这样的错误:

另外出现错误擅用Frame Debugger,能方便快捷的找到错误点,例如可以从这里发现,水和沙滩是同时考虑成Opaque被渲染的:

补充队列后就正常了:

emmmm,果然多练效果能加深理解,,

简述计算深度的流程

既然讨论到depth,首先有必要搞清楚depth在渲染管线中哪一环节起作用——光栅化阶段,GPU会根据上一阶段(屏幕映射后,传递屏幕坐标系下的顶点位置和一些深度、法线等信息)传递过来的当前像素在每个Mesh(三角形)上对应的深度值,去判断当前像素位置到底显示那个Mesh(三角形)信息。

像这次“获取相机看到的深度”、还有之前实现扫描效果需要实现的“基于深度重建世界坐标扫描”这类需求,都需要经过相同的一套流程获取深度值,再基于深度值再去做进一步操作,这套流程大概是:

获取相机深度图  -> 采样深度图 -> 深度转线性变化(我们希望实现随着深度变化)

终于有机会理一遍:

*获取相机DepthTexture

Build-in管线下,我们需要告诉Camera需要获取深度图——要挂脚本给相机开启相机深度(这是别的文章里看来的,但我后面实现没有挂脚本也能拿到深度图?):

GetComponent<Camera>().depthTextureMode = DepthTextureMode.Depth;

或者参考《入门精要》的:

void OnEnable() {Camera.main.depthTextureMode |= DepthTextureMode.Depth;
}

 开启后,可以FrameDebugger一下,会发现流程中已经加入了获取深度纹理的Pass:

其中,DepthTexture是在ShadowCaster Pass中被渲染的,由于我这里场景中所有物体给了默认Shader,Shader中默认Fallback"Duffuse"就包含了ShadowCaster这一Pass。

Shader想使用的话,直接声明Unity给的全局变量_CameraDepthTexture就可以用了:

sampler2D _CameraDepthTexture;

有时会发现有人这样定义: 

UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture); // 声明深度纹理

这是Unity提供的另一些变量,一步一步涉及到的变量整理如下,追溯到最后实际上干的事情跟简单的sampler2D一样:

#define UNITY_DECLARE_DEPTH_TEXTURE(tex) UNITY_DECLARE_TEX2DARRAY (tex)...// 2D array syntax for hlsl2glsl and surface shader analysis#if defined(UNITY_COMPILER_HLSL2GLSL) || defined(SHADER_TARGET_SURFACE_ANALYSIS)#define UNITY_DECLARE_TEX2DARRAY(tex) sampler2DArray tex...// surface shader analysis; just pretend that 2D arrays are cubemaps#if defined(SHADER_TARGET_SURFACE_ANALYSIS)#define sampler2DArray samplerCUBE

采样深度图

需要在顶点shader里计算齐次坐标系下的屏幕坐标值,

o.positionCS = UnityObjectToClipPos(v.positionOS);
o.screenPos = ComputeScreenPos(o.positionCS);

其中ComputeScreenPos()是Unity Shader的内置函数,关于这个推导就不展开细说了,可以看这篇文章:Unity Shader中的ComputeScreenPos函数或者直接去看《入门精要》的4.9.3小节,讲得很详细。

此外,还需要转换成从人眼出发的深度(观察线性深度?就是Eye Depth),UnityCG.cginc中有给到计算的函数:

// Depth render texture helpers
#define DECODE_EYEDEPTH(i) LinearEyeDepth(i)
#define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z

就是计算出顶点在观察空间中的z分量,为后面做深度差做准备,取负是因为观察空间的z轴是翻转的。(另外,因为前面因为写shader习惯把vertex直接替换成positionCS,这里发现Unity定义COMPUTE_EYEDEPTH()的时候默认是直接取v.vertex的,意味着还是换成vertex比较好?还是改回来,,,):

COMPUTE_EYEDEPTH(o.screenPos.z); // 线性变化

*补充一点,实际上根据透视投影矩阵(正交投影就不是了,w恒为1),裁剪空间的zw和观察空间的zw一致,而裁剪空间的w实际上就是观察空间的-z,那这一步完全可以省略,后面赋值的时候直接取o.screenPos.w就行了.

接着在fragment shader里:

float depth = UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));

其中,UNITY_SAMPLER_DEPTH是在HLSLSupport.cginc中定义的,用以获取r通道储存的深度值:

// Deprecated; use SAMPLE_DEPTH_TEXTURE & SAMPLE_DEPTH_TEXTURE_PROJ instead
#if defined(SHADER_API_PSP2)
#   define UNITY_SAMPLE_DEPTH(value) (value).r
#else
#   define UNITY_SAMPLE_DEPTH(value) (value).r
#endif

所以上面那行代码直接可以写成这样:

float depth = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)).r;

此外,tex2Dproj定义如下,就是做了一个/.w的操作,事实上跟tex2D一模一样:

#if defined(SHADER_API_PSP2)// For tex2Dproj the PSP2 cg compiler doesn't like casting half3/4 to// float3/4 with swizzle (optimizer generates invalid assembly), so declare// explicit versions for half3/4half4 tex2Dproj(sampler2D s, in half3 t)        { return tex2D(s, t.xy / t.z); }half4 tex2Dproj(sampler2D s, in half4 t)        { return tex2D(s, t.xy / t.w); }

这样的话,以下两种方式道理是一样的:

// tex2Dproj
float depth = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)).r;// tex2D
float depth = tex2D(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos.xy/i.screenPos.w)).r;

最后还有个UNITY_PROJ_COORD,在HLSLSupport.cginc中定义如下:

#if defined(SHADER_API_PSP2)
#define UNITY_BUGGY_TEX2DPROJ4
#define UNITY_PROJ_COORD(a) (a).xyw
#else
#define UNITY_PROJ_COORD(a) a
#endif

感觉就是Unity根据平台API做了一些小规范? 

除了上面那个计算depth的方法,在其他人的文章里还有可能遇到如下采样定义:

float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos));

 其中,SAMPLE_DEPTH_TEXTURE_PROJ定义如下:

 #undef SAMPLE_DEPTH_TEXTURE_PROJ#define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) UNITY_SAMPLE_TEX2DARRAY(sampler, float3((uv).x/(uv).w, (uv).y/(uv).w, (float)unity_StereoEyeIndex)).r

做的工作都是一样的,只不过是Unity把各种各样的不同计算方式封装起来,方便我们去直接使用。

深度转线性变化

多数时候我们希望基于深度做的效果变化是均匀的,但事实上Depth Texture储存的深度值不是线性的,具体原因这里就不多说啦,可以参考【Unity】深度图(Depth Texture)的简单介绍,所以我们最后还需要一步:

depth = LinearEyeDepth(depth);

Unity在UnityCG.cginc中提供了把z-buffer里储存的值转变成线性变化深度的函数:

// Z buffer to linear 0..1 depth
inline float Linear01Depth( float z )
{return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
// Z buffer to linear depth
inline float LinearEyeDepth( float z )
{return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

那么一套下来写进Pass里就是:

		Pass {CGPROGRAM#pragma target 3.0#pragma multi_compile_fwdbase#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture); // 声明深度纹理struct appdata {float4 vertex : POSITION;};struct v2f {float4 vertex : SV_POSITION;float4 screenPos  : TEXCOORD1;};v2f vert (appdata v) {v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.worldPos = mul(unity_ObjectToWorld, v.vertex.xyz);o.screenPos = ComputeScreenPos(o.vertex); // 计算屏幕坐标COMPUTE_EYEDEPTH(o.screenPos.z);          // 线性变化return o;}fixed4 frag (v2f i) : SV_TARGET {float depth = UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));  // 采样纹理获得深度值depth = Linear01Depth(depth);  // 深度线性变化return fixed4(1,1,1,1);}ENDCG}

1 水深浅区域颜色

首先聊第一种方案:

深水区和浅水区水的颜色不一样,我们要明白,我们是用一个Plane着色去模拟水,所以我们是要给平面上的点上色,就要计算(plane上每个片元的深度值-场景深度值),大概画了画(图里红色那部分):

刚才计算的那个深度值是场景的深度值depth1,片元深度depth2实际上就是ScreenPos的z分量(不知道为什么的直接看《入门精要》4.9.3啦):

float depthFrag = i.screenPos.z;   // 当前水片元深度
float depth = saturate(depthFrag - depthScene); // 差值

虽然大部分基于深度做水颜色变化、浪花的文章都是用上面这个方法,但是这个方法深度值会随着相机视角的变化而变化,,总之会出一些很怪的效果,特别是后来实现浪花的时候,,效果异常的丑,而且状况百出,就像这样:

一切问题都出自深度不固定,那我们就让他固定!

计算都是比较基础的了,之前做扫描的时候就学习过一次,就不解释原理了,过程的话可以看这篇文章:Unity从深度缓冲重建世界空间位置=

这里就直接截取我的Shader:

				// 采样获取深度值float depthScene = UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));depthScene = LinearEyeDepth(depthScene); // 场景深度// 1.基于相机的深度差// float depthFrag = i.screenPos.z;   // 当前水片元深度// float depthZ = saturate((depthScene - depthFrag)/_DepthAtten); // 差值// 2.脱离相机的垂直深度差i.worldSpaceDir *= -depthScene / i.viewSpaceZ;float3 worldPosScene = _WorldSpaceCameraPos + i.worldSpaceDir; // 沿着向量float depthZ =  saturate((worldPos - worldPosScene).y / _DepthAtten);

1.1 他人方案

判断依据有了,接下来是上色环节,先来简单看看别人是怎么做水颜色的:

【Unity URP】风格化水体渲染 - 知乎 (zhihu.com)这篇文章水颜色是直接根据差值lerp:

Unity中水的简单实现 - 知乎 (zhihu.com) 这篇文章是用纹理来规定深浅度,采样后lerp颜色:

Unity Shader 水体渲染 - 知乎 (zhihu.com)这篇文章也是,采样了一个渐变纹理:

 最后是这一篇日本大佬的文章:【Unity , shader】原神の海を再現したい - Qiita,自定义cos渐变色函数!!!达到渐变色且不用纹理

​但是这个方法感觉对美术不太友好?类似于需要预先调整颜色给定公式,不能即时查看颜色,但是这个方案真的很吸引人!!自定义渐变色可太酷了,直接采样渐变纹理的话方法挺简单的,这里就直接尝试这位日本大佬的方案。

1.2 自定义渐变色

网站指路:grad - Cosine Gradient in Multiple Color Spaces (sp4ghet.github.io)

调一调,虽然看上去参数非常多,试了一下其实是能感觉到每个参数控制的是什么,让颜色尽量接近原神里的:

调好之后,会自动生成代码:

手动给他转成Cg/HLSL就行,我这里的话是转成Cg:

// 生成自定义渐变色函数float4 cosine_gradient(float x, float4 phase, float4 amp, float4 freq, float4 offset) {float TAU = 2 * 3.14159265;phase *= TAU;x *= TAU;return float4(offset.r + amp.r * 0.5 * cos(x * freq.r + phase.r) + 0.5,offset.g + amp.g * 0.5 * cos(x * freq.g + phase.g) + 0.5,offset.b + amp.b * 0.5 * cos(x * freq.b + phase.b) + 0.5,offset.a + amp.a * 0.5 * cos(x * freq.a + phase.a) + 0.5);}fixed3 toRGB(float3 grad) {return grad.rgb;}

然后fragment shader里加上,加了一个_ColorAtten控制颜色变化、_DepthAtten控制深浅程度变化:

			fixed4 frag (v2f i) : SV_TARGET {float depthScene = UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));depthScene = LinearEyeDepth(depthScene); // 场景深度float depthFrag = i.screenPos.z;   // 当前水片元深度float depthZ = saturate((depthScene - depthFrag)/_DepthAtten); // 差值// 生成的值const float4 phases = float4(0.28, 0.44, 0.00, 0.);const float4 amplitudes = float4(3.27, 0.14, 0.39, 0.);const float4 frequencies = float4(0.00, 0.67, 0.28, 0.);const float4 offsets = float4(0.04, 0.14, 0.14, 0.);fixed4 cos_grad = cosine_gradient(saturate(_ColorAtten - depthZ), phases, amplitudes, frequencies, offsets);cos_grad = clamp(cos_grad,0,1);fixed4 color = fixed4(toRGB(cos_grad),1);// 水越浅,越透明,刚好可以用depthZ来表示,值越小越浅float alpha = saturate(depthZ);color.a = alpha;return color;}

Alpha做了处理,随着深浅控制.a值。

1.3 效果

最后颜色效果(_ColorAtten=1.45,_DepthAtten=10):

2 水面波纹-处理法线

2.1 拿法线纹理

处理完颜色,开始给水波了。正常想法是给个噪声贴图实现。尝试RenderDoc+MuMu模拟器抓帧,大概找了白天晚上两个一样的地方,水渲染的Pass都出现了这两张法线纹理:

那就拿这两张法线纹理叠加做出来的水波效果。因为不知道为什么模拟器只能选Vulkan或DX,用DX连RenderDoc会崩,Vulkand的shader又看不懂,所以目前只能做到拿到法线纹理了(悲)

2.2 两次采样叠加

那就开始,基于上面法线纹理,_WaveSpeed控制两次UV采样的程度,两张纹理效果叠加:

// 两套UVo.normalUV1.xy = o.uv + float2(_Time.x * _WaveSpeed.x, _Time.x * _WaveSpeed.y);o.normalUV1.zw = o.uv + float2(_Time.x * _WaveSpeed.z, _Time.x * _WaveSpeed.w);

然后就做正常的世界空间法线变换+叠加两套UV采样效果:

// 采样法线贴图,两次叠加float3 normal = UnpackNormal(tex2D(_NormalTex0, i.normalUV1.xy))*0.5 + UnpackNormal(tex2D(_NormalTex1, i.normalUV1.zw))*0.5;normal.xy *= _NormalScale;normal.z = sqrt(1.0 - saturate(dot(normal.xy, normal.xy)));float3 worldNormal = normalize(float3(dot(i.TtoW0.xyz, normal), dot(i.TtoW1.xyz, normal), dot(i.TtoW2.xyz, normal)));

做到这里实际上只是提供了一个被扰动的法线,真实的体现出波浪的效果还需要结合光照计算,而且后期需要调整,因为考虑到原神本身海面并没有扰动那么明显,后面一定会做相应的调整。

*不加顶点动画更省事的方案

做的过程中突然看到有篇文章评论区提出了这样一种方法:

啊啊啊!确实!既然水面一直是平面,且没有顶点动画:那为什么还要做复杂的法线计算?毕竟算来算去Plane上顶点的法线方向始终朝向y轴正方向,即float3(0,1,0),直接采样法线纹理扭曲这个方向就行了!!

意味着,这个:

float3 worldPos = mul(unity_ObjectToWorld, v.vertex.xyz);float3 worldNormal = UnityObjectToWorldNormal(v.normal).xyz;float3 worldTangent = UnityObjectToWorldDir(v.tangent).xyz;float3 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);

和这个:

float3 worldPos = mul(unity_ObjectToWorld, v.vertex.xyz);float3 worldNormal = float3(0,1,0);float3 worldTangent = float3(-1,0,0);float3 worldBinormal = float3(0,0,-1);o.TtoW0 = float4(-1, 0, 0, worldPos.x);o.TtoW1 = float4(0, 0, 1, worldPos.y);o.TtoW2 = float4(0, -1, 0, worldPos.z);

 是完全等价的!那就不需要算这么多了,,,直接在片元里计算就行。但是,我的水后面可能需要加顶点动画,所以这里就先不做简化!

3 基础光照

我们再回到效果初衷——无论是《RIME》里的还是《原神》里的水,水面高光都不至于完全复刻真实水体的那种波光粼粼,例如Unity只在一个面片上实现真实水体渲染 - 知乎 (zhihu.com)这篇文章最后实现的效果:

所以光照计算也不至于PBR,先考虑Diffuse

3.1 基础色+高光

高光specular考虑成Blinn-Phong高光项,

fixed3 specular = _SpecularColor.rgb * _SpecularAtten * pow(ndoth, _Gloss);

输出diffuse+specular,这里加上了一个CubeMap天空盒,所以光源在天空上没显示了:

3.2 静态的:反射天空盒+菲涅尔

目前我了解到的反射方案有:CubeMap、Reflection Probe、PlanarReflection、ScreenSpaceReflection(SSR)、还有SSR+PlanarRelflection,这里先实现一个最简单直接的CubeMap:

// 反射天空盒
float3 reflectDir = reflect(-viewDir, worldNormal);
fixed3 reflecColor = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, reflectDir);

单独输出的话:

啊肯定不能这样,,还要考虑菲涅尔的——接近天空的部分才反射的很明显,靠近实现的地方几乎没什么反射,能看到水底,这里用最基础的Fresnel:

// 菲涅尔 Fresnel-Schlickinline float3 Unity_Fresnel(float3 F0, float cosA){float a = pow((1 - cosA), 5);return (F0 + (1 - F0) * a);}

片元shader里加上:

// 菲涅尔项
float F0 = 0.02;
float F = saturate(Unity_Fresnel(F0,dot(viewDir, worldNormal)) * _FresnelAtten);...fixed3 color = lerp(diffuse + specular , reflecColor, F);

下面是考虑菲涅尔和不考虑的对比:

效果不错,但很遗憾,,CubeMap始终是一种静态的反射方案,如果天空盒保持不动还好说,但像原神这种水面是需要跟动态天空盒配合实现反射的。而且水面上石头啊、人物走进的倒影也是没办法呈现出来的,所以秉持着做东西要能投入实际使用的原则,只有CubeMap方案的水体反射是不完整的。

4 动态的:反射探针+平面反射

4.1 实现基础平面反射

上面提到的那几个方法中,SSR是基于屏幕空间的,需要拿到法线+深度图,对于前向渲染性价比比较低,,平面反射虽然也很耗,但对于移动端相比SSR更好?(待验证),先来实践一下:

概述一下实现过程,

我们需要新建一个相机ReflectCamera,让该相机相对于主相机MainCamera关于水面(xz平面)对称,将渲染结果输出到一张RT,当我们渲染水面Plane上一点的时候,直接到这张RT上采样。采样也不是用原来的uv了,不理解的话可以像我这样假想一下:RT的内容就是水里面倒影的真实样子,也就是我们是要按RT原原本本应该呈现在屏幕上的样子,我们要做的是给他原封不动拿过来贴在我们MainCamera的画面里,所以采样RT要拿屏幕坐标采样

实现的脚本主体参考:Unity Shader-反射效果,另外原作者基于平面斜截反射相机的视锥体那部分我用起来有些问题,直接用Unity的API效果是正确的:

用作者的脚本会有问题:

找不出问题在哪儿,我就直接选择用Unity提供的APICalculateObliqueMatrix(clipPlane)计算出斜裁剪矩阵:

// 平面法线朝向var normal = transform.up;// 求与平面的倒影距离var d = -Vector3.Dot(normal, transform.position);// 平面到点距离var plane = new Vector4(normal.x, normal.y, normal.z, d);// 用逆转置矩阵将平面从世界空间变换到反射相机空间var viewSpacePlane = reflectionCamera.worldToCameraMatrix.inverse.transpose * plane;// 做斜视锥体投影矩阵var clipMatrix = Camera.current.CalculateObliqueMatrix(viewSpacePlane);reflectionCamera.projectionMatrix = clipMatrix;

然后在我们的Shader中加入:

// 平面反射
fixed4 reflectColor1 = tex2Dproj(_ReflectionTex, UNITY_PROJ_COORD(i.screenPos));
fixed3 color = lerp(diffuse + specular , reflectColor1, F);

基本的平面反射是完成了,可以看到有天空+水面物体的影子:

但是,,,之前的做的很好看的水面波纹效果消失了,因为worldNormal根本没有用上,效果——太怪了!


4.2 再次分析反射思路

新的一天!继续完善反射部分。这里让我们再回看一下《原神》画面,希望确定一下最终的反射效果。

可以发现《原神》对远处天空盒的反射处理(绿色框框)和对静处场景中静态物体的反射处理(红色框框)不同,红色框框处理的很尖锐,绿色框框就很柔和:

远处的天空柔和,相对静处的物体尖锐,也是挺合理的?我们再拿同一视角下,3种不同天空颜色和云层变化的水面反射效果对比看看:

远处由于菲涅尔会直接完全反射出天空的颜色,静处是绿绿的水体本身的颜色,远处画面还有一定的雾效加持。对了,我们还要需要明确一点,《原神》是延迟渲染,所以反射很可能直接基于SSR做?(由于逆向一时半会儿也看不到shader所以只能初步假设了)

基于此再回到我们实现的效果上,想要实现的是:菲涅尔(完成)+水体颜色(完成)+天空盒反射(待)+场景其他物体倒影(待),那么我们可以采取:反射探针CubeMap实现动态天空盒反射+平面反射实现场景物体水面倒影反射,需要以某种手段剔除掉天空盒的反射。

4.3 反射探针 动态CubeMap

我们在之前CubeMap基础上,在场景如图位置加入反射探针,并调整影响范围:

Culling Mask选择Nothing,只反射天空盒:

避免出现这种把场景中其他物体倒影也包括的情况:

加上菲涅尔+法线扰动,看看效果:

水面那道高光高光是太阳的,但是天空盒没有做程序化太阳所以天空盒看不到太阳...旋转CubeMap可以看到反射效果是实时更新的,太麻烦这里就不演示了。

4.4 平面反射 剔除掉天空盒

下一步就是加上4.1实现的平面反射的同时,把CubeMap的部分剔除掉。这个办法我尝试了很多效果都欠佳,直到看到了这篇文章Unity制作仿原神水面(2)——反射、白浪,这篇文章作者也遇到我的问题,只不过他没给CubeMap做动态处理。他也注意到了物体倒影很尖锐这一点:

但是他做的海水扰动不是采样法线纹理,而是自定义了一个噪声函数,这里我选择复用之前的法线纹理某一通道作为噪声去扰动ScreenPos:

// 平面反射// 加入扰动i.screenPos.x += normal00.x*5*depthZ;fixed4 reflectColor1 = tex2Dproj(_ReflectionTex, UNITY_PROJ_COORD(i.screenPos));

放个灰色方块模拟场景中的山体,越离的近的波浪效果越弱。

接下来就是融合CubeMap和平面反射的效果了,如果只是简单的透明度剔除:

			// 剔除天空盒// col1天空色 col2 反射物体色fixed4 blendSeaColor(fixed4 col1,fixed4 col2){fixed4 col = col2 * col2.a + col1 * min(1,1.2 - col2.a);return col;}
fixed3 color = lerp(diffuse+ specular, blendSeaColor(reflectColor0,reflectColor1), F);

融合后的效果:

物体和云的扰动不同,物体的幅度更大,动态效果就不展示了。  

5 岸边浪花

感觉原神画面上具备的都有了?

参考一下RIME做一个实线浪花的效果:

做法感觉蛮简单的,肯定有采样贴图,之前不知道抓帧《原神》的哪个场景存了个这个图:

说起来《原神》里面水应该没有做浪花才对,,可能是其他的特效图吧,,长得挺像浪花?又有点像焦散效果贴图,可以先用它做浪花试试看?

				// 岸边海浪i.uv.y += _Time.x * _FoamSpeed ;i.uv.x += _SinTime.x * 0.04;fixed4 foamTex = tex2D(_FoamTex, i.uv.xy);float foamAlpha = ((foamTex.r  + foamTex.g)* depthZ); // 深度值加入透明度影响float boarder = step(depthZ, _FoamBorder); // _Border控制浪花显示范围fixed3 foamColor = smoothstep(0.5,0.7,foamAlpha*boarder) *_SpecularColor; // smoothstep控制

效果:

Shader栏:

这是一种非常基础的浪花实现效果,是十分依赖纹理的,效果也比较单一。事实上我更欣赏程序化生成浪花的方案,例如这篇文章Unity仿《原神》水渲染中提到的How to create a semi procedural cartoon foam shader (gamedeveloper.com)

由于时间问题,后面会抽时间完善这部分内容。


更新,如果是曲边的岸边,可以不是那种长长的线条,通过调整Tilling数值拿到想要的浪花形状:

还是那句话,这种基于纹理的简单浪花效果非常吃岸边的地形形状。

6 浅水域焦散效果

首先能想到的实现焦散的方法是采样一张贴图?做一下,Water Caustics Effect (Small) | OpenGameArt.org拿纹理:

因为想实现浅水域焦散,而且是水下,所以要用上面重建出来的世界坐标做焦散,,

// 焦散float2 causticUV = worldPosScene.xz * _CausticTex_ST.xy * (1 - _CausticSize) *10 + _CausticTex_ST.zw;float4 causticColor = tex2D(_CausticTex, float2(-causticUV.y + _CausticSpeed *0.1*sin(_Time.y), causticUV.x + _Time.x * _CausticSpeed* normal00.x* 0.01))*_CausticColor;color = lerp(color + causticColor, color, depthZ); // 加上浅水域焦散效果

效果:

连着做了3天,实在是有些疲惫了,,第一版就先这样。

还有很多效果没补充,后面会继续完善。 

参考

unity反射效果:移动端镜面反射,屏幕空间镜面反射实践 - 知乎 (zhihu.com)

Unity仿《原神》水渲染 - 知乎 (zhihu.com)

3D渲染技术分享:实时水面渲染方案(反射、折射、水深与水岸柔边) - 知乎 (zhihu.com)

Unity Shader-反射效果(CubeMap,Reflection Probe,Planar Reflection,Screen Space Reflection)

【Unity】深度图(Depth Texture)的简单介绍 - 知乎 (zhihu.com)

【Unity , shader】原神の海を再現したい - Qiita

Unity只在一个面片上实现真实水体渲染 - 知乎 (zhihu.com)

Unity制作仿原神水面(2)——反射、白浪 - 知乎 (zhihu.com)

Believable Caustics Reflections - Alan Zucconi

这篇关于【Unity Shader】Plane实现风格化水的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/251270

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

基于51单片机的自动转向修复系统的设计与实现

文章目录 前言资料获取设计介绍功能介绍设计清单具体实现截图参考文献设计获取 前言 💗博主介绍:✌全网粉丝10W+,CSDN特邀作者、博客专家、CSDN新星计划导师,一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设计 主要对象是咱们电子相关专业的大学生,希望您们都共创辉煌!✌💗 👇🏻 精彩专栏 推荐订阅👇🏻 单片机