请选择 进入手机版 | 继续访问电脑版

查看: 15614|回复: 146

[原创] Unity着色器教程 | 积雪效果

  [复制链接]

910

主题

1570

帖子

2万

贡献

管理员

Rank: 9Rank: 9Rank: 9

积分
20185
QQ
发表于 2016-12-30 12:13:03 | 显示全部楼层 |阅读模式
为游戏中的所有纹理都加上雪花可能需要花费大量时间。本文将展示在Unity中如何创建Image Effect(屏幕空间着色器)来快速改变场景的季节。

使用前后效果图对比如下:
0.png 1.png


工作原理

上面两张图显示的是同一个场景。它们之间唯一的区别就是第二张图启用了相机上的雪花特效。实现该效果无需对场景的贴图做任何更改。这是什么原理呢?

其实原理非常简单。就是假定所有法线朝上的像素点(如:草,地板等)都需要覆盖雪花。同样,法线朝着其它方向的像素点(如:松树,墙),则需要在雪花纹理和原始纹理之间进行平缓过渡。

获取所需数据

实现上面的雪花效果有以下准备事项:
  • 将渲染路径设置为Deferred(延迟渲染)
  • 将Camera.depthTextureMode设置为DepthNormals


由于第二项可以很方便地由屏幕特效脚本进行设置,所以如果游戏已经使用了前向渲染路径(Forward Rendering Path)时,第一项很容易出问题。

Camera.depthTextureMode设置为DepthNormals后可以读取屏幕深度(像素与相机之间的距离)和法线(所朝的方向)。

创建一个屏幕特效(Image Effect)由至少一个脚本和一个着色器构成。通常这个着色器不是用来渲染3D物体的,而是根据给定的输入数据渲染一个全屏的图像。在本文的例子中,输入数据就是一张相机渲染的结果图片以及一些用户设置的属性。

[C#] 纯文本查看 复制代码
using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class ScreenSpaceSnow : MonoBehaviour
{

	public Texture2D SnowTexture;

	public Color SnowColor = Color.white;

	public float SnowTextureScale = 0.1f;

	[Range(0, 1)]
	public float BottomThreshold = 0f;
	[Range(0, 1)]
	public float TopThreshold = 1f;

	private Material _material;

	void OnEnable()
	{
		// dynamically create a material that will use our shader
		_material = new Material(Shader.Find("TKoU/ScreenSpaceSnow"));

		// tell the camera to render depth and normals
		GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
	}

	void OnRenderImage(RenderTexture src, RenderTexture dest) 
	{
		// set shader properties
		_material.SetMatrix("_CamToWorld", GetComponent<Camera>().cameraToWorldMatrix);
		_material.SetColor("_SnowColor", SnowColor);
		_material.SetFloat("_BottomThreshold", BottomThreshold);
		_material.SetFloat("_TopThreshold", TopThreshold);
		_material.SetTexture("_SnowTex", SnowTexture);
		_material.SetFloat("_SnowTexScale", SnowTextureScale);

		// execute the shader on input texture (src) and write to output (dest)
		Graphics.Blit(src, dest, _material);
	}
}


这里只是基础的设置,还不能生成雪花。有趣的事情还在后面。

着色器

雪花着色器是无光照着色器(unlit shader),因为屏幕空间是没有光照的,所以也不会用到任何光照信息。基础模板如下:

[C#] 纯文本查看 复制代码
Shader "TKoU/ScreenSpaceSnow"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	SubShader
	{
		// No culling or depth
		Cull Off ZWrite Off ZTest Always

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			};

			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = v.uv;
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// the magic happens here
			}
			ENDCG
		}
	}
}


注意,直接新建无光照着色器(Create->Shader->Unlit Shader) 就会看到几乎同样的代码。

