unity custom shader 102

作者: goteet | 来源:发表于2014-11-27 21:47 被阅读1233次

    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 能够访问到的参数是不一致的。经过不断的测试,得出一些经验。头文件如下:

    • 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_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

    相关文章

      网友评论

      本文标题:unity custom shader 102

      本文链接:https://www.haomeiwen.com/subject/vrowtttx.html