为什么会出现蓝色的背景颜色?
这是由于 Camera 里的 “Background” 设置。将 Background 作为物体之外的填充颜色,在摄影机里设置(而不是在场景里),一方面确保了这就是最终显示的背景颜色,另一方面相较在场景级别进行设置,其处理起来更为简单。
为什么选择 "Unlit Shader" 而不是 "Standard Surface Shader"
相较于后者, Unlit Shader 提供了一个简化的环境,避免了过多复杂的光照模型和其他渲染特性,专注于颜色、纹理、UV映射等基本概念。适合初学者学习。
// shader 版本的 Hello World
Shader "Code/PA1_Fundamental/YellowSphere"
{
Properties {
_Tint("Tint", Color) = (1,1,0,1) // Tint,英文 “染色”
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityCG.cginc"
float4 _Tint;
// 顶点坐标使用 MVP 变换,从物体局部坐标转换为屏幕空间坐标
float4 Vert(float4 position: POSITION): SV_POSITION {
return mul(unity_MatrixMVP, position);
}
// 直接返回在 Properties 中定义的 _Tint
float4 Frag(): SV_Target {
return _Tint;
}
ENDCG
}
}
}
语义 POSITION 和 SV_POSITON 有什么区别?
POSITION:作为 Vert
函数中的的输入参数语义,float4 position: POSITION
表示 position
作为新声明变量,其数据类型为 float4,该变量与语义 POSITION
进行绑定,其中读取的值为模型顶点数据中的位置信息。
SV_POSITION:作为 Vert
函数的输出语义,用于写入顶点的屏幕空间位置。它读取 Vert
着色器的返回值(通过 return 函数)后,自动传递给渲染管线,可被后续的着色器所处理。
注意,SV_POSITION 的值会写入顶点数据中,而不是替代原有的顶点坐标值。
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityCG.cginc"
float4 _Tint;
struct Interpolators {
float4 position: SV_POSITION;
float3 localPosition: TEXCOORD0;
};
Interpolators Vert(float4 position: POSITION) {
Interpolators i;
i.localPosition = position.xyz;
i.position = mul(unity_MatrixMVP, position);
return i;
}
float4 Frag(Interpolators i): SV_TARGET {
return float4(i.localPosition + 0.5, 1) * _Tint;
}
ENDCG
fragment shader 返回值中设置的偏移量是怎么回事?
默认球体大小为 1,本地坐标空间落在 (-0.5, 0.5) 之间,因此所映射的颜色空间会非常暗(0~0.5)。为了让映射空间到正常颜色空间(0~1),因此在片元着色器的 return 语句中 + 0.5 偏移量。
如何读取 UV 信息?
shader 中的 texture 使用边长为单位 1 的正方形作为载体,顶点着色器可以通过语义 TEXCOORD0
可以获取其纹理坐标。如
Interpolators Vert(float4 position: POSITION, float2 uv: TEXCOORD0) {
...
}
// 使用 struct 重构代码
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityCG.cginc"
struct VertexData {
float4 position: POSITION;
float2 uv: TEXCOORD0;
};
struct Interpolators {
float4 position: SV_POSITION;
float2 uv: TEXCOORD0;
// float3 localPosition: TEXCOORD0;
};
Interpolators Vert(VertexData v) {
Interpolators i;
// i.localPosition = v.position.xyz;
i.uv = v.uv;
i.position = mul(unity_MatrixMVP, v.position);
return i;
}
float4 Frag(Interpolators i): SV_TARGET {
// return float4(i.localPosition + 0.5, 1) * _Tint;
return float4(i.uv, 1, 1);
}
ENDCG
为什么 UV 坐标会被映射为图例的颜色?
在 shader 中,float4
作为颜色输出时,四个通道分别代表 R、G、B 和 A。fragment shader 的返回值中,UV 坐标的 U 分量赋值给红色通道,V 分量赋值给绿色通道,同时蓝色通道和透明度设置为 1。
如何添加材质?
增加一个 property。
主纹理习惯命名为 “_MainTex
”,如此一来也可以使用 Material.mainTexture
访问它
Properties {
_MainTex("Texture", 2D) = "White"{} // 后面的括号是必加的,历史遗留问题
}
如何使用材质的控制柄?
为着色器添加纹理属性(property)后,材质检查器同时为其添加了平移和偏移控制器。通过在纹理属性后添加 _ST 后缀来对其进行访问。比如 _MainTex
的控制器为 _MainTex_ST
。控制器的数据类型为 float4
。
其中,Tiling vector 通过 XY 分量对其访问,其默认值为 (1, 1);
Offset vector 通过 ZW 分量对其访问,其默认值为 (0, 0)
i.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;
此外,UnityCG.cginc
包含一个方便的宏,可以简化上述操作:
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
如何进行纹理的采样?
2D 纹理的数据类型是 sampler2D
。声明后,可以在 fragment shader 中使用 tex2D
函数使用它。
tex2D
第一个参数是引用的纹理名称,第二个参数是一个 float2 类型的 UV 坐标,表示要采样的位置。
float4 Frag(Interpolators i): SV_TARGET {
return tex2D(_MainTex, i.uv);
}
Shader "Code/PA1_Fundamental/UVTexture"
{
Properties {
_MainTex("Texture", 2D) = "white"{}
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityCG.cginc"
;
;
struct VertexData {
float4 position: POSITION;
float2 uv: TEXCOORD0;
};
struct Interpolators {
float4 position: SV_POSITION;
float2 uv: TEXCOORD0;
};
Interpolators Vert(VertexData v) {
Interpolators i;
// i.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
i.position = mul(unity_MatrixMVP, v.position);
return i;
}
float4 Frag(Interpolators i): SV_TARGET {
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}
为什么 _MainTex 声明之后要加大括号?
在 Unity Shader 中,Properties
块的语法是用来定义材质属性的,而大括号 {}
是用来表示该属性的结束。
这种格式的作用主要是为了确保属性定义的完整性和可读性。大括号以后还可以用来添加更多属性。比较类似于 if
后面单语句也会加上大括号。
为什么 Tilling 和 Offset 要在 vertex shader 阶段(而不是 fragment shader)做?
主要是出于性能考虑和 UV 计算的原因。
性能优化:vertex shader 的计算是针对每个顶点进行的,而 fragment shader 是在每个像素上进行的。如果 Tiling 和和 Offset 在 vertex shader 中处理,可以减少 fragment shader 中的计算负担;
插值计算:Tiling 和 Offset 在 vertex shader 中应用后,UV 坐标会被插值。这样,fragment shader 接收到的是已经调整过的 UV 坐标,这使得UV映射更加平滑和一致。