何为shader

shader中文翻译为着色器,其实是一种用来渲染图形的技术,我们可以通过shader编程,来自定义显卡渲染画面的算法,显示我们所期望的结果。

shader种类

  • 顶点着色器: 处理每个顶点,将顶点的空间位置投影在屏幕上,即计算顶点的二维坐标。同时,它也负责顶点的深度缓冲(Z-Buffer)的计算。顶点着色器可以掌控顶点的位置、颜色和纹理坐标等属性,但无法生成新的顶点。顶点着色器的输出传递到流水线的下一步。如果有之后定义了几何着色器,则几何着色器会处理顶点着色器的输出数据,否则,光栅化器继续流水线任务。

  • 像素着色器(Direct3D),常常又称为片断着色器(OpenGL): 处理来自光栅化器的数据。光栅化器已经将多边形填满并通过流水线传送至像素着色器,后者逐像素计算颜色。像素着色器常用来处理场景光照和与之相关的效果,如凸凹纹理映射和调色。名称片断着色器似乎更为准确,因为对于着色器的调用和屏幕上像素的显示并非一一对应。举个例子,对于一个像素,片断着色器可能会被调用若干次来决定它最终的颜色,那些被遮挡的物体也会被计算,直到最后的深度缓冲才将各物体前后排序。

  • 几何着色器: 可以从多边形网格中增删顶点。它能够执行对CPU来说过于繁重的生成几何结构和增加模型细节的工作。Direct3D版本10增加了支持几何着色器的API, 成为Shader Model 4.0的组成部分。OpenGL只可通过它的一个插件来使用几何着色器。

shader编程语言

shader并不是一个统一的标准,不同的图形接口的shader并不相同。目前主流的有三种语言:

  • 基于OpenGL的OpenGL Shading Language, 简称GLSL。
  • 基于DirectX的High Level Shading Language, 简称HLSL。
  • NVIDIA公司的C for Graphic, 简称Cg语言。

GLSL与HLSL分别是基于OpenGL和DirectX的接口,两者不能混用。而Cg语言是用于图形的C语言。

Cg语言是Microsoft和NVIDIA相互协作在标准硬件光照语言的语法和语义达成一致,所以HLSL和Cg其实是同一种语言。一般来说为了跨平台,学Cg更通用点。因为Cg/HLSL有更好的跨平台性,更倾向于使用Cg/HLSL来编写Shader程序,官方的建议也是用Cg/HLSL来编写。

Unity Shader

何为 Unity Shader

Unity Shader严格来说并不是传统上的Shader,而是Unity自身封装后的一种便于书写的Shader,又称为ShaderLab

Unity Shader 种类

  • Surface Shaders(表面着色器): 是Unity提出的一个概念,其实就是Unity对Vertex/Fragment Shader的又一层包装,以使Shader的制作方式更符合人类的思维模式,同时可以以极少的代码来完成不同的光照模型与不同平台下需要考虑的事情。编写着色器与光照的交互是复杂的,光源有很多类型,不同的阴影选项,不同的渲染路径(正向和延时渲染),表面着色器将这一部分简化。Unity建议使用表面着色器来编写和光照有关的Shader。

  • Vertex/Fragment Shaders(顶点/片断着色器): 和OpenGL,Direct3D中的顶点着色器和片段着色器没有什么区别。顶点片段着色器比表面着色器使用更自由也更强大,当然光照需要自行处理。Unity也允许在里面编写几何着色器,一般用得不多。

  • Fixed Function Shaders(固定管线着色器): 已经被淘汰

Surface Shader也有它的局限性,就是Vertex/Fragment Shader能实现的效果,Surface Shader不一定能实现,反过来则成立,Surface Shader能实现的Vertex/Fragment Shader则一定可以实现。

Unity Shader 程序结构

Unity Shader 基本结构如下图:

在Unity中创建一个Shader, 得到如下代码

unity shader
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Custom/Diffuse Texture"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf CustomDiffuse

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;

            //3
            float2 uv_Bump;
            float3 worldNormal; INTERNAL_DATA
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutput o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }

        inline float4 LightingCustomDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
        {
            float difLight = dot(s.Normal, lightDir);
            float hLambert = difLight * 0.5 + 0.5;

            float4 col;
            col.rgb = s.Albedo * _LightColor0.rgb * (difLight * atten * 2);
            col.a = s.Alpha;
            return col;
        }
       
        ENDCG
    }
    FallBack "Diffuse"
}

