UnityUnity Shader分享Unity游戏开发入门

unity custom shader 102

2014-11-27  本文已影响1233人  goteet

Trick


Name of Property

在 unity 提供的默认 shader 中,properties 的名称都是固定的,当材质切换 shader 的时候,unity 会根据相同的 properties 词条名称将原有状态赋值到下一个状态中。

same_name_of_property

特别是美术贴图的设置,设置起来比较麻烦,所以维持一些 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 能够访问到的参数是不一致的。经过不断的测试,得出一些经验。头文件如下:

想要访问 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 :

其实 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_4LightPosX0unity_4LightPosY0unity_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

上一篇 下一篇

猜你喜欢

热点阅读