这篇笔记主要写一些 Unity 中创建的 Shader 的代码基础。
在 Asset 窗口中右键 Shader > Unlit Shader,就可以创建一个新的 Shader 文件。
双击通过 Jetbrains Rider 打开这个 Shader(不建议使用 Visual Studio,因为没有默认根据 shader 调整反人类的自动换行)。
下面这个基本框架是一个 Shader 代码最基本的模板。
Shader "Name"
{
Properties
{
}
SubShader
{
Pass{
}
}
}
Property 属性
在 Properties{} 部分中,我们可以通过注释中的模板来声明一个新的属性。这个属性可以在 Shader 附着的材质球的材质面板上显示出来,以便我们从 Inspector 中进行修改。
Properties
{
//variable name + (the name you want to show in the inspector) + , + type + = + default value
// Float
_VariableName("Shown Name", Float) = 0.0
// Int
_SomeProperty("Some Property", Int) = 2
// Range(min, max)
_Range("Range", Range(0.0, 1.0)) = 0.0
// Vector
_Vector("Vector", Vector) = (1,1,1,1)
// Color
_Color("Color", Color) = (0.5,0.5,0.5,0.5)
// 2D
_Texture("Texture", 2D) = "white" {}
// 3D
_Model("Model", 3D) = "black"{}
// Cube
_Cube("Cube", Cube) = "white"{}
}
SubShader
真正调试着色方法的代码是写在 SubShader{} 中的。下面是一个典型的 SubShader 模板。
SubShader
{
// Optional
Tags {}
// Optional
// [Render Setup]
// 在 SubShader 中可以有多个 Pass{}。
Pass
{
// 在 CGPROGRAM - ENDCG 中实现代码
CGPROGRAM
// #pragma vertex 定义一个名为 vert 的顶点着色器
#pragma vertex vert
// #pragma fragment 定义一个名为 frag 的片元着色器
#pragma fragment frag
// include some header files
#include "UnityCG.cginc"
ENDCG
}
}
Tag{} 和 Render Setup 的部分详见 Unity Shader #2.1。
Pass{} 块
回顾一下渲染管线。
整个 SubShader 对应的部分实际上是 GPU 中的两个部分,即几何阶段和光栅化阶段。Pass{} 定义了我们实际的操作。
在 Unity 中,一个最基本的符合我们之前分析过的顶点片元着色器使命的 Shader 应该要有如下的特性:
顶点着色器返回一个 float4,将模型空间坐标转化到裁剪空间坐标。
片元着色器返回一个 float4,输出该片元的颜色。
那么,一个最简单的顶点片元着色器就可以这么写:
SubShader {
Pass {
CGPROGRAM
// 定义顶点着色器 vert 和片元着色器 frag
#pragma vertex vert
#pragma fragment frag
// 顶点着色器 vert 返回坐标值,所以是 float4
// 顶点着色器输入值应该是模型坐标,其中 POSITION 指的就是模型空间中的顶点位置,这是 Unity 定义的特殊含义
// SV_POSITION 指的是裁剪空间中的顶点位置
float4 vert(float4 v: POSITION) : SV_POSITION {
return mul(UNITY_MATRIX_MVP, v);
}
// 片元着色器 frag 返回颜色值,所以也是 float4
// 片元着色器并不一定需要从顶点获取什么信息,它只要定义这个片元最终会在这个像素上显示什么颜色即可
float4 frag() : SV_Target {
return fixed4(1,1,1,1);
}
ENDCG
}
}
顶点着色器 Vertex Shader
输入:顶点着色器接受的数据通常包括模型空间的顶点位置、法线信息、纹理坐标等。这些数据通常来自于 appdata 数据结构。这个数据结构我们也可以通过自定义的方式来获取我们所需要的特定的信息。
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// 通过 appdata 来获取应用阶段的信息
struct appdata
{
// POSITION 指的是模型空间下的物体顶点坐标
float4 vertex : POSITION;
// TEXCOORD[n] 的语义是纹理坐标,n不能重复,可以有多组纹理坐标
float2 uv : TEXCOORD0;
// NORMAL 的语义是顶点法线
float3 normal : NORMAL;
// COLOR 的语义是顶点颜色
float4 color : COLOR;
// TANGENT 描述每个顶点的切线向量
float4 tangent : TANGENT;
// VERTEXID 是顶点的唯一标识符
uint id : VERTEXID;
// BLENDWEIGHT 表示骨骼动画的权重
float blendweight : BLENDWEIGHT;
};
ENDCG
}
}
处理:类似的,我们也可以定义从顶点着色器到片元着色器的输出结构,通常被命名为 v2f,意为 Vertex To Fragment。在这个结构中,我们会将一些数据传递给片元着色器。我们在顶点着色器中完成对这些数据的计算,这个函数通常是 vert 函数。
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
// vertex to fragment structure
struct v2f
{
// SV_POSITION 指的是顶点在裁剪空间中的位置
float4 pos : SV_POSITION;
// WORLDPOS 指的是顶点在世界空间中的位置
float4 worldPos: WORLDPOS;
// NORMAL 指的是顶点的法线向量
float4 normal : NORMAL;
// TEXCOORD[n] 与 appdata 中类似
float2 texcoord : TEXCOORD0;
};
ENDCG
}
}
输出:到这里,我们就可以将原来的 vert 函数的返回值从 float4 变成 v2f 了。也可以让 vert 的输入值从原来简单的 float4 变成 appdata。vert 函数将对一个顶点输出一个 v2f 数据结构。
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
};
// 注意加粗部分和之前的简易顶点片元着色器的区别
v2f vert(appdata v)
{
// use o as a shorthand for output
v2f o;
float4 pos_clip = mul(UNITY_MATRIX_MVP, v.vertex);
o.pos = pos_clip;
return o;
}
ENDCG
}
}
虽然在整个 Shader 代码中并没有任何一个 for 循环,但我们要记得 vert 函数的操作是逐顶点进行(Per vertex)的。模型的每个顶点都会被顶点着色器处理。
在顶点着色器和片元着色器之间,通常还有插值的环节。在顶点着色器完成计算之后,我们只有与顶点数量相同的 v2f 结构。然而,很快我们会知道片元着色器是逐像素的,每个像素都必须要有自己对应的 v2f 结构,因此,顶点着色器结束之后 v2f 的数量显然是不足的。
在这个阶段, Unity 会自动地为所有没有计算过 v2f 数据结构的像素进行插值,这个插值是根据这个像素附近的顶点经过某种算法进行插值的。经过这个步骤,每个像素都有自己的 v2f 结构了。
片元着色器 Fragment Shader
输入:最后我们再实现我们的片元着色器,通常被命名为 frag 函数。frag 接收一个 v2f 作为一个输入值,而这个 v2f 根据我们上面讲过的,是一个已经插值过的 v2f 数据结构。因此,即使 frag 是逐像素进行(Per pixel)的,我们也依然根据 v2f 来计算像素的颜色结果。其中,在 frag 后面的 SV_Target 也是一个系统语义,将用户的输出颜色存储到帧缓存中。
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert(appdata v)
{
v2f o;
float4 pos_clip = mul(UNITY_MATRIX_MVP, v.vertex);
o.pos = pos_clip;
return o;
}
float4 frag(v2f i) : SV_Target
{
return float4(0.5,1.0,0.5,1.0);
}
ENDCG
}
}
保存我们的 Shader,这就是一个简单地将显示的颜色 trivially 显示成一个固定颜色的 shader。
如果我们想要使用我们在 Properties{} 中定义过的变量, 我们也需要在 SubShader{} 中定义一遍。确保名字必须一样。
Shader "Unlit/NewUnlitShader"
{
Properties
{
_Color("Color", Color) = (0.5,0.5,0.5,0.5)
_MainTex("MainTex", 2D) = "white" {}
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert(appdata v)
{
v2f o;
float4 pos_world = mul(_Object2World, v.vertex);
float4 pos_view = mul(UNITY_MATRIX_V, pos_world);
float4 pos_clip = mul(UNITY_MATRIX_P, pos_view);
o.pos = pos_clip;
return o;
}
float4 frag(v2f i) : SV_Target
{
float4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
댓글