UnlitShader最开始的代码如下:
Shader "Unlit/HiShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
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);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
这个初始Shader看上去不是那么简单!但不要担心, 我们将逐步介绍每个部分。
让我们看看这个简单的主要部分。
Shader
Shader命令包含一个带有 着色器名称的字符串。在材质检视面板中选择着色器时,可以使用正斜杠字符“/”将着色器放置在子菜单中。
Properties
Properties代码块包含着色器变量 (纹理、颜色等),这些变量将保存为材质的一部分, 并显示在材质检视面板中。在我们的无光照着色器模板中, 声明了单个纹理属性。
SubShader
一个着色器 (Shader) 可以包含一个或多个子着色器(SubShader),主要 用于实现不同 GPU 功能的着色器。
Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
子着色器使用标签来告知它们期望何时以何种方式被渲染到渲染引擎。指定 TagName1 具有 Value1,TagName2 具有 Value2。可根据需要添加任意数量的标签。从根本上说,标签就是键/值对。在 SubShader 中,标签用于确定子着色器的渲染顺序和其他参数。请注意,由 Unity 识别的标签必须位于 SubShader 部分中,不能在 Pass 中!
Pass
每个子着色器由多个通道(Pass)组成,每个通道代表 为使用着色器材质渲染的同一对象执行 顶点和片元代码。 许多简单的着色器只使用一个通道,但与光照交互的 着色器可能需要更多通道。通道内部的 命令通常设置固定函数状态,例如 混合模式。
CGPROGRAM ..ENDCG
这两个关键字包裹着顶点和片元着色器中的 CG/HLSL 代码 部分。通常,大多数CG/HLSL相关代码都包含在这里。在代码片段的开头,可使用 #pragma 语句的形式提供编译指令。以下指令可指示要编译的着色器函数:
其他编译指令:
每个代码片段必须至少包含一个顶点程序和一个片元程序。因此,#pragma vertex 和 #pragma fragment 指令是必需的。
主顶点着色器函数(由 #pragma vertex
指令表示)需要在所有输入参数上都有语义。 这些对应于各个网格数据元素,如顶点位置、法线网格和纹理坐标。
对于 Cg/HLSL 顶点程序, 网格顶点数据作为输入传递给顶点 着色器函数。每个输入都需要有指定的语义:例如,POSITION
输入表示顶点位置,NORMAL
表示顶点法线。
通常,顶点数据输入在结构中声明,而不是 逐个列出。在UnityCG.cginc文件中 定义了几个常用的顶点结构,在大多数情况下, 仅使用它们就足够了。这些结构为:
appdata_base
:位置、法线和一个纹理坐标。appdata_tan
:位置、切线、法线和一个纹理坐标。appdata_full
:位置、切线、法线、四个纹理坐标和颜色。要访问不同的顶点数据,您需要自己声明 顶点结构,或者将输入参数添加到 顶点着色器。顶点数据由 Cg/HLSL 语义标识,并且必须来自 以下列表:
POSITION
是顶点位置,通常为 float3
或 float4
。NORMAL
是顶点法线,通常为 float3
。TEXCOORD0
是第一个 UV 坐标,通常为 float2
、float3
或 float4
。TEXCOORD1
、TEXCOORD2
和 TEXCOORD3
分别是第 2、第 3 和第 4 个 UV 坐标。TANGENT
是切线矢量(用于法线贴图),通常为 float4
。COLOR
是每顶点颜色,通常为 float4
。当网格数据包含的分量少于顶点着色器输入所需 的分量时,其余部分用零填充,但默认值为 1 的 .w
分量除外。例如,网格纹理坐标 通常是仅包含 x 和 y 分量的 2D 矢量。如果 顶点着色器使用 TEXCOORD0
语义声明一个 float4
输入,则 顶点着色器接收的值将包含 (x,y,0,1)。
最终UnlitShader基础代码语法解析:
// Shader 的路径名称 默认为文件名,也可以与文件名不同
Shader "Unlit/HiShader"
{
// 属性
// Material Inspector显示的所有参数都在需要在这里进行声明
Properties
{
// 通常所有属性名都以下划线字符开头 _MainTex
_MainTex ("Texture", 2D) = "white" {}
// 比较常见的属性类型
// ————————————————————————————————————————————————
_Integer ("整数(新版)", Integer) = 1
_Int ("整数(旧版)", Int) = 1
_Float ("浮点数", Float) = 0.5
_FloatRange ("浮点数滑动条", Range(0.0, 1.0)) = 0.5
// Unity包含以下内置纹理, 可以直接填充
// “white”(RGBA:1,1,1,1)
// “black”(RGBA:0,0,0,1)
// “gray”(RGBA:0.5,0.5,0.5,1)
// “bump”(RGBA:0.5,0.5,1,0.5)
// “red”(RGBA:1,0,0,1)
_Texture2D ("2D纹理贴图", 2D) = "red" {}
// 字符串留空或输入无效值,则它默认为 “gray”
_DefaultTexture2D ("2D纹理贴图", 2D) = "" {}
// 默认值为 “gray”(RGBA:0.5,0.5,0.5,1)
_Texture3D ("3D纹理贴图", 3D) = "" {}
_Cubemap ("立方体贴图", Cube) = "" {}
// Inspector会显示四个单独的浮点数字段
_Vector ("Example vector", Vector) = (0.25, 0.5, 0.5, 1)
// Inspector会显示拾色器拾取色彩RGBA值
_Color("色彩", Color) = (0.25, 0.5, 0.5, 1)
// ————————————————————————————————————————————————
// 除此之外 属性声明还可以具有一个可选特性 用来告知Unity如何处理它们
// HDR可以使色彩亮度的值超过1
[HDR]_HDRColor("HDR色彩", Color) = (1,1,1,1)
// Inspector隐藏此属性
[HideInInspector]_Hide("看不见我~", Color) = (1,1,1,1)
// Inspector隐藏此纹理属性的Scale Offset字段
[NoScaleOffset]_HideScaleOffset("隐藏ScaleOffset", 2D) = "" {}
// 指示纹理属性为法线贴图,如果分配了不兼容的纹理,编辑器则会显示警告。
[Normal]_Normal("法线贴图", 2D) = "" {}
}
// 子着色器
// 一个Shader至少有一个或者多个子着色器SubShader,这些子着色器互不干扰,且只有一个会运行
// 在加载shader时Unity会遍历所有SubShader列表,并最终选择用户机器支持的第一个
SubShader
{
// 可以通过Tags来向子着色器分配标签
// 只可以写在SubShader语块内,不可写在Pass内
/* 以键值对的形式存在,可以出现多个键值对
Tags {
"TagName1" = "Value1"
"TagName2" = "Value2"
"TagName3" = "Value3"
...
}
*/
// RenderPipeline: 声明子着色器是否与通用渲染管线 (URP) 或高清渲染管线 (HDRP) 兼容
// 仅与 URP 兼容
// Tags { "RenderPipeline"="UniversalRenderPipeline" }
// 仅与 HDRP 兼容
// Tags { "RenderPipeline"="HighDefinitionRenderPipeline" }
// RenderPipeline不声明或任何其他值表示与 URP 和 HDRP 不兼容
// ————————————————————————————————————————————————
// Queue: 声明渲染队列
// Tags { "Queue"="Background" } // 最早被调用的渲染,用来渲染天空盒或者背景
// Tags { "Queue"="Geometry" } // 这是默认值,用来渲染非透明物体(普通情况下,场景中的绝大多数物体应该是非透明的)
// Tags { "Queue"="AlphaTest" } // 用来渲染经过Alpha Test的像素,单独为AlphaTest设定一个Queue是出于对效率的考虑
// Tags { "Queue"="Transparent" }// 以从后往前的顺序渲染透明物体
// Tags { "Queue"="Overlay" } // 用来渲染叠加的效果,是渲染的最后阶段(比如镜头光晕等特效)
// ————————————————————————————————————————————————
// RenderType: 用来区别这个Shader要渲染的对象是属于什么类别的。
// 设置渲染类型 用一种称为着色器替换的技术在运行时交换子着色器,用来区别这个Shader要渲染的对象是属于什么类别的
// 这里表示非透明物体渲染
Tags { "RenderType"="Opaque" }
// 更多详细内容可参考官网文档 https://docs.unity.cn/cn/2021.3/Manual/SL-SubShaderTags.html
// LOD (Level of Detail)
LOD 100
// 每个子着色器由多个通道组成,许多简单的着色器只使用一个通道,但想要一些更复杂的效果,着色器可能需要更多通道
// 一个Pass就是一次绘制,可以看成是一个Draw Call而Pass的意义在于多次渲染,
// 如果你有一个Pass,那么着色器只会被调用一次,如果你有多个Pass的话,
// 那么就相当于执行多次SubShader了,这就叫双通道或者多通道。
// Draw Call:其实就是CPU调用图像编程接口的渲染命令,CPU每次调用DrawCall,都需要向GPU发送许多数据啊、渲染状态等等,
// 一旦CPU执行完应用阶段,GPU就会开始执行这次的渲染流程。而GPU渲染的速度比CPU提交命令的速度要快的多,
// 所以如果DrawCall数量过多的情况下,CPU需要进行大量的计算,进而就会导致CPU过载,影响游戏的运行效率。
Pass
{
CGPROGRAM
// 声明顶点着色器
#pragma vertex vert
// 声明像素着色器
#pragma fragment frag
// 使雾生效
#pragma multi_compile_fog
// 引用CG的核心代码库
#include "UnityCG.cginc"
// 应用程序阶段结构体
struct appdata
{
// 参考:https://docs.microsoft.com/zh-cn/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics
// POSITION 着色器语言的语义,用来限定着色器的输入输出值的类型
// 模型空间的顶点坐标
float4 vertex : POSITION;
// 模型的第一套UV坐标
float2 uv : TEXCOORD0;
};
struct v2f
{
// UV
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
// SV_POSITION 当这个值需要作为输出值输出给系统用的时候 前面需要加SV_前缀
// 当然因为有向下兼容的机制 不加也没啥太大问题
float4 vertex : SV_POSITION;
};
// 在Properties中声明的参数要在这里相对应的定义后才可以使用
sampler2D _MainTex;
float4 _MainTex_ST;
// 定义顶点着色器函数 函数名要与声明顶点着色器名称相同
v2f vert (appdata v)
{
v2f o;
// 将顶点坐标从模型空间变换到裁剪空间
o.vertex = UnityObjectToClipPos(v.vertex);
// Transforms 2D UV by scale/bias property
// #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
// 等价于v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;
// 简单来说,TRANSFORM_TEX主要作用是拿顶点的uv去和材质球的tiling和offset作运算,
// 确保材质球里的缩放和偏移设置是正确的
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
// SV_Target可以视为COLOR ,虽说他也是作为输出值输出给系统的
// 但它其实是告诉系统把输出的颜色值存储到RenderTarget中
// 所以这里我们用SV_Target
fixed4 frag (v2f i) : SV_Target
{
// 采样2D纹理贴图
fixed4 col = tex2D(_MainTex, i.uv);
// 应用雾
UNITY_APPLY_FOG(i.fogCoord, col);
// 返回经过处理后的最终色彩
return col;
}
ENDCG
}
}
}