【Unity Shader】Plane实现风格化水

news2024/11/29 5:51:27

写在前面

长文警告!!!!!

很久没更新博客了,,这次是要做一个风格化水效果,是基于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/4
    half4 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采样的程度,两张纹理效果叠加:

// 两套UV
				o.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-Schlick
			inline 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)

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/590066.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ChatGPT vs Google:谁是答案之王?微软揭示未来搜索的样子

已经习惯了网购的你&#xff0c;有没有想过一旦电子商务行业遭受重创&#xff0c;会对我们的日常生活造成什么影响呢&#xff1f; 我们已经习惯了在生活中碰到什么不懂的事情就 Google 一下或者百度一下&#xff0c;甚至许多人的职业都跟 Google 密切相关&#xff0c;比如自媒…

从外包到转岗华为正式员工月薪17K,这一百多天的心酸谁能懂.....

本人毕业于某普通二本院校非计算机专业&#xff0c;跨专业入行测试&#xff0c;至今有近 5年工作经验。 第一份测试工作是在华为做了两年外包。总体感受就是 这份工作缺乏归属感&#xff0c;心里总有一种落差&#xff0c;进步空间不大&#xff0c; 接触不到核心技术&#xf…

跨端框架+小程序容器:一种让App开发提高效率的思路

WePY&#xff08;微信小程序开发框架&#xff09;是一个基于组件化开发思想的微信小程序开发框架。它类似于Vue.js框架&#xff0c;通过封装小程序原生的API&#xff0c;提供了更加简洁、高效的开发方式。 WePY的主要特点包括&#xff1a; 组件化开发&#xff1a;WePY将页面拆…

matplotlib常用功能汇总

文章目录 1.IDE里的1.1 显示模式&#xff08;plt.ion()和plt.ioff()&#xff09;1.2 backend说明 2 jupyter里的3 通用的3.1 cmap3.2 subplot()相关3.3 绘制动态图&#xff08;Animation类&#xff09;3.4 matplotlib利用rcParams配置样式参数 4. 与opencv连用可能遭遇的问题1.…

MySQL_8 相当牛逼的索引机制

目录 一、索引机制的引入 1.索引机制&#x1f402;B在哪里&#xff1f; 2.索引机制提高查询速度的原理 : 二、索引的创建 1.索引分类 : 2.使用格式 : 3.代码演示 : 三、索引的删除 1.格式 : 2.演示 : 四、索引的查询 1.格式 : 2.演示 : 五、索引的使用规则 一、索…

Linux---强制停止、退出登出、history、yum

1. ctrl c 强制停止 Linux某些程序的运行&#xff0c;如果想要强制停止它&#xff0c;可以使用快捷键ctrl c&#xff1a; [shaonianlocalhost ~]$ tail ^C [shaonianlocalhost ~]$ 命令输入错误&#xff0c;也可以通过快捷键ctrl c&#xff0c;退出当前输入&#xff0c;重…

如何在 Alpine Linux 上启用或禁用防火墙?

防火墙是计算机网络安全的重要组成部分&#xff0c;它用于保护计算机和网络免受未经授权的访问和恶意攻击。Alpine Linux 是一种轻量级的 Linux 发行版&#xff0c;常用于构建容器化应用和嵌入式系统。本文将详细介绍如何在 Alpine Linux 上启用或禁用防火墙。步骤 1&#xff1…

MySQL复习

文章目录 1、操作数据库1.1、操作数据库1.2、数据库的数据类型1.3、数据库的字段属性1.4、创建数据表1.5、MyISAM和InnoDB1.6、修改删除表 2、MySQL数据管理2.1、外键2.2、DML语言&#xff08;全部记住&#xff09;2.3、添加2.4、修改2.5、删除 3、DQL查询数据3.1、DQL3.2、查询…

EMPIRE: LUPINONE实战演练

文章目录 EMPIRE: LUPINONE实战演练一、前期准备1、相关信息 二、信息收集1、端口扫描2、访问网站3、查看源码4、dirsearch扫描目录5、访问robots文件6、访问myfiles文件7、模糊测试8、访问秘密文件9、查找秘钥10、查看秘钥11、解码12、解密13、远程连接 三、提权1、查找flag2、…

Unity---委托与事件

