Trick
Name of Property
在 unity 提供的默认 shader 中,properties 的名称都是固定的,当材质切换 shader 的时候,unity 会根据相同的 properties 词条名称将原有状态赋值到下一个状态中。
特别是美术贴图的设置,设置起来比较麻烦,所以维持一些 unity 默认的用法是有好处的,或许不太好看,觉得不符合自己的风格。即使要使用自己的风格,也应该尽量让自己一套 shader 在一致用法上使用相同名称。不过我建议还是和 unity 一致比较好。不然调试效果的时候简直崩溃。下面收集了一些比较常用的名称列表:
名称 | 说明 |
---|---|
_Color | 颜色值 |
_MainTex | 基贴图什么的 |
_BumpMap | 法线图什么的 |
_LightMap | 光照图,手机上应该比较常用了 |
_Detail | 细节贴图 |
_TintColor | 做particle都在用这个颜色。wiki |
_Cutoff | 一般是半透明柔化边界用的裁减系数 |
UV Animation
可以从上图中看到每一张贴图都会有这么一个 Tiling-Offset的设置做 uv 动画用的。参数在 shader 中是一个 vector4。unity 对它有包装,在 unityCG.cginc 头文件中定义了 transform_tex 函数,此函数会通过输入的贴图变量的名称 _texture_name,加上_ST
后缀查找一个名字为 uniform float4 _texture_name_ST 变量,对uv做scale和offset操作。所以在获取纹理UV坐标的时候,用 transform_tex 对 UV 变换。
properties
{
_MainTex ("基贴图", 2D) = "white" { }
}
....
CGINCLUDE
#include "unityCG.cginc"
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
....
vs_out vertex_shader( ....
half4 texcoord : TEXCOORD0,
....)
{
out.texcoord = TRANSFORM_TEX(texcoord, _MainTex);
....
每个采样器都能生成相应的 _ST,参数也会被正确设置。transform 的定义如下:
#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
其余的可以参考手册Accessing shader properties in Cg
Lighting
在 unity 中使用 cg 书写跟光照相关的 shader 是比较具有挑战的,因为这方面手册欠缺说明,只能通过阅读 unityCG/Lighting/AutoLight.cginc 几个头文件窥斑见豹。其中比较难解决的问题有2,分别是:如何获取光照参数、如何获得点光源、探照灯的衰减
在unity场景中,每个光源可以通过设置 render mode
来指定光源的渲染计算方式,选用important
的光源会强制使用pixel lighting进行渲染。对于点光源和探照灯,无论设为important
还是auto
,unity都会使用 add pass 计算渲染。当一个物体受到多个光源影响的时候,需要根据情况排序,大概情况是光照按照强度 intensity、与材质的距离进行排序,受光强的排前面使用 base pass,之后最多有 n-1 个点光源(render setting 中设置)进行 add pass 计算,最后是SH光照。具体的规则可以参照手册 Forward Rendering Path Details
光照用到的参数在 unity 中并没有详细说明,主要参数分布在 unity 提供的一些 build-in 头文件中,在渲染过程中,forward 和 vetrex 能够访问到的参数是不一致的。经过不断的测试,得出一些经验。头文件如下:
- UnityShaderVariables.cginc
- UnityCG.cginc
- Lighting.cginc
- AutoLight.cginc
想要访问 unity built-in 的参数,可以直接 include 以上的文件,也可以记住一些常用的参数名称直接把uniform参数写出来,前提是名称要对的。
环境光在 unity 中默认是整个完整项目使用同一个参数,一般在 edit->render setting 中设置。不过这个参数可以通过代码修改,一个场景可以略微改变 ambient 适应环境氛围。在 shader 中,可以根据 built-in 参数 float4 UNITY_LIGHTMODEL_AMBIENT 进行访问。需要注意的是,因为早起的 unity 会把 ambient 除以 2 再传给 shader,为了保证一致性,现在 UNITY_LIGHTMODEL_AMBIENT 的值,会是实际设置的一般,需要在shader中由书写者乘2操作。
forward
forward 渲染需要书写 2 个 pass :
- forward base:
tags { "lightmode" = "ForwardBase" }
base pass 计算主光源的颜色值,这个阶段输出主要颜色值。 - forward add:
tags { "lightmode" = "ForwardAdd" }
add pass,根据物体受到光的数量,每盏灯光都以 add 的方式叠加在 base 的颜色值上。
其实 base / add 的主要任务是计算1盏光照的颜色,因为add pass 是以叠加的方式渲染,设置blending,然后做一个预编译选项如下:
pass
{
....
tags { "LightMode" = "ForwardAdd" }
blending one one
CGPROGRAM
#pragma multi_compile_fwdadd
....
}
使用 forward 时可以访问并使用以下被正确设置过的参数:
uniform参数 | 说明 |
---|---|
float4 _LightColor0 | 光的颜色值 |
float4 _WorldSpaceLightPos0 | 世界坐标当前光的位置 |
float4 _WorldSpaceCameraPos | 世界坐标中摄像机的位置 |
注意 vertex 有自己的参数,在使用 vertex 渲染的时候,这三个值是默认值,没有被正确设置过的。由于提供的参数都是世界坐标的,因此需要计算法线时也应该尽量转到世界空间中会比较好。可以通过访问_World2Object 、_Object2World 两个变换矩阵来达到目的。
float3 normal_w = normalize(mul( float4( normal, 0.0 ), _Word2Object).xyz);
因为 forward 中,point light 只能是 add pass,在 base pass 计算 view direction 的时候,可以不用访问 light_position.w。但是 add pass 有可能会有 directional light 参与,所以仍然需要判断光是否具有位置性。
vertex
当使用vertex光照的时候,可以使用unity_前缀的参数,这些参数包括
uniform参数 | 说明 |
---|---|
float4 unity_LightPosition[n] | 光源在摄像机中的位置 |
float4 unity_LightColor[n] | 光源颜色 |
float4 unity_LightAtten[n] | 光源的衰减,一般我们使用它的z分量 |
光源是view空间的坐标。平行光会将.w值设定为0,其余为1,因此光源方向我们可以根据这个公式计算出来。
float3 light_direction = normalize( unity_LightPosition[ index ].xyz - eye_position * unity_LightPosition[ index ].w );
最早尝试以 unity_4LightPosX0、unity_4LightPosY0、unity_4LightPosZ0来指定物体坐标,位置能按照下头的公式来计算:
float4 light_position = float4(
unity_4LightPosX0[ index ],
unity_4LightPosY0[ index ],
unity_4LightPosZ0[ index ],
1.0);
但是在 vertex 中就没办法取到 attenuation,attenuation主要是计算点光源、探照灯的衰减以模拟出明暗效果。为了配合 unity_4LightAtten0,一致采用 unity_LightPosition 访问坐标。
光源的衰减
attenuation = 1.0 / (1.0 + distance(eye_2_object) * unity_LightAtten[i])
UnityCG里有是通过这段代码来计算衰减的。在 unity 提供的默认参数里,虽然有 range 参数,但是在 vertex path 中是无法正确获取的。所以因为物体远离光源产生的衰减,就只能靠 unity_LightAtten 来计算了。虽然用了以上函数,但是仍然无法得到和 built-in 效果一致的表现,经过测试用了两个魔法数字可以得到比较不错的效果:
float light_attenuation = unity_LightAtten[ index ].z;
float diffuse_attenuation = 3.0 / (1.0 + length(light_direction) * light_attenuation);
其他的效果可以参照:各种效果的教程
Post Effect
做屏幕特效的做法是给camera加上组件。component脚本都需要继承自MonoBehaviour,除此之外要实现post effect,还需要要求摄像机。摄像机会自动调用脚本的 void OnRenderImage(RenderTexture source, RenderTexture destination)。代码就会像下面这个样子。
public class PostEffect : MonoBehaviour {
private void OnRenderImage(RenderTexture _src, RenderTexture _dst) {
Graphics.Blit(_src, _dst);
//Graphics.Blit(_src, _dst, m_material, pass_index);
}
}
在函数内部可以用 Graphic 的操作进行图像渲染,一般会需要获取一个RenderTexture做一些source的处理,最后不要忘记释放它。也可以在程序启动的时候就创建这些临时rt,这样可以在运行期间节省分配的时间。
....
RenderTexture temp = RenderTexture.GetTemporary(width, height, depth, RGBA);
material.SetFloat("name", float_value);
Graphics.Blit(_src, temp, m_material, pass_index1);
material.SetTexture("texture", temp);
Graphics.Blit(_src, _dst, m_material, pass_index2);
temp.DiscardContents(need_discard_color, need_discard_depth);
....
Graphics.Blit(_dst, temp, m_material, pass_index2);
RenderTexture.ReleaseTemporary(temp);
DiscardContent
其中有一个坑爹的极易让人忽略的地方就是:多次对某一个rt进行多次绘制的时候,绘制之前千万不要忘记擦除RenderTexture!不然会发现效率损耗很大!满脸都是泪。
_CameraDepthTexture – mini G-buffer
当需要深度的时候可以直接在shader中sample这张贴图_CameraDepthTexture,方式如下:
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, screen_position));
float depth01 = Linear01Depth( depth );
float depth_eyelinear = LinearEyeDepth( depth );
要正确获得深度图,需要设置 Camera.DepthTextureMode 属性,把深度渲染打开,可在shader中正确获取深度信息,其余的就参考手册可以得到答案[手册]。当然光有深度图还不行,屏幕坐标也得计算好,有屏幕坐标,就需要像素点大小了,可以通过half4 _MainTex_TexelSize来取得。说到像素点大小,不得不说1diot遇到的坑,一下是内置的pixel perfect的实现,各位自己体会吧。
pixel perfect
inline float4 UnityPixelSnap (float4 pos)
{
float2 hpc = _ScreenParams.xy * 0.5f;
#ifdef UNITY_HALF_TEXEL_OFFSET
float2 hpcO = float2(-0.5f, 0.5f);
#else
float2 hpcO = float2(0,0);
#endif
float2 pixelPos = round ((pos.xy / pos.w) * hpc);
pos.xy = (pixelPos + hpcO) / hpc * pos.w;
return pos;
}
坑爹的replacement shader
在逐步深入post effect的过程中,发现自己定制的对象比如半透明的物体什么的,无法正确渲染出深度。unity 通过一个叫做 replacement shader 的东西来渲染深度图。 unity 选用 camera 设定好的 replacement shader(没有就是默认预设的深度图),对场景上的物体做一些特殊的操作,比如渲染深度什么的。其余的请参考:Rendering with Replaced Shaders
可以在 built-in shader 中找到 Camera-DepthTexture0.shader 所有的代码,它定义了每一种 RenderType 渲染深度的具体 shader 。由于没尝试过定制 replacement shader,就不献丑了。至于为啥没有正确渲染深度,看看默认 transparency 的代码:
fixed4 frag(v2f i) : SV_Target {
fixed4 texcol = tex2D( _MainTex, i.uv );
clip( texcol.a*_Color.a - _Cutoff );
UNITY_OUTPUT_DEPTH(i.depth);
}
观察许久,感触良多。你懂的, _Cutoff 不能变啊!除了 _Color 之外,还有 _DetailTex 、 _MainTex 几个固定参数,最好能在写shader的时候都保持名字不被修改。除了因为在材质切换 shader 的时候参数不会丢失,还因为成 depthmap、particle 使用的 alpha blend支持等特殊特性需要这些默认名字的支持。
参考文献
Accessing shader properties in Cg
Forward Rendering Path Details
Rendering with Replaced Shaders
各种效果的教程
built-in shader package download