PA1 Shader Fundamentals

Shader 与渲染算法 - 基础 - 着色器基础

1 世界伊始:A Yellow Bing

黄饼饼。Fundamental_Yellow-Sphere

1.1 准备工作

Stripping It Down

  1. 在环境中创建一个 Sphere(球体)。

  2. 为了避免 Unity 中新建项目默认的光源干扰,进行超级减配:

    1. 删除默认环境。Lighting - Environment,删除 Skybox Material 和 Sun Source。

    2. 删除默认灯光。

  3. 此时 Unity 中场景应如图所示:

为什么会出现蓝色的背景颜色?

这是由于 Camera 里的 “Background” 设置。将 Background 作为物体之外的填充颜色,在摄影机里设置(而不是在场景里),一方面确保了这就是最终显示的背景颜色,另一方面相较在场景级别进行设置,其处理起来更为简单。

创建 Shader 文件

在路径 Create - Shader - "Unlit Shader" 中创建一个 Shader 文件。

为什么选择 "Unlit Shader" 而不是 "Standard Surface Shader"

相较于后者, Unlit Shader 提供了一个简化的环境,避免了过多复杂的光照模型和其他渲染特性,专注于颜色、纹理、UV映射等基本概念。适合初学者学习。

1.2 粗糙的想法 Rough Idea

  1. vertex shader:将物体好端端放在屏幕上即可;

  2. fragment shader:直接返回一个 “定义的颜色”;

  3. ” 应当使 Unity 编辑器的观众看到,方便实时修改。

1.3 CODING

YellowSphere.shader
// 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 的值会写入顶点数据中,而不是替代原有的顶点坐标值。

如何理解 Properties?

2 传递更多:渐变饼饼

漂亮球球。Fundamental_Gradient_Sphere

2.1 粗糙的想法 Rough Idea

  1. vertex shader:坐标作为颜色插值(xyz -> rgb)。传回物体空间坐标;

  2. fragment shader:将物体空间坐标映射到颜色值,再乘以 Tint,从而得到色彩滤镜效果;

  3. refactor:通过 structure 封装数据,进行多数据在 vertex shader 与 fragment shader 之间的数据传递。

2.2 CODING

GradientSphere.shader
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 偏移量。

重构前代码
MyFirstShader.shader
// 坐标作为颜色插值,xyz 变成 rgb
// out 关键字让括号内的参数写入值。由此 shader 可以将数据绑定到同一输出语义,实现顶点的着色。
float4 Vert(float4 position: POSITION, out float3 localPosition: TEXCOORD0): SV_POSITION {
    localPosition = position.xyz;
    return mul(unity_MatrixMVP, position);
}

float4 Frag(float4 position: SV_POSITION, float3 localPosition: TEXCOORD0): SV_Target {
    return float4(localPosition + 0.5, 1);
}
如何理解 TEXCOORD 寄存器?

3 UV 初步:可视化 UV 坐标

uv as colors (上帝视角)

3.1 粗糙的想法 Rough Idea

  1. vertex shader:传回物体顶点 UV 坐标(物体空间坐标);

  2. fragment shader:将 UV 坐标映射为颜色的四个分量。

如何读取 UV 信息?

shader 中的 texture 使用边长为单位 1 的正方形作为载体,顶点着色器可以通过语义 TEXCOORD0 可以获取其纹理坐标。如

Interpolators Vert(float4 position: POSITION, float2 uv: TEXCOORD0) {
    ...
}

3.2 CODING

UVAsColor.shader
// 使用 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。

4 UV 进步:花纹球球

textured sphere

4.1 粗糙的想法 Rough Idea

  1. 添加一个可以放材质(及其控制柄)的地方;

  2. vertex shader:物体顶点 UV 坐标考虑材质 Tilling 和 Offset;

  3. fragment shader:输出材质的采样。

如何添加材质?

增加一个 property。

主纹理习惯命名为 “_MainTex”,如此一来也可以使用 Material.mainTexture 访问它

MyFirstShader.shader
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 函数使用它。

MyFirstShader.shader
sampler2D _MainTex;

tex2D 第一个参数是引用的纹理名称,第二个参数是一个 float2 类型的 UV 坐标,表示要采样的位置。

MyFirstShader.shader
float4 Frag(Interpolators i): SV_TARGET {
    return tex2D(_MainTex, i.uv);
}

4.2 CODING

UVTexture.shader
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映射更加平滑和一致。

Tiling
Offset

APPENDIX

Anki

Reference

Catlike Coding - Rendering 2 - Shader Fundamentals

Last updated