首页 > Unity > [蛮牛干货] 巧用Shader,游戏玩法新思路
2017
08-10

[蛮牛干货] 巧用Shader,游戏玩法新思路

今天为大家分享一位游戏开发学生与他的小型团队,在为期5天的课堂Game Jam中使用Unity制作小游戏《Color Wars》的过程,以及其中使用着色器为游戏画面添加各种颜色的方法。
他们制作的《Color Wars》是一款非常简单的2.5D多人对战游戏,玩家可以射击敌人,为敌人或者为场景加上颜色。游戏效果如下:

[蛮牛干货] 巧用Shader,游戏玩法新思路 - 第1张  | FreemanApp

图形部分
所有游戏的特效魔法背后,都是最原始的3D模型、图片与人物精灵来组成整个场景。这些组成场景的元素都有其颜色及纹理。

[蛮牛干货] 巧用Shader,游戏玩法新思路 - 第2张  | FreemanApp

其中比较棘手的部分是需要在Alpha通道中屏蔽所有元素的颜色。换而言之,默认情况下,屏幕的整个Alpha通道都是黑色的,直到玩家开始喷射油漆,才会使被油漆溅到区域的Alpha通道变为白色。然后图像效果就是其原有颜色与灰度进行混合。
如下:

[蛮牛干货] 巧用Shader,游戏玩法新思路 - 第3张  | FreemanApp
[蛮牛干货] 巧用Shader,游戏玩法新思路 - 第4张  | FreemanApp

从上图可以看出,使用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)
{
        // Albedo 来自带颜色的纹理
        fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
        o.Albedo = c.rgb;
        // Metallic 和 smoothness 来自滑块变量
        o.Metallic = _Metallic;
        o.Smoothness = _Glossiness;
        o.Alpha = 0;  // 我只添加了这一行!
}

下面这行很重要,用于避免Unity更改自定义的Alpha值。将#pragma那行代码改为如下:

[C#] 纯文本查看 复制代码
1
2
CGPROGRAM
#pragma surface surf Standard fullforwardshadows keepalpha
// 添加 "keepalpha" 告诉Unity不要覆盖我们的alpha值!

注意,该技巧不可用于Unity中的延迟渲染管线,因为它重写了G-Buffer中的Alpha通道来存储遮罩数据。
油漆喷射
当子弹撞击某个表面时就会在撞击处动态生成Unity Projector。这些Projector带有自定义材质与自定义着色器。材质纹理是一张带有Alpha通道喷溅形状图,本文示例使用的纹理如下图:

[蛮牛干货] 巧用Shader,游戏玩法新思路 - 第5张  | FreemanApp

注意,纹理导入设置中要将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);
 
                                // For me, splatters were being projected on both sides of the
                                // object, so I used the view direction and the surface normal
                                // to check if it was facing the camera.
                                float3 normView = normalize(float3(unity_Projector[2][0], unity_Projector[2][1], unity_Projector[2][2]));
                                float nv = dot(v.normal, normView);
                                // negative values means surface isn't facing the camera
                                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 );
                                // Multiply by alpha channel to
                                // remove back-side projection.
                                res.a *= i.nv;
 
                                UNITY_APPLY_FOG_COLOR(i.fogCoord, res, fixed4(1,1,1,1));
                                return res;
                        }
                        ENDCG
                }
        }
}

下面来介绍其中最为重要的混合部分。
混合原理
当着色器计算某个像素的颜色时,该颜色必须作用于屏幕上该点已经存在的像素颜色之上。默认情况下,新的像素会完全覆盖原有像素,但新像素也可以与原有像素进行混合。混合通常用于让对象呈透明或半透明效果,当然也可以实现很多其它的炫酷特效。
关键字Blend可以包含在Subshader或Pass标签中,甚至对同一个着色器的不同Pass进行混合。添加Blend关键字后,必须写入混合因子。混合因子如下:

[蛮牛干货] 巧用Shader,游戏玩法新思路 - 第6张  | FreemanApp

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);
 
        // This lines generates a Black&White version of the screen
        fixed3 bnw = dot(col.rgb, float3(0.3, 0.59, 0.11));
        // Switch between B&W and Color based on alpha channel
        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官方中文社区

最后编辑:
作者:freeman
这个作者貌似有点懒,什么都没有留下。

留下一个回复

你的email不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据