zoukankan      html  css  js  c++  java
  • 翻译2 Unity Shader Fundamentals

    顶点变换.
    Color pixels.
    shader 属性.
    从顶点传数据至片元函数.
    查看编译后的shader代码.

    使用Unity5.6.6f2

    1 默认场景

    新建一个默认场景,新建一个圆球。这个默认场景本身进行了大量复杂的渲染,为了更容易的掌握Unity的渲染过程,我们先做一些简化设置,把,默认的某些花里胡哨的东西先剥离掉。

    1.1 剥离天空盒

    打开Window / Lighting,查看光照设置选项。弹出带有3个选项卡的面板,我们先关注Scene选项卡.

    image

    1-1. 默认光照设置

    第一节Environment是跟环境光照相关,在这里可以设置天空盒。这个Default-Skybox当前正被用于场景的背景光、环境光、和反射光。设置为none就能关闭这些光。就手把下面的Realtime LigtingMixed Lighting也关掉,现在还用不上,后面会陆续介绍。

    关闭了天空盒,环境颜色自动切换为了纯色,这个颜色默认是带着一丝蓝的黑灰色(说好的纯呢,外表很黑内心很蓝?)。而反射光会变成纯黑色。如下所示,设置后球体变暗了,背景变成了纯色。而这个背景深蓝色从哪里来的呢?

    image image

    1-2. 简单光照

    这个深蓝色被定义在摄像机,它默认使用天空盒渲染,当天空盒失效后它会默认退回到使用纯色模式。

    image1-3. 默认的摄像机设置

    为了进一步简化渲染,再隐藏或删除方向光对象。这将消除场景中的直接光照,以及所有它投射的阴影。剩下纯色背景和球体的轮廓。

     1-4. 球体轮廓

    2 图像渲染

    分两步绘制上面的场景,一是使用相机的背景色填充图像,然后再在上面画出球体的轮廓。

    Unity如何知道该画这个球体呢?我们有一个球体对象并且绑定了MeshRenderer组件,如果这个球体位于摄像机的视野内,那么它就会被渲染出来。Unity通过检测球体的边界盒是否与摄像机的视锥体相机来验证这一点。包围盒在Unity中定义为Bounds结构体Collider.bounds, Mesh.bounds and Renderer.bounds.

     2-1. 球体默认自带组件

    Transform组件用于更改坐标、方向,以及网格和包围盒的尺寸。这里有对Transform层次结构的清晰描述。如果一个物体最终处于摄像机视野内,它就会被安排渲染。

    最后,GPU负责渲染物体的mesh。这些具体的渲染指令在物体的material定义好的,这个material引用了一个shader-GPU程序。

     2-2. 各司其职

    当前这个球体使用了Unity的默认材质,自带了一个标准 shader。我们现在把它去掉替换成自己的shader,从头开始写。


    2.1 第一个Shader

    通过点击Assets / Create / Shader / Unlit Shader创建并命名自己的shader,双击shader文件打开,并删除里面的内容从头写。

     2-3. 第一个shader

    Shader是通过shader+关键字定义,关键字是一个字符串,在下拉界面中选择时显示的也是该关键字。它不必与文件名相同。

    Shader "Unlit/MyShader"
    {
    }
    

    保存文件,回到编辑器会收到警告提示none of subshaders/fallbacks are suitable因为它是空的,没有sub-shader或回调shader。尽管这个shader没有内容也有警告,我们仍能指定给material。点击Assets / Create / Material创建,然后通过下拉菜单指定。

     2-4. 给材质指定Shader

    给球体指定上我们新建的Material,替换掉默认的。这时的球体会立即变成紫红色。发生这个的原因是Unity切换到了错误的shader,它故意使用这个颜色来提醒开发者这是一个错误。

    2-5. 指定MyMaterial

    shader warning中提到了没有sub-shader. 我们可以使用sub-shader操作shader变量进行分组, 这允许程序员为不同的编译平台提供不同的sub-shader.例如我们可以用一个sub-shader既支持pc又支持手机平台.定义一个SubShader块

    Shader "Unlit/MyShader"
    {
        SubShader{
    
        }
     }

    sub-shader至少包含一个以上的pass块, pass代码块是物体实际被渲染的地方,我们先写一个pass,然后在写多个pass。为了呈现多种效果,pass数量可能会超过一个以上,而则代表着物体要被渲染多次。

    Shader "Unlit/MyShader"
    {
         SubShader{
            Pass{        }
        }
    }

    我们的球体现在应该变成了白色,因为我们使用了一个空pass渲染,这也意味着我们的Shader没有出现任何错误了。

     2-6. 空shader效果

    2.2 Shader程序

    现在我们要开始编写shader代码了,我们用的Unity着色器语言是HLSL和CG着色器语言的变体。所以必须指示CGPROGRAM关键字为代码的开始,同时要用ENDCG关键字做为结束。

    Pass{
        CGPROGRAM
    
        ENDCG
    }

    再次打开编辑器编译后有一个警告Both vertex and fragment programs must be present没有顶点和片元程序,shader由这两个程序组成,vertex顶点程序负责处理网格的顶点数据,这包含了从对象空间到显示空间的转换。看这里。而fragment片元程序负责为位于网格的三角形内的单个像素着色。

    2-7. 顶点和片元程序

    我们必须通过pragma指令告诉编译器使用哪些程序

    CGPROGRAM
    
    #pragma vertex MyVertexProgram
    #pragma fragment MyFragmentProgram
    
    ENDCG
    

    编译器再次发出来错误提示,这次是因为它不能找到我们指定的程序片段,因为我们光声明没实现。首先vertex和fragment被写成方法,类似C#,尽管他们被称之为函数。先简单地创建两个同名的void方法。

    CGPROGRAM
    
    #pragma vertex MyVertexProgram
    #pragma fragment MyFragmentProgram
    
    void MyVertexProgram() {
    
    }
    
    void MyFragmentProgram() {
    
    }
    
    ENDCG
    

    这次编译后没有报错,但是球体从屏幕上消失了。


    2.3 Shader汇编

    Unity的shader编译器把我们的代码转换成了不同程序,这取决于目标平台。不同的平台需要不同的解决方案,例如Direct3D是服务于Windows平台,OpenGL针对MacOs,OpenGL ES针对手机平台。这里我们不是在处理单个编译器,而是多个编译器。

    最终使用哪种编译器取决于目标平台,这些编译器是不完全相同的,每个平台可能得到不同的结果。在这个例子中,我们的空程序在OpenGL和Direct3D 11下能很好的工作,但在Direct3D 9就会报错。

    在编辑器下点选MyFirstShader,在监视面板可以查看该shader的一些信息,以及编译错误。这也有一个Compiled code入口,该入口Compile and show code按钮和下拉菜单。如果你点击该按钮,Unity将会编译该shader并打开它,接着就可以查看生成的代码。

    2-8. shader检视面板信息

    2-9. 目标平台编译

    我们试着先选择OpenGL Core,然后再选择D3D11,看看底层代码是怎么回事的。

    OpenGL Core

    Shader "Unlit/MyShader" {
    SubShader {
      Pass {
     No keywords set in this variant.
     -- Vertex shader for "glcore":
     Shader Disassembly:
     #ifdef VERTEX
     #version 150
     #extension GL_ARB_explicit_attrib_location : require
     #extension GL_ARB_shader_bit_encoding : enable
    
    void main()
     {
         return;
     }
    
    #endif
     #ifdef FRAGMENT
     #version 150
     #extension GL_ARB_explicit_attrib_location : require
     #extension GL_ARB_shader_bit_encoding : enable
    
    void main()
     {
         return;
     }
    
    #endif
    
    
    -- Fragment shader for "glcore":
     Shader Disassembly:
     // All GLSL source is contained within the vertex program
    
     }
     }
     }
    

    提炼出两个main函数,有vertex和fragment程序

    #ifdef VERTEX
    void main()
    {
        return;
    }
    #endif
    #ifdef FRAGMENT
    void main()
    {
        return;
    }
    #endif

    D3D11自行查看,因为编译后的代码实在是太长了,不方便贴上来。只选取了一个片段:

    Pass {
    
    No keywords set in this variant.
    -- Vertex shader for "d3d11":
    Shader Disassembly:
          vs_4_0
       0: ret
    
    
    -- Fragment shader for "d3d11":
    Shader Disassembly:
          ps_4_0
       0: ret
    
    }


    2.4 引入其他文件

    编写shader代码很费劲,有时需要重复写类似的函数,为了简化书写,这里有一个类似C#程序的功能,引用其他类中的通用变量、函数等。使用#include指令就能加载一个文件。先试着加载Unity内部自带的UnityCG.cginc

    CGPROGRAM
    
        #pragma vertex MyVertexProgram
        #pragma fragment MyFragmentProgram
    
        #include "UnityCG.cginc"
    
        void MyVertexProgram() {
    
        }
    
        void MyFragmentProgram() {
    
        }
    
    ENDCG
    

    下面是UnityCg.cginc的引用层次结构

    2-10. UnityCG.cginc结构

    UnityShaderVariables.cginc定义了一大堆渲染所需的着色器变量,比如转换、相机和光zhao数据。这些都是由Unity在需要时设置的。

    UnityInstancing.cginc专门用于实例化支持,这是一种减少绘制调用的特定呈现技术。虽然它不直接包含文件,但它依赖于UnityShaderVariables。

    HLSLSupport.cginc设置了一些无论您的目标是哪个平台都可以使用相同的代码的功能。因此,您不必担心使用特定平台的数据类型等问题。

    请注意,这些文件的内容将被复制到文件中,取代include指令。这发生在预处理步骤中,该步骤执行所有预处理指令。比如#include和#pragma。


    2.5 产生输出( 语义 )

    为了渲染物体,shader必须要产生结果。

    Vertex顶点函数必须要返回最终的顶点坐标:SV_POSITION。一个顶点有几个坐标分量?4个,因为我们使用了4x4变换矩阵。现在把函数类型从void改为float4,一个float4类型是一个由4个float类型简单组成。

    float4 MyVertexProgram( ) : SV_POSITION{
        return 0;
    }

    Fragment片元函数返回像素的最终颜色:SV_TARGET。同理float4。

    float4 MyFragmentProgram( ) : SV_TARGET{
        return 0;
    }

    Vertex顶点函数的输出作为Fragment片元函数的输入。输入的参数需要匹配语义!

    float4 MyFragmentProgram
    (
        float4 position : SV_POSITION
    ) : SV_TARGET {
        return 0;
    }

    然后看看Unity的shader汇编

    //--------------D3D11-----------------
    -- Vertex shader for "d3d11":
    Shader Disassembly:
          vs_4_0                            //顶点着色器版本
          dcl_output_siv o0.xyzw, position  //声明o0作为输出值,带有系统值
       0: mov o0.xyzw, l(0,0,0,0)           //把(0,0,0,0)移动到o0中
       1: ret                               //返回
    
    -- Fragment shader for "d3d11":
    Shader Disassembly:
          ps_4_0
          dcl_output o0.xyzw
       0: mov o0.xyzw, l(0,0,0,0)
       1: ret
    
    //---------------GL CORE-----------
    #ifdef VERTEX
    void main()
    {
        gl_Position = vec4(0.0, 0.0, 0.0, 0.0);
        return;
    }
    #endif
    
    #ifdef FRAGMENT
    layout(location = 0) out vec4 SV_TARGET0;
    void main()
    {
        SV_TARGET0 = vec4(0.0, 0.0, 0.0, 0.0);
        return;
    }
    #endif

    2.6 顶点变换

    把球给我画出来!

    为了得到模型空间的顶点坐标,给Vertex顶点函数增加一条语义:POSITON。而模型空间的顶点坐标是其次坐标。先直接返回这个顶点坐标,贴汇编:

    //----D3d11-------
          vs_4_0                    //版本
          dcl_input v0.xyzw            //申明v0 输入系统值
          dcl_output_siv o0.xyzw, position    //申明o0 输出系统值
       0: mov o0.xyzw, v0.xyzw            //把v0值 移动到 o0
       1: ret
    
    //---GL CORE---
    #ifdef VERTEX
    in  vec4 in_POSITION0;
    void main()
    {
        gl_Position = in_POSITION0;
        return;
    }
    #endifView Code


    2-11. 扭曲的球

    使用MVP:model_view_projection矩阵变换顶点坐标,定义在UnityShaderVariables文件,变量名是UNITY_MATRIX_MVP。改为:

    return mul(UNITY_MATRIX_MVP, position);贴汇编看看
    -- Vertex shader for "d3d11":
    // Stats: 8 math
    Uses vertex data channel "Vertex"
    //cbuffers常量数据
    Constant Buffer "UnityPerDraw" (160 bytes) on slot 0 {
      Matrix4x4 unity_ObjectToWorld at 0
    }
    Constant Buffer "UnityPerFrame" (384 bytes) on slot 1 {
      Matrix4x4 unity_MatrixVP at 272
    }
    
    Shader Disassembly:
          vs_4_0                            //版本
          dcl_constantbuffer CB0[4], immediateIndexed    //声明常量缓冲区cbuffers,逐字索引
          dcl_constantbuffer CB1[21], immediateIndexed    //cbuffers
          dcl_input v0.xyz                    //声明输入v0
          dcl_output_siv o0.xyzw, position            //声明输入o0
          dcl_temps 2                        //声明临时寄存器2个(r0-r1)
       0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw            //将v0与cb0[1]相乘传递给r0
    
    //dest.x = cb0[0].x * v0.x + r0.x;
    //dest.y = cb0[0].y * v0.x + r0.y;
    //dest.z = cb0[0].z * v0.x + r0.z;
    //dest.w = cb0[0].w * v0.x + r0.w;
       1: mad r0.xyzw, cb0[0].xyzw, v0.xxxx, r0.xyzw    //前两相乘结果与3相加
       2: mad r0.xyzw, cb0[2].xyzw, v0.zzzz, r0.xyzw    //同理1:
    
       3: add r0.xyzw, r0.xyzw, cb0[3].xyzw            //r0+cb0[3]传递给r0
       4: mul r1.xyzw, r0.yyyy, cb1[18].xyzw            //相乘
       5: mad r1.xyzw, cb1[17].xyzw, r0.xxxx, r1.xyzw    //同理1:
       6: mad r1.xyzw, cb1[19].xyzw, r0.zzzz, r1.xyzw    //同理1:
       7: mad o0.xyzw, cb1[20].xyzw, r0.wwww, r1.xyzw    //同理1:
       8: retView Code


    2-12. 正确的球

    3 像素颜色

    先给Fragment函数返回点东西,

    float4 MyFragmentProgram(float4 position : SV_POSITION) : SV_TARGET{
        return float4(1, 1, 0, 1);
    }

    image

    3-1. 黄色球

    3.1 Shader 属性: Properties

    image

    3-2. 结构

    image

    3-3. 解释

    3.2 使用属性

    需要在pass块内声明一个同类型的同命名变量            float4 _Tint;

    float4 MyFragmentProgram(float4 position : SV_POSITION) : SV_TARGET{
        return _Tint;
    }

    看看片元函数的汇编

    -- Fragment shader for "d3d11":
    Constant Buffer "$Globals" (48 bytes) on slot 0 {
      Vector4 _Tint at 32
    }
    
    Shader Disassembly:
          ps_4_0
          dcl_constantbuffer CB0[3], immediateIndexed
          dcl_output o0.xyzw
       0: mov o0.xyzw, cb0[2].xyzw
       1: retView Code

    image

    3-4. 纯色

    3.3 从顶点到片元

    图3-4纯色球,每个像素都是同一个颜色,但是美术给的效果图是五彩斑斓的,就需要GPU光栅化三角形,取三个处理过的顶点进行插值,找到三角形内所有像素并着色

    image

    3-5. 插值数据传递

    又3-5知:处理过的顶点数据不直接传递给Fragment片元函数,而在片元函数中访问插值本地数据,需要增加一个参数,并指定语义:TEXCOORD0.它表示贴图的UV坐标。

    float4 MyVertexProgram
    (
        float4 position: POSITION,
        out float3 localPosition : TEXCOORD0
    ) : SV_POSITION{
        localPosition = position.xyz;
        return UnityObjectToClipPos(position);
    }
    
    float4 MyFragmentProgram
    (
        float4 position : SV_POSITION,
        float3 localPosition : TEXCOORD0
    ) : SV_TARGET
    {
        return float4(localPosition, 1);
    }

    image

    3-6. 插值本地数据作为颜色


    3.4 结构体

    简化!

    struct Interpolators{
        float4 position : SV_POSITION;
        float3 localPosition : TEXCOORD0;
    };
    
    Interpolators MyVertexProgram (float4 position: POSITION ){
        Interpolators i;
        i.localPosition = position.xyz;
        i.position = UnityObjectToClipPos(position);
         return i;
    }
    
    float4 MyFragmentProgram (Interpolators i) : SV_TARGET
    {
        return float4(i.localPosition, 1);
     }

    UnityObjectToClipPos是Unity5.6之后的优化:

    它对应mul(UNITY_MATRIX_MVP, v.vertex),但是该函数使用了常数1作为第四个坐标而不是依赖网格数据,源码:
    inline float4 UnityObjectToClipPosInstanced(in float3 pos)
    {
        return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorldArray[unity_InstanceID], float4(pos, 1.0)));
    }
    因为通过网格提供的数始终为1,但是编译器不能知晓。所幸干脆就直接写死为1.0,优化掉运行时再去计算第四个数到底是多少这一步。

    4 原文翻译

    文章很好,赞助作者去吧!

  • 相关阅读:
    找到了2年前的一个微博小号
    Float Equal Problem
    有用的护肤品贴
    最近状态总结
    [Coursera]Machine Learning
    KMP算法(转载)
    [Leetcode] Median of Two Sorted Arrays
    [Algorithms(Princeton)] Week1
    [Algorithms(Princeton)] Week1
    [Leetcode] Binary Tree Maximum Path Sum
  • 原文地址:https://www.cnblogs.com/baolong-chen/p/12122476.html
Copyright © 2011-2022 走看看