unity Shader 入门精要 EX

unity Shader 入门精要:

1、shader概念

Shader,中文名为着色器。
Shader其实就是专门用来渲染图形的一种技术,通过shader,我们可以自定义显卡渲染画面的算法,使画面达到我们想要的效果。

2、shader分类(顶点Shader、像素Shader)

Shader分为两类 :
顶点Shader(3D图形都是由一个个三角面片组成的,顶点Shader就是计算每个三角面片上的顶点,并为最终像素渲染做准备)。
像素Shader,顾名思义,就是以像素为单位,计算光照、颜色的一系列算法。

几个不同的图形API都有各自的Shader语言
在DirectX中,顶点shader叫做 Vertex Shader ,像素Shader叫做 Pixel Shader;
在OpenGL中,顶点Shader也叫做 Vertex Shader ,但像素Shader叫做 Fragment Shader,也就是我们常说的片断Shader或者片元Shader。

说白了,Shader其实就是一段代码,这段代码的作用是告诉GPU具体怎样去绘制模型的每一个顶点的颜色以及最终每一个像素点的颜色。

3、Shader编程语言

目前主流的Shader编程语言有三种:

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

GLSL与HLSL分别是基于OpenGL和Direct3D的接口,两者不能混用。
而Cg语言是用于图形的C语言,这其实说明了当时设计人员的一个初衷,就是让基于图形硬件的编程变得和C语言编程一样方便,*。正如C++和 Java的语法是基于C的,Cg语言本身也是基于C语言的。如果您使用过C、C++、Java其中任意一个,那么Cg的语法也是比较容易掌握的。Cg语言极力保留了C语言的大部分语义,力图让开发人员从硬件细节中解脱出来,Cg同时拥有高级语言的好处,如代码的易重用性,可读性高等。

Cg语言是Microsoft和NVIDIA相互协作在标准硬件光照语言的语法和语义上达成了一致,所以,HLSL和Cg其实是同一种语言。

4、Unity Shader

4.1概述

在Unity中编写的Shader最终会根据不同的平台来编绎成不同的着色器语言。

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

由于有更好的跨平台性,Unity官方的建议是用Cg/HLSL来编写shader。(当然你也可以使用GLSL)

4.2分类(Surface Shaders、Vertex/Fragment Shaders、Fixed Function Shaders):

在Unity中有3种Shader(其实就是三种不同的写法):

Surface Shaders 表面着色器
(Unity对Vertex/Fragment Shader的又一层包装。更符合人类的思维模式,同时可以以极少的代码来完成不同的光照模型与不同平台下需要考虑的事情。)
(在进行表面着色器的开发时,我们将直接在Subshader这个层次上写代码,系统将把我们的代码编译成若干个合适的Pass——Subshader与pass概念详见后续‘shader语法框架’相关内容)
Vertex/Fragment Shaders 顶点/片断着色器
(更基础的shader,Vertex/Fragment Shader能实现的效果,Surface Shader不一定能实现,反过来则成立)
Fixed Function Shaders 固定管线着色器
(已经被淘汰)

4.3倾向

Unity2018后的版本中推出了Unity官方自己的可视化Shader工具(Shader Graph),其生成的代码全部是用的Vertex/Fragment Shader。在今后的可编程渲染管线中,更倾向于较全面的Vertex/Fragment Shader。

5、开始在unity中接触shader

5.1shader模板(unity中Create/Shader后的选项):

Standard Surface Shader
标准表面着色器,是一种基于物理的着色系统(使用了Physically Based Rendering(简称PBR)技术,即基于物理的渲染技术),以模拟现实真实的方式来模拟材质与灯光之间的关系,可以很轻易的表现出各种金属反光效果,同时此种Shader的书写逻辑也更符合人类的思维模式。

Unlit Shader
Vertex/Fragment Shader,也就是最基本的顶点片断着色器,不受光照影响的Shader,多用于特效、UI上的效果制作。

Image Effect Shader
也是顶点片断着色器,只不过是针对后处理而定制的模版,后处理是什么呢?Bloom(也有人叫Glow/泛光/辉光等说法)、调色、景深、模糊等,这些基于最终整个屏幕画面而进行再处理的Shader就是后处理。