Property (属性定义)

在Properties{}中定义着色器属性,在这里定义的属性将被作为输入提供给所有的子着色器。每一条属性的定义的语法如下:

_Name("Display Name", type) = defaultValue[{options}]
  • _Name: 属性的名字,简单说就是变量名,在之后整个Shader代码中将使用这个名字来获取该属性的内容
  • Display Name - 这个字符串将显示在Unity的材质编辑器中作为Shader的使用者可读的内容
  • type - 这个属性的类型,可能的type所表示的内容有以下几种
    • Color - 一种颜色,由RGBA(红绿蓝和透明度)四个量来定义;
    • 2D - 一张2的阶数大小(256,512之类)的贴图。这张贴图将在采样后被转为对应基于模型UV的每个像素的颜色,最终被显示出来;
    • Rect - 一个非2阶数大小的贴图;
    • Cube - 即Cube map texture(立方体纹理),简单说就是6张有联系的2D贴图的组合,主要用来做反射效果(比如天空盒和动态反射),也会被转换为对应点的采样;
    • Range(min, max) - 一个介于最小值和最大值之间的浮点数,一般用来当作调整Shader某些特性的参数(比如透明度渲染的截止值可以是从0至1的值等);
    • Float - 任意一个浮点数;
    • Vector - 一个四维数;
  • defaultValue: 定义了这个属性的默认值,通过输入一个符合格式的默认值来指定对应属性的初始值
    • Color - 以0~1定义的rgba颜色,比如(1,1,1,1);
    • 2D/Rect/Cube - 对于贴图来说,默认值可以为一个代表默认tint颜色的字符串,可以是空字符串或者”white”,”black”,”gray”,”bump”中的一个
    • Float,Range - 某个指定的浮点数
    • Vector - 一个4维数,写为 (x,y,z,w)
  • {option}: 它只对2D,Rect或者Cube贴图有关,在写输入时我们最少要在贴图之后写一对什么都不含的空白的{},当我们需要打开特定选项时可以把其写在这对花括号内。如果需要同时打开多个选项,可以使用空白分隔。

一组属性的例子

_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0

SubShader(子着色器)

一个Shader可以有多个子着色器,这些子着色器互不相干且只有一个会在最终的平台运行,编写多个的目的是解决兼容性问题,Unity会自己选择兼容终端平台的Shader运行。 语法如下:

Subshader { [Tags] [CommonState] Passdef [Passdef ...] }

Tags

表面着色器可以被若干的标签(tags)所修饰,而硬件将通过判定这些标签来决定什么时候调用该着色器。比如我们的例子中SubShader的第一句

Tags { "RenderType"="Opaque" }

Unity定义了一些列这样的渲染过程, 使用不同的标签来调用这些过程

  • “RenderType”=“Opaque”: 表示系统应该在渲染非透明物体时调用我们
  • “RenderType” = “Transparent” 表示渲染含有透明效果的物体时调用

其他常用标签:

  • “IgnoreProjector”=“True”: 不被Projectors影响
  • “ForceNoShadowCasting”=“True”: 从不产生阴影
  • “Queue”=“xxx”: 指定渲染顺序队列

如果你使用Unity做过一些透明和不透明物体的混合的话,很可能已经遇到过不透明物体无法呈现在透明物体之后的情况,这种情况很可能是由于Shader的渲染顺序不正确导致的。Queue指定了物体的渲染顺序 预定义的Queue有:

  • Background - 最早被调用的渲染,用来渲染天空盒或者背景. Background = 1000;
  • Geometry - 这是默认值,用来渲染非透明物体(普通情况下,场景中的绝大多数物体应该是非透明的).Geometry = 2000;
  • AlphaTest - 用来渲染经过Alpha Test的像素,单独为AlphaTest设定一个Queue是出于对效率的考虑. AlphaTest = 2450;
  • Transparent - 以从后往前的顺序渲染透明物体. Transparent = 3000;
  • Overlay - 用来渲染叠加的效果,是渲染的最后阶段(比如镜头光晕等特效). Overlay = 4000;

这些预定义的值本质上是一组定义整数, 在我们实际设置Queue值时,不仅能使用上面的几个预定义值, 我们也可以指定自己的Queue值,写成类似这样:“Queue”=“Transparent+100”,表示一个在Transparent之后100的Queue上进行调用.

