# 自定义Shader

在小游戏框架里,自定义 Shader 采用类 HLSL 语法进行编写,提供的工具链会将 HLSL 跨平台编译成GLSL/MSL/SPIR-V,以便在不同的环境上运行

# 着色器输入

# 使用头文件

Shader中可以以下两种方式引入头文件

#include<common.inc>
#include "something.hlsl"

通过<>引入的是小游戏框架内建的头文件,开发者自定义的头文件可以通过""来引入,使用文件相对路径进行索引。

# 定义和使用纹理

小游戏框架里使用着色器可以通过宏来对纹理进行声明和采样

// 普通纹理的声明和采样
DECLARE_TEXTURE(_MainTex);
float4 color = SAMPLE_TEXTURE(name, TexCoord);

// Cube Map的声明和采样
DECLARE_CUBEMAP(SpeCube);
SAMPLE_CUBEMAP(SpeCube, reflectVec);

通过材质设置纹理时,需要在 .effect 里声明使用的贴图,具体可参考效果和材质

# 定义Shader Variables

目前小游戏框架支持玩家在一个Constant Buffer中定义 Shader 变量供材质使用,可以命名为cbuffer Material。可参考如下方式定义

cbuffer Material
{
    float4 _MainTex_ST;
    float4 _Color;
}

由于效率和兼容性的问题,顶点着色器和像素着色器中使用的 cbuffer 定义必须保证一致,未使用的 uniform 会在优化流程中被剔除。

小游戏框架内建Shader Variables包含以下内容

WorldSpaceViewPosition: float3 相机位置
WorldSpaceLightDir: float3 平行光方向
LightColor: float3 光照颜色
AmbientLight: float3 环境光颜色,用于多光源
GameTime: float 当前游戏时间
ShadowStrength: float 阴影强度
ShadowColor: float3 阴影颜色
EnvironmentMap: Cubemap 环境贴图

# 定义顶点输入和输出

在小游戏框架里,顶点输入是确定的,FEffect3DVertexInput是内建的顶点结构,通过 common.inc 文件里引用到 shader 中,开发者需要在顶点着色器里定义顶点着色器的输出。

#include <common.inc>

struct FVertexOutput
{
    float4 Position : SV_Position;
    float2 TexCoord : TEXCOORD0;        // world space light direction
    float3 LightDir : TEXCOORD1;        // world space view direction
    float3 ViewDir : TEXCOORD2;         // world space normal direction
    float3 WorldNormal:TEXCOORD3;
    LIGHTMAP_COORDS(4)                  //builtin lightmap coords
    FOG_COORDS(5)                       // builtin fog cooord
    SHADOW_COORDS(6)                    // builtin shadow coord
};

顶点输出内容在GLES2.0会作为 varing 传递给像素着色器使用

# 编写一组着色器

结合上面内容,我们来看一组简单的 Shader 实现

// simple3D.vertex.hlsl

// 公共头文件,包含内建 hlsl 函数、宏和结构定义
#include <common.inc>

// 材质定义,需与 .effect 文件中的配置一致
cbuffer material
{
    float4 _MainTex_ST;
    float4 _Color;
}
// vertex output, 从 vertex 传递到 fragment 的数据
struct FVertexOutput
{
    float4 Position : SV_Position;
    float2 TexCoord : TEXCOORD0;
};

// vertex入口函数
void Main(in FEffect3DVertexInput In, out FVertexOutput Out)
{
    // 内建顶点处理,进行Particle/Skin/Line/Trail等顶点处理
	FVertexProcessOutput VPOut;
	Effect3DVertexProcess(In, VPOut);

    Out.Position = WorldToClipPosition(VPOut.WorldPosition);
    Out.TexCoord = TRANSFER_TEXCOORD(VPOut.TexCoord, _MainTex_ST);
}
// simple3D.pixel.hlsl

#include <common.inc>
cbuffer material
{
    float4 _MainTex_ST;
    float4 _Color;
}
struct FVertexOutput
{
    float4 Position : SV_Position;
    float2 TexCoord : TEXCOORD0;
};

// texture声明,需与 .effect 文件中声明的一致
DECLARE_TEXTURE(_MainTex);
float4 Main(in FVertexOutput In) : SV_Target0
{
    fixed4 texColor = SAMPLE_TEXTURE(_MainTex, In.TexCoord);
    return texColor * _Color;
}

# 内建函数

  • 通用内建函数
// Transform
// 将local space position转换到world Space
float3 ObjectToWorldNormal(float3 normal);
// 将world space position 转换到 clip space
float4 ObjectToWorldPosition(float4 point);
// 将 UV 坐标和 scaleOffset 进行计算,输出最终的 UV 坐标
float2 TRANSFER_TEXCOORD(float2 TexCoord, float4 ScaleOffset);
  • 顶点着色器中使用的内建函数
//内建顶点处理,进行Particle/Skin/Line/Trail等顶点处理
void Effect3DVertexProcess(in FEffect3DVertexInput In, out FVertexProcessOutput Out);
// 顶点着色器中阴影参数计算
void TRANSFER_SHADOW(FVertexProcessOutput Out, float3 WorldPos);
// 顶点着色器中雾效参数计算
void TRANSFER_FOG(FVertexProcessOutput Out, float3 WorldPos);
// 顶点着色器中Light Map参数计算
void TRANSFER_LIGHTMAP(in FEffect3DVertexInput In, out FVertexProcessOutput Out);

// 开启 Instancing 的时候,可以在顶点着色器里通过以下内建函数进行transform
float3 ObjectToWorldDirInstancing(in FEffect3DVertexInput In, float3 dir);
float3 ObjectToWorldNormalInstancing(in FEffect3DVertexInput In, float3 normal);
float4 ObjectToWorldPositionInstancing(in FEffect3DVertexInput In, float4 point1);
  • 像素着色器中使用的内建函数
// 计算阴影
float SHADOW_ATTENUATION(FVertexOutput In);
// 光照贴图采样
float3 SAMPLE_LIGHTMAP(FVertexOutput In);
// 计算雾效值
APPLY_FOG(FVertexOutput In, out float4 Color);

# 语法规则

  • 矩阵存储和访问顺序 小游戏框架的 runtime 里,矩阵默认采用的是列矩阵进行存储,同样的,在 Shader 里,矩阵默认使用的是列矩阵,对矩阵的访问采用的是和 runtime 一致的列主序访问,即matrix[col][row]的方式。乘法规则采用的是和CG/GLSL一致的规则,即mul(Matrix, Vector),比如进行 MVP 矩阵变换,可以采用如下方式
mul(u_projection, mul(u_view, mul(u_world, position)));
  • 合法性校验 开发者编写的 Shader 工具会进行 HLSL 的语法校验,对于非法语法会在工具中提示错误的 shader 以及行号。如果开发者在顶点着色器和像素着色器定义的公共结构,如 FVertexOutput 和cbuffer不一致,则可能引起Link error。

# 性能

小游戏框架提供的 Shader 编译工具会对开发者编写的 Shader 做一定程度的编译优化,包括但不限于

  • 函数内联
  • 循环展开