Compute Shader
Compute Shader是运行在图形显卡上的一段程序,独立于常规渲染管线之外的,它可以直接将GPU作为并行处理器加以利用,从而使GPU不仅具有3D渲染能力,还具有其他的运算能力。

Ray Tracing Shader
射线追踪着色器,用于 GPU 射线追踪的着色器。此着色器应至少包含一个射线生成着色器。

Shader Variant Collection
Shader变体收集器,在上面创建的时候,你会发现Shader Variant Collection与以上四个是被隔开的,就是因为这个与它们不一样,它不是制作Shader的模版,而只是对Shader变体进行打包用的容器。

5.2材质与Shader

shader是指令代码,需关联材质才能赋予游戏对象以达成效果。
材质——按照关联的shader的规则,达到特定的画面效果。
创建材质,创建shader,并关联(一个Shader可以与无数个材质关联)。

5.3学习步骤
  1. Unlit Shader,这是最基本也是最简单的模版,通过学习它了解顶点片断着色器的基本构成,以及对Shader有初步的认识。
  2. 利用顶点片断着色器做一些简单案例,比如一些游戏内常用的效果,与此同时深入学习Shader语法,以及用到的相关数学运算符。
  3. 尝试优化你的Shader,提升美术效果,减少变体数,减少运算量。
  4. 读一读渲染管线的书,加深对Shader理解的宽度与高度。
  5. 开始接触光照模型,了解Surface Shader。
  6. 各种光照算法研究一通。。。。顺便复习下数学。。。
  7. 屏幕后处理Shader
  8. Compute Shader
  9. 各种实例制作,不断强化美感、图形学以及数学。

6、shader语法框架(Vertex/Fragment Shaders)

框架主体:

Shader "name" 
{ 
	[Properties] 
	{
	}
	SubShaders 
	{
		pass
		{
		}
	}
	[FallBack] "name"
	[CustomEditor] "EditorName"
}

其中[]方括号表示的是可选,意思就是说在Shader中可以没有它,只要不需要的话不加也是没有问题的。
可以拆分成以下几个大部分:

Shader “name” (shader路径名称,也就是我们在Project面板中的资源文件的名称,可与文件名不同)
Properties (属性,材质球面板中显示的贴图和一些参数的设置位置)
SubShaders (可以有不止一个,核心算法在SubShader中实现,可针对高中低等不同硬件配置提供不同的SubShader,加载Shader时,Unity将遍历所有SubShader列表,并最终选择用户机器支持的第一个。)
pass (渲染一次)
FallBack (备胎,在当前机器不支持此shader时替换到对应的其他shader)
CustomEditor (*定义材质面板的显示结果,它可以改写Properties中定义的显示方式。c#)

6.1 Properties 参数暴露

Properties可以理解为是材质与Shader的连接通道,我们在材质面板上需要设置的内容都必须通过Properties来实现并暴露。

属性的写法有个通用的格式:

[Attribute]_Name ("Display Name",Type) = Default Value[{options}]

/*
【】Attribute:属性关键字,对属性进行一些特殊的处理,同属性可有多个关键字。
如:
[HDR]亮度可大于1(颜色属性);
[PowerSlider(N)]数值滑动条区间微调(N值:前段精度高0~1,后段精度高1~N)
[Toggle]开关
[Enum]枚举
面板格式属性关键字(同Unity面板):
[Header]标注
[HideInInspector]隐藏
【】_Name:变量名,ID,一定要加上下划线。
(如果此Shader有FallBack的话,一定要将此Shader中的变量名与FallBack中的变量名保持一致)
【】Display Name:显示在材质面板上的名称。
【】Type:属性的类型 
如:
Color颜色;
Int整数;
Float浮点数;
Vector四维数;
2D纹理;
3D纹理;
Cube立方体纹理;
Range(min,max)范围和滑动条控制(数值属性)

详细——
Color - 一种颜色,由RGBA(红绿蓝和透明度)四个量来定义;
2D - 一张2的阶数大小(256,512之类)的贴图。这张贴图将在采样后被转为对应基于模型UV的每个像素的颜色,最终被显示出来;
Rect - 一个非2阶数大小的贴图;
Cube - 即Cube map texture(立方体纹理),简单说就是6张有联系的2D贴图的组合,主要用来做反射效果(比如天空盒和动态反射),也会被转换为对应点的采样;
Range(min, max) - 一个介于最小值和最大值之间的浮点数,一般用来当作调整Shader某些特性的参数(比如透明度渲染的截止值可以是从0至1的值等);
Float - 任意一个浮点数;
Vector - 一个四维数;

【】Default Value:属性对应默认值。第一次指定此Shader时,或者在材质面板上执行Reset时会使用默认值。
如:
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贴图有关,在写输入时我们最少要在贴图之后写一对什么都不含的空白的{},当我们需要打开特定选项时可以把其写在这对花括号内。
如果需要同时打开多个选项,可以使用空白分隔。可能的选择有ObjectLinear, EyeLinear, SphereMap, CubeReflect, CubeNormal中的一个,这些都是OpenGL中TexGen的模式。
*/

6.2 用于控制渲染的 Cg/HLSL 代码编写

详细例子:

Shader "Unlit/MyFirstShader"
{
	Properties
	{
		_Color("Color", Color) = (1,1,1,1)
	}
	
	SubShader
	{
		Pass
		{
			CGPROGRAM                           //CG代码起点
			#pragma vertex vertShaderName       //编绎指令:声明定义使用的顶点着色器名称
			#pragma fragment fragShaderName     //编绎指令:声明定义使用的片断着色器名称

			fixed4 _Color;                      //unityshader编写规则:在CG代码中 再次声明所需使用的Properties里已暴露出的属性(内嵌的CG代码块和外边的unity能直接编译的ShaderLab代码块 无法共用变量)
			//返回值 顶点着色器名          输入参数   参数语义标记  返回值语义标记  
			float4 vertShaderName ( float4 vertex : POSITION ) : SV_POSITION
			{
				return UnityObjectToClipPos(vertex);//unity内置函数,将顶点从模型坐标转换到裁剪坐标,给片断着色器用
			}
			//返回值  片断着色器        返回值语义标记
			fixed4 fragShaderName () : SV_Target
			{
				return _Color;
			}
			ENDCG                              //CG代码结束
		}
	}	
}
//标识语义:(在 顶点着色器中 可通过标识语义自动获取到 直接存储在模型中的 顶点数据——顶点位置、法线信息、顶点颜色等等)
//POSITION-模型的顶点位置信息(模型中自动获取);
//SV_POSITION-经过顶点着色器变换之后的顶点坐标(顶点着色器返回值中自动获取);
//SV_Target-系统值,下一个阶段输出的颜色值,用于最终显示颜色(片断着色器返回值中自动获取);

运行步骤:

  1. 每个顶点都会执行一次顶点着色器。
  2. 对顶点着色器输出的 对应当前模型的所有片断像素的 顶点矩阵(内部会自动进行插值计算)里的每一项 执行一次片断着色器,得到最终每个像素的颜色值。

性能:
顶点着色器 每顶点执行一次
片断着色器 每像素执行一次
显示模型要的像素比模型自身顶点多得多
所以从性能的角度来考虑,我们要尽量把计算放在顶点着色器中去执行。其次在片断着色器中也要尽量的简化算法,节省开支。

6.3 Cg/HLSL 与 Properties对应数据类型

Cg/HLSL中的几种常见数据类型:

  1. float/half/fixed(三个都是浮点数,高精度32位/中精度16位/低精度11位)(常用规则:除了位置和坐标用float以外,其余的全部用half。)
  2. integer(整型)
  3. sampler2D(2D纹理)
  4. sampler3D(3D纹理)
  5. samplerCUBE(立方体纹理)

(默认情况下在移动平台纹理会被自动转换成低精度的纹理类型,想要用高精度,需在声明时加后缀:_half或_float)

Properties与Cg/HLSL中的类型对应:

  1. Int/float/Range用浮点值表示,也就是float、half或者fixed,根据自己需要的精度来定义。
  2. Vector/Color用float4、half4或者fixed4表示。
  3. 2D类型用sampler2D表示。
  4. 3D类型sampler3D表示。
  5. CUBE类型用samplerCUBE表示。

向量分量表示:
在Cg/HLSL中我们可以通过_Color来访问颜色,也可以通过_Color.rgba来访问:
红通道就是_Color.r;
绿通道和透明通道就是_Color.ga
(也可使用.xyzw,意义相同)

6.4 渲染管线

图形渲染管线之所以被叫做管线,就是因为它和一根管子的概念很像,我们可以理解为这根管子的末端连接的是我们最终的显示屏幕,管子的起始端连接的是我们的原始素材。

这根管子从头到尾大致流程可以分为以下三个大阶段(概念上):

  1. 应用程序阶段(The Application Stage)

就是我们的原始素材准备阶段,包括我们的模型、贴图、相机和光源等,经过这个阶段会将所有素材转换成渲染图元并提交到下一阶段中(几何阶段)。
——表现为顶点着色器的输入参数

  1. 几何阶段(The Geometry Stage)顶点着色器工作阶段

主要是对上一阶段中传过来的数据进行顶点上的加工处理,包括各种矩阵转换与顶点着色等,最终处理完后会输出屏幕空间的二维顶点坐标、顶点着色等信息,并再提交到下一阶段(光栅化阶段)。
——表现为顶点着色器的返回数据,即传递给片断着色器的数据

  1. 光栅化阶段(The Rasterizer Stage)片断着色器工作阶段

经过几何阶段处理完后输送到光栅化阶段,从像素级别上对每个像素进行加工处理,最终显示于屏幕上。
——表现为最终片断着色器的输出

通过结构体(同C语言)传递更多参数
通过自定义函数(同C语言)简化着色器区块的代码

6.5 变量语义

输入数据,要靠语义来标识对应源或目标

应用阶段传入顶点着色器的数据:

struct appdata
	{
	  //数据类型 名称  赋予POSITION语义(表示该数据来源是自动获取的模型顶点数据)
		float4 vertex : POSITION;		//顶点
		float4 tangent : TANGENT;		//切线
		float3 normal : NORMAL;			//法线
		float4 texcoord : TEXCOORD0;	//UV1
		float4 texcoord1 : TEXCOORD1;	//UV2
		float4 texcoord2 : TEXCOORD2;	//UV3
		float4 texcoord3 : TEXCOORD3;	//UV4
		fixed4 color : COLOR;			//顶点色
	};

顶点着色器到片断着色器的数据:

struct v2f
	{
		float4 pos:SV_POSITION;   //处理后的顶点位置———顶点着色器输出的屏幕裁剪空间下的顶点位置。
		float2 uv:TEXCOORD0;      //TEXCOORD0、TEXCOORD1、...、TEXCOORD2等等,主要用于高精度数据。
		float dp:COLOR0;          //COLOR0、COLOR1、...、COLORN等等,主要用于低精度数据。
	};

特殊的可输入片断着色器的数据:VFACE
如果渲染表面朝向摄像机,则Face节点输出正值1,如果远离摄像机,则输出负值-1。

片断着色器输出相关语义:
通常情况下,片断着色器最终只需返回一个颜色值即可。:SV_TARGET
在需要输出多个RenderTarget时:SV_TARGET0,SV_TARGET1,SV_TARGET2…
需要自定义像素深度值时:SV_Depth

6.6 tags 检索控制标签

shader框架的更多内容:在SubShader和Pass中都有Tags可选参数组:
(表面着色器可以被若干的标签(tags)所修饰,而硬件将通过判定这些标签来决定什么时候调用该着色器。)

Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
//名称(TagName)和值(Value)是成对成对出现的,并且全部用字符串表示。
Shader "name" 
{ 
	[Properties] 
	{
	}
	SubShaders 
	{
		tags //--------------------------------------------
		{
		
		}
		pass
		{
			tags //--------------------------------------------?
			{
			}
		}
	}
	[FallBack] "name"
	[CustomEditor] "EditorName"
}

SubShader Tags:
Tag:Queue(渲染队列,指定对象什么时候渲染,每个队列其实都是利用一个整数进行索引的。)

  1. Background
    值为1000,此队列的对象最先进行渲染。
  2. Geometry
    Queue的默认值,值为2000,通常用于不透明对象,比如场景中的物件与角色等。
  3. AlphaTest
    值为2450,要么完全透明要么完全不透明,多用于利用贴图来实现边缘透明的效果,也就是美术常说的透贴。
  4. Transparent
    值为3000,常用于半透明对象,渲染时从后往前进行渲染,建议需要混合的对象放入此队列。
  5. Overlay
    值为4000,此渲染队列用于叠加效果。最后渲染的东西应该放在这里(例如镜头光晕等)。
//写法示例:
Tags{ "Queue" = "Geometry" }
//通过值的加减,提供更多层级选择的 自定义渲染队列:
Tags{ "Queue" = "Geometry+1" }

在Unity中,
渲染队列小于2500的对象都被认为是不透明的物体,从前往后绘制,不会重复绘制,无额外消耗。
反之大于2500的,会从后往前绘制,以表现透明效果,会导致像素的多次绘制,额外消耗运算资源。
所以:需要尽可能地把物体的队列设置为不透明物体的渲染队列,而尽量避免重复绘制。

Tag:RenderType

  1. Opaque: 用于大多数着色器(法线着色器、自发光着色器、反射着色器以及地形的着色器)。
  2. Transparent:用于半透明着色器(透明着色器、粒子着色器、字体着色器、地形额外通道的着色器)。
  3. TransparentCutout: 蒙皮透明着色器(Transparent Cutout,两个通道的植被着色器)。
  4. Background: Skybox shaders. 天空盒着色器。
  5. Overlay: GUITexture, Halo, Flare shaders. 光晕着色器、闪光着色器。
  6. TreeOpaque: terrain engine tree bark. 地形引擎中的树皮。
  7. TreeTransparentCutout: terrain engine tree leaves. 地形引擎中的树叶。
  8. TreeBillboard: terrain engine billboarded trees. 地形引擎中的广告牌树。
  9. Grass: terrain engine grass. 地形引擎中的草。
  10. GrassBillboard: terrain engine billboarded grass. 地形引擎何中的广告牌草。

该值为内部的约定,用来区别这个Shader要渲染的对象是属于什么类别的,
改为自定义的名称,也并不会影响到Shader的效果。
但约定名称可以利用Camera.SetReplacementShader来更改最终的渲染效果:

//替代渲染方法:
camera.SetReplacementShader (EffectShader, Tag);
//着色器设置函数:
//tag为空则所有物体Shader都替换成shaderA进行渲染。
//tag为某一类,则先检索出有该tag的对象,并判断该tag是否与EffectShader里的该tag值相等
//,相等则使用shaderA渲染,否则就直接不渲染。

Tag:DisableBatching
利用Shader在模型的顶点本地坐标下做一些位移动画时,如果此模型有批处理时会出现效果不正确的情况,这是因为批处理会将所有几何转换为世界坐标空间,因此“本地坐标空间”将丢失,导致基于本地坐标的效果失效。

  1. false
    默认值,不禁用批处理,对应情况会产生异常
  2. true
    始终禁用此着色器的批处理
  3. LODFading
    仅当LOD激活时禁用批处理

Tag:ForceNoShadowCasting
是否强制关闭投射阴影,值可为:

  1. True,强制关闭投射阴影
  2. False(默认值),不关闭投射阴影

Tag:IgnoreProjector
是否忽略Projector投影器的影响,Projector是Unity中内置的组件,可用于实现贴花等功能。

  1. True,使对象不受Projector影响
  2. False(默认值),使对象受Projector影响

Tag:CanUseSpriteAtlas
是否可用于精灵打包图集,
意思就是如果某个图片精灵被设置为打包进图集中,那么当此精灵所指定Shader中设置为
“CanUseSpriteAtlas”=“False”
时就会使其无法工作,相应的UI上也会有警告提示。

Tag:PreviewType

  1. Plane
    平面预览

  2. Skybox
    天空盒预览

材质面板的预览窗口如何显示模型,默认显示的是球体,
此功能仅仅影响的只是材质面板的预览,对Shader本身没有什么影响。

6.7 LOD 细节等级

细节等级:Level of Detail
这个数值决定了我们能用什么样的Shader。(判断对应Subshader是否可用)
在Unity的Quality Settings中我们可以设定允许的最大LOD,当设定的LOD小于SubShader所指定的LOD时,这个SubShader将不可用。
Unity内建Shader定义了一组LOD的数值,我们在实现自己的Shader的时候可以将其作为参考来设定自己的LOD数值,这样在之后调整根据设备图形性能来调整画质时可以进行比较精确的控制。

内建Shader定义的LOD数值:

  • 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
6.8 RenderState

。。。。。。待补充

7、shader语法框架(Surface Shaders)

与 Vertex/Fragment Shaders 自行在不同pass里编写shader内容代码不同,在进行 Surface Shaders 的开发时,我们将直接在Subshader这个层次上写代码,系统将把我们的代码编译成若干个合适的Pass。
(按官方可视化Shader工具输出的代码看,更倾向于使用功能更全面的 Vertex/Fragment Shaders编写方案)
(参考第6章节内容,此章节主要例举Surface Shaders相关的 额外及区别内容)
主体框架:

Shader "name" 
{ 
	[Properties] 
	{
	}
	SubShader
	{
	
	}
	[FallBack] "name"
	[CustomEditor] "EditorName"
}
7.1 用于控制渲染的 Cg/HLSL 代码编写

详细例子:(对比6.2章节的详细例子理解)

Shader "Custom/Diffuse Texture" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200
		
		CGPROGRAM
		#pragma surface surf Lambert     //编译指令(详见下方注释)

		sampler2D _MainTex;

		struct Input {
			float2 uv_MainTex;         //与Properties内容关系(详见下方注释)
		};
		//CG规定了声明为表面着色器的方法(就是我们这里的surf)的参数类型和名字,
		//因此我们没有权利决定surf的输入输出参数的类型,只能按照规定写。
		//这个规定就是第一个参数是一个Input结构(自定义struct),第二个参数是一个预定义好的inout的SurfaceOutput结构(详见下方注释)。
		void surf (Input IN, inout SurfaceOutput o) {
			half4 c = tex2D (_MainTex, IN.uv_MainTex);
			o.Albedo = c.rgb;
			o.Alpha = c.a;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}
/*
【】编译指令:#pragma surface surf Lambert
它声明了我们要写一个表面Shader,并指定了光照模型。它的写法是这样的

#pragma surface surfaceFunction lightModel [optionalparams]

surface - 声明的是一个表面着色器
surfaceFunction - 着色器代码的方法的名字
lightModel - 使用的光照模型。

【】uv_MainTex
在CG程序中,我们有这样的约定,在一个贴图变量(在我们例子中是_MainTex)之前加上uv两个字母,就代表提取它的uv值(其实就是两个代表贴图上点的二维坐标 )。我们之后就可以在surf程序中直接通过访问uv_MainTex来取得这张贴图当前需要计算的点的坐标值了。

【】SurfaceOutput结构:
struct SurfaceOutput {
    half3 Albedo;     //像素的颜色
    half3 Normal;     //像素的法向值
    half3 Emission;   //像素的发散颜色
    half Specular;    //像素的镜面高光
    half Gloss;       //像素的发光强度
    half Alpha;       //像素的透明度
};
*/

参考与引用

REF1:https://zhuanlan.zhihu.com/p/46745694 《零基础入门Unity Shader》
REF2:https://onevcat.com/2013/07/shader-tutorial-1 《猫都能学会的Unity3D Shader入门指南》
REF3:https://www.jianshu.com/p/a41cdff34f6b 《Unity 渲染流程》
REF4:https://blog.csdn.net/hbysywl/article/details/80369425 《Unity渲染优化(UI向)》
REF3:https://docs.unity3d.com/Manual/SL-SurfaceShaderExamples.html shader相关官方文档

上一篇:调试osgEarth源码(六)解析.earth----(3)


下一篇:Shader