Unity自带了很多shader,其中就包含卡通渲染和描边的shader。但是我在实际开发游戏的过程中还是遇到了这些shader无法解决的问题。
于是,我们需要理解如何写Unity的shader,并且按照我们自己的需求编写一个新的shader。
大多数情况下,我们遇到的实际问题要比一个酷炫的demo复杂和恶心。就像Daikon Forge GUI插件非常完善的Atlas图集功能却因为我们的GUI图片元素过多而变得非常鸡肋,甚至不得不做修改。 或者明明很正常的Dynamic Font功能却因为我们要渲染的文字繁而多,变得bug频频。
我们因为游戏风格和玩法的特殊需求,所以我们需要这样一个shader:
1、卡通渲染(这个主要是使用卡通渲染掩盖本身模型和贴图的不给力)
2、支持透明贴图 (我们很多模型使用了透明贴图,当时主美的说法是使用透明贴图可以减少很多面数,但是现在看来,如果我为了支持透明贴图而禁用了剔除,说不定反而是得不偿失的做法)
3、不依赖灯光 (毕竟我们的游戏不是3D的MMO,而且即便是火炬之光似乎在人物渲染的时候也是不依赖灯光的,对于我们的游戏风格而言,如果因为灯光造成明显的明暗区分是会削弱表现的)
4、最好有灯光影响 (这个跟上一点不冲突,灯光可以在shader中计算,场景中无论有没有灯光,人物都会有一定的明暗区分,很多情况下这个是加分的)
5、能够正确处理好透明和剔除 (如果处理不正确,要么会使人物表现错乱,甚至无法分辨出哪条腿在前那条退在后,要么会因为背面剔除而造成一部分部件无法显示)
我先贴出修改后的shader(基础是Unity自带的Toon shader),shader还没有最终修改完,等修改完毕再做更新。
一、卡通渲染的shader
Shader "Toon/Basic" {
Properties {
_Color ("Main Color", Color) = (.5,.5,.5,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_ToonShade ("ToonShader Cubemap(RGB)", CUBE) = "" { Texgen CubeNormal }
_Cutoff ("Alpha cutoff", Range(0,1)) = 0.9
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
Name "BASE"
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
sampler2D _MainTex;
samplerCUBE _ToonShade;
float4 _MainTex_ST;
float4 _Color;
fixed _Cutoff;
struct appdata {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : POSITION;
float2 texcoord : TEXCOORD0;
float3 cubenormal : TEXCOORD1;
};
v2f vert (appdata v)
{
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
o.cubenormal = mul (UNITY_MATRIX_MV, float4(v.normal,0));
return o;
}
float4 frag (v2f i) : COLOR
{
float4 col = _Color * tex2D(_MainTex, i.texcoord);
float4 cube = texCUBE(_ToonShade, i.cubenormal);
clip(col.a - _Cutoff);
return float4(2.0f * cube.rgb * col.rgb, col.a);
}
ENDCG
}
}
// SubShader {
// Tags { "RenderType"="Opaque" }
// Pass {
// Name "BASE"
// Cull Off
// SetTexture [_MainTex] {
// constantColor [_Color]
// Combine texture * constant
// }
// SetTexture [_ToonShade] {
// combine texture * previous DOUBLE, previous
// }
// }
// }
Fallback "VertexLit"
}
一些说明:
1、第一行指明了shader的名字,它有两个用处,首先可以在Material中通过这个名字来指定shader,代码中也可以使用Shader.Find来查找对应shader;其次在使用UsePass复用Pass的时候必须使用这个名字来指定shader
2、Unity的shader的基本结构包含了Properties和Subshader,Properties是shader的入口参数,所有参数都可以在Unity编辑器中设置和修改。它的类型只有固定几种。
Subshader可以有n个。Subshader中包含Tags和Pass。Tags指定了一些基础属性。这些属性都是Unity预先定义好的,需要根据文档设置。它可以控制shader的渲染次序等等。
Pass就是shader的基础渲染单元。每个Pass都可以指定一个Name以便复用代码。比如下面带描边的shader就使用了 UsePass "Toon/Basic/BASE"指定了复用卡通渲染的shader。Pass内还有一些指令可以控制灯光、剔除、深度测试等等。这个非常重要,同样需要仔细查阅文档。
Pass中CGPROGRAM和ENDCG括起来的部分就是常规意义上的shader,它包含顶点处理函数和像素处理函数,计算每个顶点和像素的处理。
#pragma vertex vert和#pragma fragment frag 这两行分别指定了顶点处理函数和像素处理函数为vert和frag。
我们在Properties里面指定的属性不能直接使用,那个是为编辑器准备的,我们必须在CG代码中声明才能正常使用,比如sampler2D _MainTex。
UnityCG.cginc中包含一些自定义的顶点结构和一些函数,可以省去我们一些基础的操作。
appdata是程序传入的数据信息,可以包含顶点坐标 纹理坐标 法线等等 v2f是我们自己定义的结构,这个概念上与glsl的shader基本一致。
3、后面注掉的一大段貌似是固定渲染管线的shader代码,也就是不支持vert和frag这样真正的shader代码的时候的处理函数。一般情况下用不到,删掉了事。
4、最后一行包含了一个Fallback,就是说如果你这个牛b的shader无法在这个显卡上面执行则指定替代的shader。
5、一些细节实现的说明。 这个shader通过一个cubemap实现了类似灯光的明暗效果(当然我们不需要一个实际的灯光来照亮物体) 我添加了一个clip的调用来cutoff掉透明部分实现透明贴图的效果。 Cull off这条指令指定了禁用剔除以避免部分穿帮。这个会有损效率,因为正常来说背面是不用渲染的,但是现在无论正面还是背面都需要进行渲染。
二、卡通渲染带描边的shader
Shader "Toon/Basic Outline" {
Properties {
_Color ("Main Color", Color) = (.5,.5,.5,1)
_OutlineColor ("Outline Color", Color) = (0,0,0,1)
_Outline ("Outline width", Range (.002, 0.03)) = .005
_MainTex ("Base (RGB)", 2D) = "white" { }
_ToonShade ("ToonShader Cubemap(RGB)", CUBE) = "" { Texgen CubeNormal }
_Cutoff ("Alpha cutoff", Range(0,1)) = 0.9
}
CGINCLUDE
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : POSITION;
float4 color : COLOR;
};
uniform float _Outline;
uniform float4 _OutlineColor;
v2f vert(appdata v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
float2 offset = TransformViewToProjection(normal.xy);
o.pos.xy += (offset * o.pos.z * _Outline) / mul(UNITY_MATRIX_MVP, v.vertex).z;
return o;
}
ENDCG
SubShader {
Tags { "RenderType"="Opaque" }
UsePass "Toon/Basic/BASE"
Pass {
Name "OUTLINE"
Tags { "LightMode" = "Always" }
Cull Front
ZWrite On
ColorMask RGB
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
half4 frag(v2f i) :COLOR { return _OutlineColor; }
ENDCG
}
}
//
// SubShader {
// Tags { "RenderType"="Opaque" }
// UsePass "Toon/Basic/BASE"
// Pass {
// Name "OUTLINE"
// Tags { "LightMode" = "Always" }
// Cull Front
// //Cull off
// ZWrite On
// ColorMask RGB
// Blend SrcAlpha OneMinusSrcAlpha
//
// CGPROGRAM
// #pragma vertex vert
// #pragma exclude_renderers shaderonly
// ENDCG
// SetTexture [_MainTex] { combine primary }
// }
// }
//
Fallback "Toon/Basic"
}
1、描边效果并不是很理想。 描边时要注意考虑摄像机远近,如果不注意的话,摄像机拉远后会看见一大坨黑影
2、待续