How long are |

CatSevenMillion

园龄:2年6个月粉丝:14关注:0

Untiy Shader学习基础(build in管线)

1.渲染流水线

  流水线的任务是从3D模型出发,绘制出一个2D的屏幕场景。

  渲染流水线一共分为三个阶段:1.应用阶段,主要作用是准备好场景数据,执行Culling操作,设置每个模型的渲染状态,输出渲染图元给下一个阶段 2.几何阶段,决定绘制的图元是什么,要怎么样绘制。并将数据变换到屏幕上,将数据与着色等信息传递给下一个阶段 3.光栅化阶段,使用上一个阶段的数据,产生像素渲染到屏幕上。

  在流水线阶段,两个可完全编程也是我们最长使用到的着色器是:顶点着色器(Vertex Shader)片元着色器(Fragment Shader)

  顶点着色器输入来自于CPU,处理的单位是顶点,并且无法创建销毁顶点也无法得知顶点与顶点之间的关系。由于这样的相互独立性,导致了顶点着色器可以并行处理顶点。顶点着色器完成的主要功能是:坐标变换和逐顶点光照。 坐标变换:把顶点坐标从模型空间转换到齐次空间。逐顶点光照:计算顶点颜色。

  片元着色器:片元着色器的输入数据是光栅化阶段中三角形遍历后的数据这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一是纹理采样(要实现纹理采样,需要在顶点着色器阶段输出每个顶点对应的纹理坐标)。片元着色器可以完成很多重要效果,但是它仅可以影响单个片元,也就是说不能把自己的任何结果直接发送给邻居。

  虽然流水线的过程比较复杂,但是Unity为我们封装了很多功能,绝大多数情况下,我们只需要在Untiy Shader中设置输入,编写顶点、片元着色器,设置设置一些状态就可以实现绝大多数想要的功能了。

2.HLSL,GLSL,Cg

  这三个是着色语言,专门用于编写Shader。常见的有DirectX的HLSL,OpenGL的GLSL,NVIDIA的Cg。在Unity中使用Build in渲染管线使用的是Cg语言,URP和HDRP 的Shader Lab 采用的是HLSL语言。

3.Unity Shader

  3.1 材质与Unity Shader

  Unity中我们需要配合材质(Material)和Unty Shader才能实现一个渲染效果,一般基本流程如下:

    1)创建一个材质

    2)创建一个Unity Shader,并把它赋给创建的材质

    3)把材质赋给想要渲染的对象

    4)面板中调整Untiy Shader参数,实现想要的效果

  Unity中的材质需要结合Mesh或者粒子系统才能工作。Unity在创建Shader时会为我们提供多种选项。Unlit Shader会产生一个不包含光照的基本顶点/片元着色器。Image Effect Shader为我们实现各种屏幕后处理效果提供了一个模版。Compute Shader会产生一种特殊的,利用GPU并行性进行一些与常规渲染流水线无关的计算。Standard Surface Shader为我们提供了典型的表面着色器的实现方法。  

  3.2 Shader Lab

  Unity的Shader Lab是Untiy为我们提供的高层次的渲染抽象层。它使用一些嵌套在花括号中的语义来描述一个Unity Shader的文件结构。例如Properties语句块中定义了着色器所需的各种属性。Untiy会将它结构编译成真正的代码和Shader文件,开发者只需要和Untiy Shader打交道。

  一个Unity Shader的基础结构如下:

复制代码
Shader "ShaderName"{
    Properties {
    //属性
    }
    SubShader {
    //显卡A使用的子着色器
    }
    SubShader {
    //显卡B使用的子着色器
    }
    Fallback  "VertexLit"
}
复制代码

   3.3 材质与Shader Lab的桥梁:Properties

  Properties包含了一系列属性,它的语义块定义如下:

Properties {
    Name ("display name", Properties) = DefaultValue
    Name ("display name", Properties) = DefaultValue
    //更多属性
}

  如果我们需要在Shader中访问它们,就需要每个属性的名字(Name),这些名字通常由下划线开始。显示名字(display name)则是出现在材质面板上的名字。除此之外我们还需要指定它们的属性(PropertyType)。每个属性还需要设置默认值

  Properties支持的属性有以下:包括Int,Float,Range数字类型。Vector四维数组。2D,Cube,3D三种纹理类型,默认是字符串+花括号,字符串可以为空或者是“white”“black”“gray”“bump”内置纹理名称。

复制代码
Properties
    {
        _Int ("Int",Int) = 2
        _Float ("Float",Float) = 1.5
        _Range ("Range",Range(0.0,5.0)) = 3.0
        //
        _Color ("Color", Color) = (1,1,1,1)
        _Vector ("Vector",Vector) = (2,3,5,1)
        //
        _2D ("2D",2D) = "" {}
        _Cube ("Cube",Cube) = "white" {}
        _3D ("3D",3D) = "black"{}
    }
