unity制作仿原神水面(1)——上色、造浪

文章参考自: https://qiita.com/uynet/items/f8b087d47f5cf316eb7e

最终效果图:

这是一位日本友人的教程,按照他的方法制作出的水面虽然有着一些缺点,但是效果还是很出色的。

场景部分:

我们首先新建两个plane,然后让它们之间呈现一定的角度,这样我们就拥有了一片沙滩和一个水面。

Shader部分:

定义结构体

struct appdata
        {
            float4 vertex : POSITION;                           
        };

        struct v2f
        {
            UNITY_FOG_COORDS(3)
            float4 vertex : SV_POSITION;
            float4 screenPos : TEXCOORD1;
            float3 worldPos : TEXCOORD2;
        };

首先要为水体上色,这里使用了自定义的使用cos的渐变色函数,作者使用了网站:https://sp4ghet.github.io/grad/ 来生成

fixed4 cosine_gradient(float x,  fixed4 phase, fixed4 amp, fixed4 freq, fixed4 offset){
            const float TAU = 2. * 3.14159265;
            phase *= TAU;
            x *= TAU;

            return fixed4(
                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(fixed3 grad){
             return grad.rgb;
        }

网站页面截图(通过手动调整各通道内各个参数可以生成不同的渐变色代码,代码为GLSL,我们简单调整为CG即可使用)

接下来我们依据相机深度值为水体上色,来形成从浅水到深水的绿色到蓝色的渐变

片元深度与场景中的深度差值为d2-d1

可以看到,水深由深到浅,我们要渲染的片元深度与场景中的深度差值将越来越小,将1减去这个数值作为我们刚刚渐变色函数cosine_gradient的x变量,就可以得到一个不错的渐变效果。代码如下: 首先我们需要开启相机深度,将以下脚本挂载在场景的摄像机上

using UnityEngine;

[ExecuteInEditMode]
public class DepthOn : MonoBehaviour {

    void Start ()
    {
        GetComponent<Camera>().depthTextureMode = DepthTextureMode.Depth;
    }
}

在顶点着色器中,我们需要在顶点着色器中计算顶点深度并使其线性变化

v2f vert (appdata v)
        {
            v2f o;
            ...
            o.vertex = UnityObjectToClipPos(v.vertex);              
            o.screenPos = ComputeScreenPos(o.vertex);       
            COMPUTE_EYEDEPTH(o.screenPos.z);
            return o;
        }

在shader中声明深度纹理

UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

在片元着色器中,我们来获取到片元深度与场景中的深度差值并计算渐变色

fixed4 frag (v2f i) : SV_Target
        {

            fixed4 col = (1,1,1,1);
            float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));
            float partZ = i.screenPos.z;
            float diffZ = saturate( (sceneZ - partZ)/5.0f);//片元深度与场景深度的差值
            const fixed4 phases = fixed4(0.28, 0.50, 0.07, 0);//周期
            const fixed4 amplitudes = fixed4(4.02, 0.34, 0.65, 0);//振幅
            const fixed4 frequencies = fixed4(0.00, 0.48, 0.08, 0);//频率
            const fixed4 offsets = fixed4(0.00, 0.16, 0.00, 0);//相位
            //按照距离海滩远近叠加渐变色
            fixed4 cos_grad = cosine_gradient(saturate(1.5-diffZ), phases, amplitudes, frequencies, offsets);
            cos_grad = clamp(cos_grad, 0, 1);
            col.rgb = toRGB(cos_grad);
            ...
            }

可以注意到,这里我使用的是1.5-diffZ而不是像原作者那样使用1-diffZ,这是由于我发现原神中的水色彩更绿,而1-diffZ得到的效果相交较之略有偏蓝。

现在的效果

接下来制作水面上的波纹,也就是海浪。原作者没有使用制作好的噪声贴图,而是使用了程序化生成Perlin噪声 噪声生成部分代码

