今天为大家分享一位游戏开发学生与他的小型团队,在为期5天的课堂Game Jam中使用Unity制作小游戏《Color Wars》的过程,以及其中使用着色器为游戏画面添加各种颜色的方法。
他们制作的《Color Wars》是一款非常简单的2.5D多人对战游戏,玩家可以射击敌人,为敌人或者为场景加上颜色。游戏效果如下:
图形部分
所有游戏的特效魔法背后,都是最原始的3D模型、图片与人物精灵来组成整个场景。这些组成场景的元素都有其颜色及纹理。
其中比较棘手的部分是需要在Alpha通道中屏蔽所有元素的颜色。换而言之,默认情况下,屏幕的整个Alpha通道都是黑色的,直到玩家开始喷射油漆,才会使被油漆溅到区域的Alpha通道变为白色。然后图像效果就是其原有颜色与灰度进行混合。
如下:
从上图可以看出,使用Projector将喷漆绘制到物体表面并创建颜色遮罩。每个Projector都使用程序化动态生成,在子弹(空中飞行的白点)接触到某个表面时进行初始化。Projector带有一个盒式碰撞体,当子弹落在Projector上时,不会初始化新的Projector,而是让原先的Projector变大。这样漆量会变多,而场景中的Projector数量却保持不变。
默认情况下,Unity标准着色器会为所有不透明对象的Alpha通道写入1。所以下面使用自定义着色器来替换Unity标准着色器。新建一个标准表面着色器,将其表面函数替换为如下:
[C#] 纯文本查看 复制代码
1
2
3
4
5
6
7
8
9
|
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = 0;
}
|
下面这行很重要,用于避免Unity更改自定义的Alpha值。将#pragma那行代码改为如下:
[C#] 纯文本查看 复制代码
1
2
|
CGPROGRAM
#pragma surface surf Standard fullforwardshadows keepalpha
|
注意,该技巧不可用于Unity中的延迟渲染管线,因为它重写了G-Buffer中的Alpha通道来存储遮罩数据。
油漆喷射
当子弹撞击某个表面时就会在撞击处动态生成Unity Projector。这些Projector带有自定义材质与自定义着色器。材质纹理是一张带有Alpha通道喷溅形状图,本文示例使用的纹理如下图:
注意,纹理导入设置中要将Wrap Mode设为“Clamp”而非“Repeat”。用于Projector材质的着色器从Unity提供的ProjectorLight修改而来,代码如下:
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
Shader "Projector/ProjectAlpha"
{
Properties
{
_ShadowTex ( "Cookie" , 2D) = "gray" {}
}
Subshader
{
Tags { "Queue" = "Transparent" }
Pass
{
ZWrite Off
Blend Zero One, One One
Offset -1, -1
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct Input
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 uvShadow : TEXCOORD0;
UNITY_FOG_COORDS(2)
float4 pos : SV_POSITION;
fixed nv : COLOR0;
};
float4x4 unity_Projector;
float4x4 unity_ProjectorClip;
v2f vert (Input v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uvShadow = mul(unity_Projector, v.vertex);
UNITY_TRANSFER_FOG(o,o.pos);
float3 normView = normalize(float3(unity_Projector[2][0], unity_Projector[2][1], unity_Projector[2][2]));
float nv = dot(v.normal, normView);
o.nv = nv < 0 ? 1 : 0;
return o;
}
sampler2D _ShadowTex;
sampler2D _FalloffTex;
fixed4 frag (v2f i) : COLOR
{
fixed4 texS = tex2Dproj (_ShadowTex, UNITY_PROJ_COORD(i.uvShadow));
fixed4 res = fixed4(1, 1, 1, texS.a );
res.a *= i.nv;
UNITY_APPLY_FOG_COLOR(i.fogCoord, res, fixed4(1,1,1,1));
return res;
}
ENDCG
}
}
}
|
下面来介绍其中最为重要的混合部分。
混合原理
当着色器计算某个像素的颜色时,该颜色必须作用于屏幕上该点已经存在的像素颜色之上。默认情况下,新的像素会完全覆盖原有像素,但新像素也可以与原有像素进行混合。混合通常用于让对象呈透明或半透明效果,当然也可以实现很多其它的炫酷特效。
关键字Blend可以包含在Subshader或Pass标签中,甚至对同一个着色器的不同Pass进行混合。添加Blend关键字后,必须写入混合因子。混合因子如下:
Src指向着色器用于计算的颜色。Dst指向屏幕上已有的像素颜色。着色器用于计算的颜色会与第一个因子相乘,而屏幕上已有颜色会与第二个因子相乘。将两个结果相加,就是最终写到屏幕的颜色。
所以”Blend SrcAlpha One”会将自身Alpha值与当前着色器计算的颜色相乘,此时屏幕上的颜色暂未改动。然后再将屏幕颜色计算后的结果与前者相加。还可以使用逗号分隔两组因子,逗号前的混合选项用于计算颜色,逗号后的混合选项仅计算Alpha通道。可以查阅Unity文档了解更多关于混合的内容。
用于Projector的着色器就是“Blend Zero One, One One”,“Zero One”移除了飞溅纹理的颜色,使用子弹所飞溅到的表面颜色。“One One”将飞溅物的Alpha值与表面Alpha值相加。
现在使用上面的着色器与材质来生成Projector,应该将场景视图的Alpha通道设为白色。
颜色与灰度
现在可以随意修改Alpha通道,但还未达到最终效果。下面利用Alpha遮罩来创建游戏所需的图像特效。
首先,创建要使用图像特效的着色器。在Unity中新建默认的Image Effect Shader,然后将片段代码替换为如下:
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
|
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed3 bnw = dot(col.rgb, float3(0.3, 0.59, 0.11));
col.rgb = lerp(bnw, col.rgb, col.a);
return col;
}
|
可以随意更改bnw变量以达到理想的混合效果。最后还需要新建脚本来运行该图像特效。脚本非常简单,代码如下:
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
|
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityStandardAssets.ImageEffects;
[ExecuteInEditMode]
[ImageEffectAllowedInSceneView]
public class AlphaColorSwitch : ImageEffectBase
{
void OnRenderImage ( RenderTexture source, RenderTexture destination )
{
Graphics.Blit ( source, destination, material );
}
}
|
注意,这里用到了ImageEffectBase,该资源在Unity标准资源库中。导入标准资源库后,将脚本绑定到相机(确保将相机的渲染模式设为Forward)上,并将公共的着色器变量设为前面提到的着色器。
到此就可以向场景中喷射油漆啦!
已知限制
本文提到的实现方式还存在一些限制,不一定适合所有的应用场景,但对于《Color War》这款游戏来说已足够。主要存在以下两点限制:
1、采用Alpha遮罩就意味着没有Alpha通道,也不支持延迟渲染。这也许可以使用模板、命令缓冲区甚至多渲染目标来解决。
2、项目使用的自定义着色器过多,这种做法并不推荐。如果您的项目可以使用延迟渲染解决其它问题,那么这个问题也就不存在了。
结语
本文为大家分享了在Unity中利用着色器来实现喷绘效果的过程,文中的解决方法仅作为一种思路参考,不一定适用于所有项目。大家也可以按照项目的实际需求,来选取更加适合的解决方案。
来源:Unity官方中文社区 |