复制代码

赋给材质球后效果:

   3.4 重量级成员 SubShader

  每一个Unity Shader文件可以有多个SubShader,但最少一个。Unity在加载时会扫描所有的SubShader语义块,选择一个能够在目标平台上运行的SubShader。如果都不支持的话就会使用FallBack语义指定的Unity Shader。这样做的目的是因为不同显卡有不同的能力,为了兼容更多的设备。

  SubShader语义块定义如下:

复制代码
SubShader {
    // 可选
    [Tags]

    // 可选
    [RenderSetup]

    Pass {

    }
    //Other Pass
}
复制代码

  SubShader中定义了一系列Pass以及可选的状态[RenderSetup]标签[Tags]设置。每个Pass定义了一次完整的渲染流程,但是Pass过多可能会导致渲染性能下降。因此尽可能要减少Pass的使用。

  状态设置:

|状态名称|设置指令|解释|
|–|–|
|Cull|Cull Back | Front |Off|设置裁剪模式|
|ZTest|ZTest Less Greater | LEqual | GEqual | Equal | NotEqual | Always |设置深度测试时使用的函数|
|ZWrite|ZWrite On | Off|开启/关闭深度写入|
|Blend|Blend SrcFactor DstFactor|开启并设置混合模式|

  状态设置可以设置显卡的各种状态,并且设置了的渲染状态会影响到所有的Pass。如果不想这样,我们可以在Pass语义块中单独设置。
  SubShader标签:

   标签是一组键值对,它们用来告诉渲染引擎,我们应该如何渲染这个对象。

  Pass语义块,如下:

Pass {
    [Name]      
    [Tags]
    [RenderSetup]
    //other code
}

  我们可以定义Pass的名称: Name “MyPass” , 通过这个名称我们可以在其他的Unity Shader的Pass :  UsePass “MyShader/MYPASSNAME”

       我们还可以设置Pass的标签和设置,告诉引擎我们应该如何渲染该物体。

  3.5 后路Fallback

  跟在SubShader后面可以是一个Fallback指令。它用来告诉Untiy如果上面所有的SubShader都行不通那就使用这个最低级的Shader。

  语义如下:

Fallback “name”
// 或者
Fallback Off
// 使用最低级Shader
Fallback "VertexLit"

 

当然除此之外,你应当还需要具备矢量,矩阵的一定基础。

  顶点的空间变换过程:

  一个顶点最开始在模型空间上被定义,之后会被变换到世界空间(无限大的一个唯一空间)上,之后会从世界空间变换到观察空间上,我们可以通过平移整个观察空间,让摄像机远点位于世界坐标远点,坐标轴与前者重合。之后从观察空间变换到裁剪空间,裁剪空间的目的是方便地对渲染图元进行裁剪。一般是使用视椎体来决定的,被剔除的部分后续不会被渲染。当所有裁剪操作执行完毕就可以从裁剪空间变换到屏幕空间上了。

  法线变换,法线是需要我们特殊处理的一种方向矢量。游戏中,模型往往一个顶点会携带额外信息,顶点法线就是其中的一种信息。法线在模型变换时可能会有不与面垂直,我们可以利用与切线永远垂直的特性重新确定法线。

 

4.步入Untiy Shader

  4.1简单的顶点片元着色器

  下面是一个简单的顶点片元着色器

复制代码
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Untiy Book/Chapter5/SimpleShader"
{
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            float4 vert(float4 v:POSITION):SV_POSITION{
                return UnityObjectToClipPos(v);
            }
            fixed4 frag():SV_Target {
                return fixed4(1.0,1.0,1.0,1.0);
            }
            ENDCG
        }    
    }
}
复制代码

   这里省略调了Properties语块,因为它并不是必备的,声明了一个SubShader和Pass语块。由于没有任何的标签和渲染设置所以都是使用的默认设置。重点都在CGPROGEAM和ENDCG里

#pragma vertex vert
#pragma fragment frag

这里类似于函数声明,告诉了Unity哪个函数包含了顶点着色器的代码,哪个函数包含了片元着色器的代码。

float4 vert(float4 v:POSITION):SV_POSITION{
       return UnityObjectToClipPos(v);
}

这是顶点着色器的实现部分,它是逐顶点执行的。v包含了这个点位置,通过POSITION指定。返回值是顶点在裁剪空间的位置。POSITION和SV_POSITION都是Cg/HLSH中的语义。SV_POSITION相当于函数返回值类型,告诉Untiy返回值是一个裁剪空间的位置类型是float4。