//Perlin噪声生成
        float2 rand(float2 st, int seed)
        {
            float2 s = float2(dot(st, float2(127.1, 311.7)) + seed, dot(st, float2(269.5, 183.3)) + seed);
            return -1 + 2 * frac(sin(s) * 43758.5453123);
        }

        float noise(float2 st, int seed)
        {
            st.y += _Time.y;//采样位置随时间变化

            float2 p = floor(st);
            float2 f = frac(st);

            float w00 = dot(rand(p, seed), f);
            float w10 = dot(rand(p + float2(1, 0), seed), f - float2(1, 0));
            float w01 = dot(rand(p + float2(0, 1), seed), f - float2(0, 1));
            float w11 = dot(rand(p + float2(1, 1), seed), f - float2(1, 1));

            float2 u = f * f * (3 - 2 * f);

            return lerp(lerp(w00, w10, u.x), lerp(w01, w11, u.x), u.y);
        }

在法线的处理上,原作者使用了ddx和ddy这两个函数来计算噪声干扰后的法线,以此计算出 x 方向,y 方向各自的梯度,然后取其外积,就可以得到干扰后的法线。(GPU在光栅化时,会在同一时刻并行运行很多Fragment Shader,但不会一个一个片元地去计算,而是将片元组织为2*2的组来并行进行,而ddx(x)会获取到屏幕空间下右边的片元的x值减去左边片元的x值,ddy类似)

//海浪的涌起法线计算
float3 swell( float3 pos ){
            float3 normal;
            float height = noise(pos.xz * 0.1,0);
            normal = normalize(
                cross ( 
                    float3(0,ddy(height),1),
                    float3(1,ddx(height),0)
                )//两片元间高度差值得到梯度,叉乘获得法向量
            );
            return normal;
        }

这样制作出的海面法线在地平线处的观感不是很好,这是由于屏幕部分区域内的海浪密度过高造成的

地平线处海浪的密度过高

解决方法是将地平线附近的海浪高度降低

原作者的配图,意思是要把这部分降低

可以看到,附近的波浪(红色)在屏幕上显得很大,而远处的波浪(蓝色)在屏幕上显得很小。

因此,我们可以通过获取相机中朝向水面上点的矢量 v 的水平面上的射影 Vp大小的微分,使用ddy(length ( v.xz ))来区分地平线上的部分,可以得到如下图的效果

将ddy(length ( v.xz ))输出得到的效果

我们在原函数的基础上增加上anisotropy变量来控制海浪在地平线部分的高度

float3 swell( float3 pos , float anisotropy){
            float3 normal;
            float height = noise(pos.xz * 0.1,0);
            height *= anisotropy ;//使距离地平线近的区域的海浪高度降低
            normal = normalize(
                cross ( 
                    float3(0,ddy(height),1),
                    float3(1,ddx(height),0)
                )//两片元间高度差值得到梯度
            );
            return normal;
        }

接着我们就可以在片元着色器中计算法线

float anisotropy = saturate(1/ddy(length(v.xz))/10);//通过临近像素点间摄像机到片元位置差值来计算哪里是接近地平线的部分
    float3 swelledNormal = swell( i.worldPos , anisotropy);

这些工序完成后,我们在片元着色器中添加对天空盒的反射

half3 reflDir = reflect(-worldViewDir, swelledNormal);
fixed4 reflectionColor = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, reflDir);

如果要加入对场景中物体的反射,可以考虑使用反射探针 接下来,我们在片元着色器中使用Fresnel-Schlick近似模型计算菲涅尔效果

// 菲涅尔反射
            float f0 = 0.02;
            float vReflect = f0 + (1-f0) * pow(1 - dot(worldViewDir,swelledNormal),5);
            vReflect = saturate(vReflect * 2.0);                
            col = lerp(col , reflectionColor , vReflect);

最后一步,我们让靠近岸边的地方更加透明,呈现出从浅水逐渐变为深水的感觉,为实现这个效果,我们只需要根据之前计算出的片元深度与场景中的深度差值来修改透明度

//接近海滩部分更透明
            float alpha = saturate(diffZ);          
            col.a = alpha;

完整shader

