Unity之Shader基础探索

一、什么是Shader?

Shader又名为着色器,是渲染管线运行的一段小程序,负责通知GPU如何渲染图形。那问题又来了,什么是渲染管线呢?通过学习收集资料发现,渲染管线也称渲染流水线,是显示芯片(提供显示功能的芯片,是显卡上面的一个最重要的芯片,主要处理显卡的计算工作,如同电脑中cpu的地位)内部处理图形信号相互独立的并行处理单元,渲染管线可以分为,固定渲染管线、可编程渲染管线。。。

1.Shader的开发语言

HLSL: 主要用于Direct3D。平台:windows。

GLSL: 主要用于OpenGL。 平台:移动平台(iOS,安卓),mac(only use when you target Mac OS X or OpenGL ES 2.0)

CG:与DirectX 9.0以上以及OpenGL 完全兼容。运行时或事先编译成GPU汇编代码。
CG比HLSL、GLSL支持更多的平台,Unity Shader采用CG/HLSL作为开发语言。

2.着色器用途

(1) 作为图形管线一部分的着色器是最常见的着色器类型。它们执行一些计算来确定屏幕上像素的颜色。在 Unity 中,通常是通过 Shader 对象使用这种类型的着色器。

(2) 计算着色器在常规图形管线之外,在 GPU 上执行计算。

(3) 光线追踪着色器执行与光线追踪相关的计算。

3.着色器的编辑

(1) ShaderLab-一种用于编写着色起的Unity特定语言。

(2)Shader Graph-一种无需编写代码即可创建着色器的工具。

4.着色器性能分析工具

https://developer.imaginationtech.com/pvrshadereditor/

5.着色器编译

每次构建项目时,Unity 编辑器都会编译构建所需的所有着色器:针对每个所需的图形 API 编译每个所需的着色器变体。

当您在 Unity 编辑器中工作时,编辑器不会提前编译所有内容。这是因为为每个图形 API 编译每个变体可能需要很长时间。

相反,Unity 编辑器会这样做:

当导入一个着色器资源时,会执行一些最小的处理(例如表面着色器生成)。
当需要显示着色器变体时,它会检查 Library/ShaderCache 文件夹。
如果找到使用相同源代码的先前编译的着色器变体,则会使用该着色器变体。
如果没有找到匹配项,则编译所需的着色器变体并将结果保存到缓存中。
注意:如果您启用异步着色器编译,它在后台执行此操作并同时显示占位着色器。

6.异步着色器的编译工作原理

(1).当编辑器第一次遇到未编译的着色器变体时,它会将着色器变体添加到作业线程上的编译队列中。编辑器右下角的进度条会显示编译队列的状态。
(2).在加载着色器变体时,编辑器使用占位着色器渲染几何体,该着色器显示为纯青色。
(3).当编辑器完成对着色器变体的编译后,它会使用着色器变体来渲染几何体。

7.内置着色器中的着色器替换标签

Opaque:大部分着色器(法线、自发光、反射和地形着色器)。
Transparent:大部分半透明着色器(透明、粒子、字体和地形附加通道着色器)。
TransparentCutout:遮罩透明度着色器(透明镂空、两个通道植被着色器)。
Background:天空盒着色器。
Overlay:光环、光晕着色器。
TreeOpaque:地形引擎树皮。
TreeTransparentCutout:地形引擎树叶。
TreeBillboard:地形引擎公告牌树。
Grass:地形引擎草。
GrassBillboard:地形引擎公告牌草。

二、固定渲染管线

只提供了一些渲染功能的开关项,不能灵活控制渲染的每个片段,是OpenGL ES 1.0所使用的渲染管线,从OpenGL ES 2.0开始,Unity全面支持可编程渲染管线,现在已经不建议使用固定渲染管线,在这里做个简单了解。