现在看看重要部分:片段着色器。首先,需要通过ScreenSpaceSnow脚本来获取所有数据:
[C#] 纯文本查看 复制代码
sampler2D _MainTex;
sampler2D _CameraDepthNormalsTexture;
float4x4 _CamToWorld;

sampler2D _SnowTex;
float _SnowTexScale;

half4 _SnowColor;

fixed _BottomThreshold;
fixed _TopThreshold;


half4 frag (v2f i) : SV_Target
{
	
}


找出需要下雪的地方

正如之前所说,所有法线朝上的表面都将覆盖雪。相机已经设置了生成深度法线贴图,所以现在直接获取即可。代码如下:
[C#] 纯文本查看 复制代码
sampler2D _CameraDepthNormalsTexture;


查看Unity官方文档可以了解该命名的意义:
深度贴图可以作为一个着色器的全局着色器属性进行采样。通过声明名为_CameraDepthTexture的采样器,就能够采样相机的主深度纹理。
_CameraDepthTexture总是引用相机的主深度贴图。


现在开始获取法线:
[C#] 纯文本查看 复制代码
half4 frag (v2f i) : SV_Target
{
	half3 normal;
	float depth;

	DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
	normal = mul( (float3x3)_CamToWorld, normal);

	return half4(normal, 1);
}


Unity文档解释深度和法线的数据都打包为16位。这里需要像代码那样调用DecodeDepthNormal方法进行解包。

这个方法检索的是相机空间的法线。也就是说,如果旋转屏幕相机,那么法线朝向也会改变。脚本中将法线乘以_CamToWorld 矩阵就是为了避免这种情况。它会将法线从相机空间转换为世界空间,这样就不再依赖于相机的透视。

为了让着色器正确编译就必须返回一些东西,所以上面的代码设置了返回语句。这样也便于预览结果以确认计算是否正确。

2.png


暂时渲染为RGB图像。在Unity中,Y轴是默认向上的。图中绿色部分表示Y坐标轴的值。目前为止结果不错!

现在将其转换为雪量的因子。
[C#] 纯文本查看 复制代码
half4 frag (v2f i) : SV_Target
{
	half3 normal;
	float depth;

	DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
	normal = mul( (float3x3)_CamToWorld, normal);

	half snowAmount = normal.g;
	half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;
	snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);

	return half4(snowAmount, snowAmount, snowAmount, 1);
}


这里会用到绿色分量。接下来配置积雪覆盖区域顶部和底部的阀值,以便于调整场景的积雪量。
3.png


雪纹理

如果没有纹理,雪看起来会不真实。最难的部分就是将2D纹理(屏幕空间)应用到3D物体上。一种方法是获取像素的世界坐标,然后将世界坐标的X和Z值作为纹理坐标。

[C#] 纯文本查看 复制代码
half4 frag (v2f i) : SV_Target
{
	half3 normal;
	float depth;

	DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
	normal = mul( (float3x3)_CamToWorld, normal);

	// find out snow amount
	half snowAmount = normal.g;
	half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;
	snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);

	// find out snow color
	float2 p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);
	float3 vpos = float3( (i.uv * 2 - 1) / p11_22, -1) * depth;
	float4 wpos = mul(_CamToWorld, float4(vpos, 1));
	wpos += float4(_WorldSpaceCameraPos, 0) / _ProjectionParams.z;

	half3 snowColor = tex2D(_SnowTex, wpos.xz * _SnowTexScale * _ProjectionParams.z) * _SnowColor;

	return half4(snowColor, 1);
}


这里涉及到一些数学知识,您只需知道vpos是视口坐标,wpos是由视口坐标与_CamToWorld矩阵相乘而得到的世界坐标,并且它通过除以远平面的位置(_ProjectionParams.z)来转换为有效的世界坐标。最后使用XZ坐标乘以可配置参数_SnowTexScale和远平面,来计算雪的颜色并获取适当的值。

4.png


合并

下面将积雪与场景进行合并。

[C#] 纯文本查看 复制代码
half4 frag (v2f i) : SV_Target
{
	half3 normal;
	float depth;

	DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
	normal = mul( (float3x3)_CamToWorld, normal);

	// find out snow amount
	half snowAmount = normal.g;
	half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;
	snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);

	// find out snow color
	float2 p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);
	float3 vpos = float3( (i.uv * 2 - 1) / p11_22, -1) * depth;
	float4 wpos = mul(_CamToWorld, float4(vpos, 1));
	wpos += float4(_WorldSpaceCameraPos, 0) / _ProjectionParams.z;

	wpos *= _SnowTexScale * _ProjectionParams.z;
	half3 snowColor = tex2D(_SnowTex, wpos.xz) * _SnowColor;

	// get color and lerp to snow texture
	half4 col = tex2D(_MainTex, i.uv);
	return lerp(col, snowColor, snowAmount);
}


上述代码获取原始颜色,并使用snowAmount进行插值渐变为snowColor 。

5.png


最后一步:将_TopThreshold设为0.6:

6.png


完成!

结论

全屏效果见下图。看起来不错吧?
7.png 8.png


本文着色器可免费下载,您也可以将其用于您的项目!(请回复本帖下载着色器):
游客,如果您要查看本帖隐藏内容请回复


欢迎大家加入Unity官方中文论坛群:470161914

原文链接:Let It Snow! How To Make a Fast Screen-Space Snow Accumulation Shader In Unity
感谢Unity官方翻译组成员“yangtze0621”对本文翻译所做的贡献。
转载请注明来源:Unity官方中文社区 (forum.china.unity3d.com)。请勿私自更改任何版权说明信息。


0

主题

11

帖子

80

贡献

初级UU族—2级

Rank: 2

积分
80
QQ
发表于 2017-2-15 07:25:06 | 显示全部楼层
代码有语法错误  将half3 snowColor改成half4 snowColor可解决。角色在场景中移动的话,则雪地的贴图会造成在表面漂移的问题。



天道酬勤 厚德载物

0

主题

5

帖子

80

贡献

初级UU族—2级

Rank: 2

积分
80
QQ
发表于 2017-1-1 05:32:55 | 显示全部楼层
1111111111111111111111
我奔着我目标而去我不会放弃.

1

主题

46

帖子

930

贡献

中级UU族—1级

Rank: 4

积分
930
发表于 2017-1-2 01:15:38 | 显示全部楼层
虽然现在还看不懂,但是还是顶一下
心之所往,必将全力以赴

0

主题

1

帖子

10

贡献

初级UU族—1级

Rank: 1

积分
10
发表于 2017-1-2 05:16:49 | 显示全部楼层
感受一下新特色

0

主题

5

帖子

50

贡献

初级UU族—2级

Rank: 2

积分
50
发表于 2017-1-2 05:19:54 | 显示全部楼层
66666666666666666666666666666666666

0

主题

1

帖子

10

贡献

初级UU族—1级

Rank: 1

积分
10
发表于 2017-1-2 05:41:34 | 显示全部楼层

谢谢分享

0

主题

6

帖子

105

贡献

初级UU族—2级

Rank: 2

积分
100
发表于 2017-1-2 06:46:23 | 显示全部楼层
kankan,what's wrong....

0

主题

1

帖子

20

贡献

初级UU族—1级

Rank: 1

积分
20
发表于 2017-1-2 07:40:01 | 显示全部楼层


谢谢分享

2

主题

10

帖子

175

贡献

初级UU族—2级

Rank: 2

积分
175
发表于 2017-1-2 08:48:19 | 显示全部楼层
看看效果

0

主题

5

帖子

60

贡献

初级UU族—2级

Rank: 2

积分
60
发表于 2017-1-2 11:12:23 来自手机 | 显示全部楼层
好玩222222222222222222
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表