LOD

LOD很简单,它是Level of Detail的缩写,在这里例子里我们指定了其为200(其实这是Unity的内建Diffuse着色器的设定值)。

这个数值决定了我们能用什么样的Shader。在Unity的Quality Settings中我们可以设定允许的最大LOD,当设定的LOD小于SubShader所指定的LOD时,这个SubShader将不可用.

  • VertexLit及其系列 = 100
  • Decal, Reflective VertexLit = 150
  • Diffuse = 200
  • Diffuse Detail, Reflective Bumped Unlit, Reflective Bumped VertexLit = 250
  • Bumped, Specular = 300
  • Bumped Specular = 400
  • Parallax = 500
  • Parallax Specular = 600

Shader本体

为了方便,我把上面的shader本体复制下来

代码
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf CustomDiffuse
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
    float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
    // put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutput o)
{
    // Albedo comes from a texture tinted by color
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}
inline float4 LightingCustomDiffuse (SurfaceOutput s, fixed3 lightDir, fixed atten)
{
    float difLight = dot(s.Normal, lightDir);
    float hLambert = difLight * 0.5 + 0.5;
    float4 col;
    col.rgb = s.Albedo * _LightColor0.rgb * (difLight * atten * 2);
    col.a = s.Alpha;
    return col;
}

ENDCG

  1. CGPROGRAM 表示CG程序的开始, ENDCG表示CG程序的结束
  2. #pragma surface surf CustomDiffuse 编译指令,它声明了我们要写一个表面Shader,并指定了光照模型。它的写法是这样的
#pragma surface surfaceFunction lightModel [optionalparams]
  • surface: 声明的是一个表面着色器
  • surfaceFunction: 着色器代码的方法的名字
  • lightModel: 使用的光照模型。
  1. sampler2D _MainTex 在CG中,sampler2D就是和texture所绑定的一个数据容器接口。更简单地理解,sampler2D就是GLSL中的2D贴图的类型,相应的,还有sampler1D,sampler3D,samplerCube等等格式

在Properties中已经声明了_MainTex, 为什么还需要再声明一次? 其原因是因为在CGPROGRAM….ENDCG这段代码块中,这是一段CG程序。 要想访问在Properties中所定义的变量的话,必须使用和之前变量相同的名字进行声明。于是其实sampler2D _MainTex; 做的事情就是再次声明并链接了_MainTex,使得接下来的CG程序能够使用这个变量

  1. struct Input
struct Input
{
    float2 uv_MainTex;
};
  • 定义了一个 Input结构体, 作为输入的结构体必须命名为Input。
  • float和vec都可以在之后加入一个2到4的数字,来表示被打包在一起的2到4个同类型数.
  • uv_MainTex: 包含两个浮点数的变量
    • UV mapping的作用是将一个2D贴图上的点按照一定规则映射到3D模型上,是3D渲染中最常见的一种顶点处理手段。
    • 在CG程序中,我们有这样的约定,在一个贴图变量(在我们例子中是_MainTex)之前加上uv两个字母,就代表提取它的uv值(其实就是两个代表贴图上点的二维坐标 )。
    • 我们之后就可以在surf程序中直接通过访问uv_MainTex来取得这张贴图当前需要计算的点的坐标值了。
  1. 函数surf
void surf (Input IN, inout SurfaceOutput o)
{
    // Albedo comes from a texture tinted by color
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}
  • tex2D函数: CG程序中用来在一张贴图中对一个点进行采样的方法,返回一个float4
  • 这里对_MainTex在输入点上进行了采样,并将其颜色的rbg值赋予了输出的像素颜色,将a值赋予透明度。
  • 着色器工作内容:找到贴图上对应的uv点,直接使用颜色信息来进行着色。

Fallback(回滚)

如果子着色器在终端平台上都无法运行,那么使用Fallback指定的备用Shader,俗称备胎。 语法如下:

Fallback "name"

Pass

一个Pass就是一次绘制。对于表面着色器,只能有一个Pass,所以不存在Pass节。顶点片段着色器可以有多个Pass。多次Pass可以实现很多特殊效果,例如当人物被环境遮挡时还可以看到人物轮廓就可以用多Pass来实现。 语法如下:

Pass { [Name and Tags] [RenderSetup] }

参考

--完--