fixed4 frag():SV_Target {
       return fixed4(1.0,1.0,1.0,1.0);
}
通过上面的学习,我们可以知道它没有输入,返回值是一个裁剪按空间上的渲染目标SV_Target。返回了一个(1,1,1,1)的颜色向量。

  模型数据从哪来?
  我们现在是使用POSITION得到了模型的顶点位置,但是如果想要获得纹理坐标,法线方向等更多模型数据该怎么办呢?我们可以定义一个结构体,然后顶点着色器输入参数不是输入坐标,而是输入这个结构体。如下:
复制代码
struct a2v{
       float4 vertex : POSITION;
       //为模型法线空间的法线填充normal变量
       float3 normal : NORMAL;
       //用模型的第一套纹理坐标填充texcoord变量
       float4 texcoord : TEXCOORD0;
       };
            
       float4 vert(a2v v):SV_POSITION{
           return UnityObjectToClipPos(v.vertex);
}
复制代码

  顶点,片元着色器如何通讯?

  我们可以再定义一个结构体,用来充当顶点着色器的输出,片元着色器的输入。

 

复制代码
struct v2f{
       float4 pos : SV_POSTION;
       fixed3 color : COLOR0;
};
            
v2f vert(a2v v){
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.color = v.normal*0.5 +fixed3(0.5,0.5,0.5);
    return o;
}
fixed4 frag(v2f i):SV_Target {
    return fixed4(i.color,1.0);
}
复制代码

  如何使用属性?

  在前面的代码中我们都没有使用到属性Properties语块,属性相当于可以给我们实时自定义控制的一些参数。所以我们可以修改如下:添加一个Properties语块,添加颜色变量。所有代码如下

 

复制代码
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Untiy Book/Chapter5/SimpleShader"
{
    Properties {
        _Color ("Color Tint", Color) = (1.0,1.0,1.0,1.0)    
    }
    
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            //在Cg代码中,我们需要定义一个与属性名和类型都匹配的变量
            fixed4 _Color;
            
            struct a2v{
                float4 vertex : POSITION;
                //为模型法线空间的法线填充normal变量
                float3 normal : NORMAL;
                //用模型的第一套纹理坐标填充texcoord变量
                float4 texcoord : TEXCOORD0;
            };

            struct v2f{
                float4 pos : SV_POSITION;
                fixed3 color : COLOR0;
            };
            
            v2f vert(a2v v){
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.color = v.normal*0.5 +fixed3(0.5,0.5,0.5);
                return o;
            }
            fixed4 frag(v2f i):SV_Target {
                fixed3 c = i.color;
                c*= _Color.rgb;
                return fixed4(c,1.0);
            }
            ENDCG
        }    
    }
}
复制代码

效果:

   4.2 使用内置文件

  类似于C语言,我们也可以使用一些其他头文件的函数。

 

CGPROGEAM
//..
#include "UntiyCG.cginc"
//..
ENDCG

这里面包含了常用的结构体。还有一些帮助函数,具体可自行查询。

  刚才例子中说到了Cg/HLSL的语义。具体的可以查询DirectX的文档,但值得注意的是Shader并不是支持所有的语义。

  4.3 Shader中的Debug方法

  1.假色彩技术

    指的是采用假色彩技术生成的一种图像。实现方法就是把需要调试的变量映射到[0,1]之间,把它们作为颜色输出到屏幕上,然后通过屏幕上显示的像素值用来判断是否正确。

  2.Frame Debugger

    这是Unity自带的一种针对渲染的调试器,打开Window->Analysis->Frame Debugger。它可以用于查看渲染帧时进行的各种渲染事件。

5.Shader的简洁之道

  5.1 float、half、fixed

  从左到右精度一次降低,当使用时尽可能使用精度较低的类型,因为这可以优化Shader的性能。从大体范围来看,我们可以使用fixed存储颜色和单位矢量,更大范围的数据使用half,最差的情况再使用float。

  5.2 避免不必要的计算

  我们不能无限制的在Shader中,尤其是片元着色器中进行大量计算。因为这会使得需要的临时寄存器或指令数目超出限制。

  5.3 慎用分支和循环语句

  虽然现在的GPU已经发展出可以使用分支语句了,但是GPU使用了不同的底层来处理这些分支语句,对性能的消耗是极大的。所以尽可能的不要在Shader中写分支语句。

  如果一定要使用,建议是把计算都提前交给cpu,gpu接收计算好的数据。或者是分支判断语句中的条件变量最好是常数,分支中的指令数尽可能少,分支嵌套尽可能减少。  

  5.4不要除以0

  同大多数的编程语言一样,这会导致结果不可预期。解决办法是对于那些分母可能为0的情况,强制截取到非0范围,例如设置一个0.000001用来保证分母大于0

 

  

 

 

本文作者:CatSevenMillion

本文链接:https://www.cnblogs.com/CatSevenMillion/p/17534466.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   CatSevenMillion  阅读(187)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起