目录 1.委托和事件在使用上的区别是什么&#xff1f; 2. delegate委托 2.1示意图 2.2 DelegetTest.cs 2.3 Deleget_A.cs 2.4 Deleget_B.cs 2.5 运行unity. 点击按键 A 2.6 点击按键 B 3.Event 事件 3.1单个通知 3.1.1示意图 3.1.2 Event_Test.cs 3.1.3 Event_A.cs 3…

专家警告AI可能会导致人类灭绝

人工智能可能导致人类灭绝&#xff0c;包括 OpenAI 和 Google Deepmind 负责人在内的专家警告说 数十人支持在人工智能安全中心 的网页上发表的声明。 它写道&#xff1a;“减轻人工智能灭绝的风险应该与其他社会规模的风险&#xff08;如流行病和核战争&#xff09;一起成为全…

Spring(二)获取bean和依赖注入

一、获取bean的三种方式&#xff1a; 1.根据bean的id获取&#xff1a; Student studentOne (Student) ioc.getBean("studentOne"); 2.获取bean所需要的类型的class对象&#xff1a; Student student ioc.getBean(Student.class); 我们运行之后如下所示&#xff1…

为kong网关添加限流插件

限流用于控制发送到上游服务的请求速率。 它可用于防止 DoS 攻击、限制网络抓取和其他形式的过度使用。 如果没有速率限制&#xff0c;客户可以无限制地访问您的上游服务&#xff0c;这可能会对可用性产生负面影响。 一、全局范围内的限流 1、启用限流 [rootmin ~]# curl -i…

AI落地:儿童节贺卡

昨天有个朋友Lisa找到我&#xff0c;她是幼儿园的老师&#xff0c;看到我最近搞了个爱落地星球&#xff0c;在研究各行各业AI落地的事情&#xff0c;问我能不能用AI帮她写一百多张贺卡。 说起来写贺卡&#xff0c;我只会写“节日快乐”。现在有了ChatGPT&#xff0c;那就大不一…

十六、多线程(中)

文章目录 一、线程互斥&#xff08;一&#xff09;四个概念1.临界资源2.临界区3.互斥特性4.线程互斥5.原子性 二、互斥&#xff08;一&#xff09;在执行语句的任何地方&#xff0c;线程可能被切换走&#xff08;二&#xff09;切换会保存上下文&#xff08;三&#xff09;抢票…

用HTML、CSS和JavaScript实现鼠标可交互的3D太阳和月亮切换效果

部分数据来源&#xff1a;ChatGPT 引言 太阳和月亮对于我们来说是一种常见的对比&#xff0c;这篇文章将介绍一个使用HTML、CSS和JavaScript创建的网页场景&#xff0c;能够把太阳和月亮切换展示给用户。这个场景能够让用户使用鼠标和滚轮与场景互动&#xff0c;带来更多的趣…

解锁Qt QListWidget的全部潜力——用最佳实践和技巧赢得用户的喜爱和赞誉!

文章目录 前言一、属性和方法添加列表项获取当前选中的列表项删除列表项列表显示模式交替背景色 二、信号与槽选中的行数变化item被点击 三、解决icon图标模式下图标不对称的问题1、设置属性2、面向结果的手动换行 总结 前言 在现代的GUI应用程序中&#xff0c;列表框是必不可…

什么是千兆光模块和万兆光模块?它们有什么区别?

众所周知千兆光模块和万兆光模块的主区别在于它们的传输速率不一样&#xff0c;那你还知道千兆光模块和万兆光模块的其他区别吗&#xff1f;接下来海翎光电的小编将对千兆光模块和万兆光模块的区别进行详细解析。 什么是千兆光模块&#xff1f; 千兆光模块即传输速率为1000Mbps…

Java之路:构建坚实基础,系统学习Java技术的终极指南

无论是初学者还是有经验的专业人士&#xff0c;在学习一门新的IT技术时&#xff0c;都需要采取一种系统性的学习方法。作为一名Java技术er&#xff0c;下面我将介绍我是如何系统的学习Java技术的。 一、Java技术介绍 Java是一种广泛应用于软件开发的高级编程语言&#xff0c;…

数据链路层:点对点协议PPP

数据链路层&#xff1a;点对点协议PPP 笔记来源&#xff1a; 湖科大教书匠&#xff1a;点对点协议PPP 声明&#xff1a;该学习笔记来自湖科大教书匠&#xff0c;笔记仅做学习参考 数据链路层只负责直接相连的两个结点之间的通信 PPP是点对点数据链路层协议 用户通过ISP接入因特…