Shader "Unlit/FixedShader"    //表示Shader的显示目录与shader代码语块
{
    
    
	Properties				  //Shader的属性部分,这里配置渲染管线需要用到的参数
	{
    
    
		//主纹理
		_MainTex("Texture",2D) = "white" {
    
    }
	}
	SubShader //子着色器,Shader可以包含多个子着色器,Unity会自动找到当前设备硬件支持的SubShader执行
	{
    
    
		Pass  //渲染通道,可以添加多个Pass,由于一个Pass就是一个DrawCall,所以尽量只使用一个Pass。
		{
    
    
			//设置主纹理
			SetTexture [_MainTex]
			{
    
    
				combine texture
			}
		}
	}
}
Shader "Unlit/FixedShader01"
{
    
    
	Properties
	{
    
    
		//主纹理
		_MainTex("Texture",2D) = "white" {
    
    }
		//颜色
		_Color("Main Color",Color) = (1,1,1,0)
	}
	SubShader //子着色器,Shader可以包含多个子着色器,Unity会自动找到当前设备硬件支持的SubShader执行
	{
    
    
		Pass  //渲染通道,可以添加多个Pass,由于一个Pass就是一个DrawCall,所以尽量只使用一个Pass。
		{
    
    
			Lighting On
			Material
			{
    
    
				Diffuse [_Color] //接收漫反射光
				Ambient [_Color] //接收环境光
			}

			//设置主纹理
			SetTexture [_MainTex]
			{
    
    
				combine previous*texture
			}
		}
	}

三、可编程渲染管线

程序代码可以对每一个片段进行着色,如果要对每个片段像素点做特殊着色,那么在Shader中首先需要获取集合图形对应的顶点以及UV信息,然后通过UV以及贴图拿到当前片段的像素信息,然后就可以自定义着色了。

Shader "Unlit/VertexandFragmentShader"
{
    
    
	Properties
	{
    
    
		_MainTex("Texture", 2D)="White"{
    
    }
	}
	{
    
    
		Pass
		{
    
    
			//标记CG程序块的起始位置
			//c++语法
			CGPROGRAM
			#pragma vertex vert
			#include "UnityCG.cginc"
			struct v2f
			{
    
    
				float2 uv : TEXCOORD0;
				float4 vertex : SV_POSITION;
			} ;
			sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata_base v)
            {
    
    
            	//输出几何图形顶点以及UV信息
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                // 输入自定义着色信息
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            //标记CG程序块的结束
            ENDCG
		}
	}
}

四、可编程渲染管线的表面着色器

该着色器可以省略编写#pragma vertex vert方法,并且Shader中不需要写Pass代码块,#pragma surface surf Lambert表示执行光照模型,SurfaceOutput 表示 vertex 输出的结构对象。

Shader "Custom/NewSurfaceShader"
{
    
    
    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
        //基于物理的标准照明模型,并在所有灯光类型上启用阴影
        #pragma surface surf Standard fullforwardshadows

        //使用shader模型3.0目标,以得到更好的外观照明
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
    
    
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
        // 在这里放置更多的每个实例属性
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
    
    
            // 反照率来自于由颜色着色的纹理
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // 金属度与光滑度
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

SurfaceOutPut的结构定义:

struct SurfaceOutput
{
    
    
	fixed3 Albedo;
	fixed3 Normal;
	fixed3 Emission;
	half Specular;
	fixed Gloss;
	fixed Alpha;
};

五、 深度排序

模型之间存在遮挡关系,因此需要设置模型间的渲染顺序,这可以在Shader中表明,如Tags{“RenderType”=“Opaque”}(Opaque: 用于大多数着色器(法线着色器、自发光着色器、反射着色器以及地形的着色器)。),共分为5个类型(数值越小,越先渲染):

1.Background: 代表1000,天空盒或背景,其它元素都盖在它前面。
2.Geometry: 代表2000,几何体、地形、地上的房子、树木等不需要带透明通道的模型。
3.AlphaTest:代表2450,透明测试(透明测试开启时,当前像素根据设定条件决定是否输出颜色)。
4.Transparent:代表3000,透明或半透明的模型。
5.Overlay:代表4000,渲染在最前面,比如UI一类。

这些数值可以直接对其进行加减,也可以对其进行二次编辑。如Tags{“RenderType”=“Geometry+1”}。
技巧分享:在游戏地形之上,还会绘制很多建筑一类的元素,如果先绘制地形再绘制建筑的话,那么重合的像素点就需要画很多遍,所以可以将地形的RenderType值设置的比地上建筑大,这样就会先绘制建筑,然后再绘制地形。

可以在Window->Analysis->Frame Debug窗口中依次查看当前的渲染顺序:
FrameDebug

六、透明

在Shader中可以用Alpha Test和Alpha Blend这两种方式实现透明效果,Alpha Blend透明部分会和背景混合,Alpha Test不会,它只会出现透明和不透明两种结果,Alpha Test无法做混合,由于移动平台下不支持Early-Z,它的效率会比Alpha Blend慢,不过游戏中有时会需要用到它,比如实现自身溶解的效果,Alpha Blend使用的场景比较多,比如粒子特效、角色身体、翅膀等发光效果。
游戏中应当减少使用透明通道,因为透明会出现混合的现象,这样的渲染队列必须是从后向前渲染,此时就会出现大量的过度绘制(overdraw)现象。如果是不透明的话,可以将它设置到Geometry上,这样的渲染顺序就会从前向后渲染,因为后面的像素挡住了前面的像素,所以会大量降低过度绘制,总之,能不用透明的地方就不用。
Unity专门提供了几种放在Shader->Mobile下的着色器,它们是专门优化过的:
在这里插入图片描述

七、裁切

Shader中的Stencil(模板),它与深度测试比较像,测试能否写入像素,测试成功后写入像素。Stencil也可以做裁切,在裁切与被裁切的Properties代码块中添加一个唯一标识的ID:

Properties
{
    
    
	_MainTex{
    
    "Texture",2D} = "white" {
    
    }
	_ID{
    
    "Mask ID",Int} = 1
}

在需要裁切的模型上写入Stencil,其中Ref[_ID]表示唯一标识符,Comp equal用来和裁切区域比较是否显示这个像素:

Stencil
{
    
    
	Ref {
    
    _ID}
	Comp equal
}

此外,还需要设置一个裁切区域,其中Ref[_ID]和裁切模型匹配,Comp always和Pass replace表示在这个区域内的像素永远显示,否则将被裁切掉:

Stencil
{
    
    
	Ref {
    
    _ID}
	Comp always
	Pass replace
}

在这里插入图片描述
代码如下:

Shader "Unlit/Mask" 
{
    
    
    Properties
    {
    
    
        _ID("Mask ID", Int) = 1
    }
    SubShader
    {
    
    
        Tags{
    
     "RenderType" = "Opaque" "Queue" = "Geometry" }
        ColorMask 0
        ZWrite off          
        Stencil
        {
    
    
             Ref[_ID]
             Comp always
             Pass replace
        }
        Pass
        {
    
    
            CGINCLUDE
            struct appdata 
            {
    
    
            	float4 vertex : POSITION;
            };
            
            struct v2f 
            {
    
    
           		float4 pos : SV_POSITION;
            };
            
            v2f vert(appdata v)
            {
    
    
           		v2f o;
            	o.pos = UnityObjectToClipPos(v.vertex);
            	return o;
            }
            
            half4 frag(v2f i) : SV_Target
            {
    
    
            	return half4(1,1,1,1);
            }
            ENDCG
        }
    }
}

八、着色器变体采集

通常,Unity中的Shader可以直接放在Resources目录下,运行时可以这样读取:

Shader a = Resources.Load<Shader>("Shader Name");
Shader b = Shader.Find("Shder Name")

采用这种方法加载出来的Shader第一次赋值给材质时,会进行解析,因此会带来一点卡顿,为了避免卡顿,可以将Shader放在Shader Variant Collection中提前进行预热。
Create->Shader->Shader Variant Collection创建,在Graphics Setting中,拉到最下方,点击Save to asset…即可创建Shader并将其包含进Shader Variant Collection中。
在这里插入图片描述
然后在初始化的地方进行预热:

Resources.Load<ShaderVariantCollection>("NewShaderVariants").WarmUp();

Unity提供了一种将Shader预制在包体中的功能,操作方法在上Graphics Settings中的Always Included Shaders处,将需要的Shader拖拽进来即可,但这样做有个隐患——变体(Variant)。
如果Shader预制在Always Included Shaders中,那么所有的变体组合都会进行打包,这会大幅度增加包体,并且在加载时会带来额外开销。因此要先确定变体有多少个,再决定将它放在哪里。选择一个Shader,点击Compile and show code,即可查看变体数量:
在这里插入图片描述
若数量太多,不要放进Always Included Shaders中。

参考文献:
1.Unity官方文档:https://docs.unity.cn/cn/current/Manual/UnityManual.html。
2.Unity3D游戏开发(第二版)。

猜你喜欢

转载自blog.csdn.net/qq_45548042/article/details/120182614