Unity使用着色器为游戏添加颜色的方法 VR资源

清风 2017-08-10 17:04:00

今天为大家分享一位游戏开发学生与他的小型团队,在为期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标准着色器。新建一个标准表面着色器,将其表面函数替换为如下:

 
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那行代码改为如下:
 
1
2
CGPROGRAM
#pragma surface surf Standard fullforwardshadows keepalpha
// 添加 "keepalpha" 告诉Unity不要覆盖我们的alpha值!
注意,该技巧不可用于Unity中的延迟渲染管线,因为它重写了G-Buffer中的Alpha通道来存储遮罩数据。
油漆喷射
当子弹撞击某个表面时就会在撞击处动态生成Unity Projector。这些Projector带有自定义材质与自定义着色器。材质纹理是一张带有Alpha通道喷溅形状图,本文示例使用的纹理如下图:
 
 

注意,纹理导入设置中要将Wrap Mode设为“Clamp”而非“Repeat”。用于Projector材质的着色器从Unity提供的ProjectorLight修改而来,代码如下:
 
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关键字后,必须写入混合因子。混合因子如下:
 
 


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,然后将片段代码替换为如下:

 
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变量以达到理想的混合效果。最后还需要新建脚本来运行该图像特效。脚本非常简单,代码如下:
 
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中利用着色器来实现喷绘效果的过程,文中的解决方法仅作为一种思路参考,不一定适用于所有项目。大家也可以按照项目的实际需求,来选取更加适合的解决方案。

99VR视界二维码
热门推荐
Hot Recommended
在线客服