Shader "Unlit/sea"
	{
	SubShader
	{
		Tags { "RenderType"="Transparent" }
		LOD 100
		Blend SrcAlpha OneMinusSrcAlpha 

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;							
			};

			struct v2f
			{
				UNITY_FOG_COORDS(3)
				float4 vertex : SV_POSITION;
				float4 screenPos : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
			};
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);				
				UNITY_TRANSFER_FOG(o,o.vertex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;	
				o.screenPos = ComputeScreenPos(o.vertex);		
				COMPUTE_EYEDEPTH(o.screenPos.z);
				return o;
			}
			//利用cos生成的渐变色,使用网站:https://sp4ghet.github.io/grad/
			fixed4 cosine_gradient(float x,  fixed4 phase, fixed4 amp, fixed4 freq, fixed4 offset){
				const float TAU = 2. * 3.14159265;
  				phase *= TAU;
  				x *= TAU;

  				return fixed4(
    				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(fixed3 grad){
  				 return grad.rgb;
			}
			//噪声图生成
			float2 rand(float2 st, int seed)
			{
				float2 s = float2(dot(st, float2(127.1, 311.7)) + seed, dot(st, float2(269.5, 183.3)) + seed);
				return -1 + 2 * frac(sin(s) * 43758.5453123);
			}
			float noise(float2 st, int seed)
			{
				st.y += _Time.y;

				float2 p = floor(st);
				float2 f = frac(st);
 
				float w00 = dot(rand(p, seed), f);
				float w10 = dot(rand(p + float2(1, 0), seed), f - float2(1, 0));
				float w01 = dot(rand(p + float2(0, 1), seed), f - float2(0, 1));
				float w11 = dot(rand(p + float2(1, 1), seed), f - float2(1, 1));
				
				float2 u = f * f * (3 - 2 * f);
 
				return lerp(lerp(w00, w10, u.x), lerp(w01, w11, u.x), u.y);
			}
			//海浪的涌起法线计算
			float3 swell( float3 pos , float anisotropy){
				float3 normal;
				float height = noise(pos.xz * 0.1,0);
				height *= anisotropy ;//使距离地平线近的区域的海浪高度降低
				normal = normalize(
					cross ( 
						float3(0,ddy(height),1),
						float3(1,ddx(height),0)
					)//两片元间高度差值得到梯度
				);
				return normal;
			}

			UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
			
			fixed4 frag (v2f i) : SV_Target
			{
				
				fixed4 col = (1,1,1,1);
    			float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));
				float partZ = i.screenPos.z;
				float diffZ = saturate( (sceneZ - partZ)/5.0f);//片元深度与场景深度的差值
				const fixed4 phases = fixed4(0.28, 0.50, 0.07, 0);//周期
				const fixed4 amplitudes = fixed4(4.02, 0.34, 0.65, 0);//振幅
				const fixed4 frequencies = fixed4(0.00, 0.48, 0.08, 0);//频率
				const fixed4 offsets = fixed4(0.00, 0.16, 0.00, 0);//相位
				//按照距离海滩远近叠加渐变色
				fixed4 cos_grad = cosine_gradient(saturate(1.5-diffZ), phases, amplitudes, frequencies, offsets);
  				cos_grad = clamp(cos_grad, 0, 1);
  				col.rgb = toRGB(cos_grad);	
				//海浪波动
				half3 worldViewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
				float3 v = i.worldPos - _WorldSpaceCameraPos;
				float anisotropy = saturate(1/ddy(length(v.xz))/10);//通过临近像素点间摄像机到片元位置差值来计算哪里是接近地平线的部分
				float3 swelledNormal = swell( i.worldPos , anisotropy);

				// 只反射天空盒
                half3 reflDir = reflect(-worldViewDir, swelledNormal);
				fixed4 reflectionColor = UNITY_SAMPLE_TEXCUBE(unity_SpecCube0, reflDir);

				// 菲涅尔反射
				float f0 = 0.02;
    			float vReflect = f0 + (1-f0) * pow(1 - dot(worldViewDir,swelledNormal),5);
				vReflect = saturate(vReflect * 2.0);				
				col = lerp(col , reflectionColor , vReflect);
				//接近海滩部分更透明
				float alpha = saturate(diffZ);			
                col.a = alpha;

				return col;
			}
			ENDCG
		}
	}
}

最后附上原作者GitHub项目链接:https://github.com/Uynet/Gensin-Sea

这样就完成了对原神海面的仿照制作,与实际原神的海面还具有一定差距,且具有众多不完善的地方,例如高光、反射、色散等效果都没有包含,且效果的体现很依赖天空盒的样式。但其完成的效果还差强人意且足够简单,在此做出学习分享,如有错误还望各位大佬不吝赐教 !

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值