跳到主要内容

着色器预处理器

编译着色器之前,PlayCanvas 对源码执行 C 风格预处理,以管理变体、条件包含代码和注入数值。预处理器适用于 GLSL、WGSL 和计算着色器。

预处理指令​

支持以下指令:

定义与取消定义​

#define FEATURE_ENABLED
#define MAX_LIGHTS 4
#define MULTIPLIER 2.5

#undef FEATURE_ENABLED

宏可以是无值的简单标志,也可以具有值。

条件编译​

#ifdef FEATURE_ENABLED
// Code included only if FEATURE_ENABLED is defined
#endif

#ifndef FEATURE_ENABLED
// Code included only if FEATURE_ENABLED is NOT defined
#endif

#if defined(FEATURE_A) && defined(FEATURE_B)
// Code included if both FEATURE_A and FEATURE_B are defined
#endif

#if MAX_LIGHTS > 2
// Code included if MAX_LIGHTS is greater than 2
#endif

If-Else-Elif 分支链​

#if QUALITY == 0
// Low quality path
#elif QUALITY == 1
// Medium quality path
#else
// High quality path
#endif

逻辑运算符​

预处理器支持 &&(与)、||(或)和 !(非):

#if defined(FEATURE_A) && !defined(FEATURE_B)
// FEATURE_A is defined but FEATURE_B is not
#endif

#if defined(FEATURE_A) || defined(FEATURE_B)
// At least one of FEATURE_A or FEATURE_B is defined
#endif

比较运算符​

支持 ==、!=、<、<=、>、>=。

#if MAX_LIGHTS >= 4
// 4 or more lights supported
#endif

#if QUALITY != 0
// Not low quality
#endif

包含指令​

#include 从已注册着色器代码块中插入内容:

#include "chunkName"

例如,包含引擎代码块:

#include "gammaPS"
#include "tonemappingPS"

注册自定义着色器代码块​

建议通过 ShaderChunks 注册自定义包含内容。可同时提供 GLSL 和 WGSL 版本,引擎自动选择适当版本:

// Get the shader chunks for each language
const chunksGLSL = pc.ShaderChunks.get(device, pc.SHADERLANGUAGE_GLSL);
const chunksWGSL = pc.ShaderChunks.get(device, pc.SHADERLANGUAGE_WGSL);

// Register your custom chunk in both languages
chunksGLSL.set('myUtilsPS', `
float myHelper(float x) {
return x * 2.0;
}
`);

chunksWGSL.set('myUtilsPS', `
fn myHelper(x: f32) -> f32 {
return x * 2.0;
}
`);

注册后,在着色器中使用 #include 引用:

#include "myUtilsPS"

void main() {
float result = myHelper(0.5);
}

循环包含​

可通过循环计数器多次包含同一代码块:

#define LIGHT_COUNT 4
#include "lightPS, LIGHT_COUNT"

这会包含 lightPS 四次,将代码块中的 {i} 分别替换为 0、1、2、3。

注入宏与普通宏​

预处理器支持两类宏,以语法区分:

普通宏​

普通宏与 #ifdef、#if 等预处理指令配合:

#define FEATURE_ENABLED
#define MAX_LIGHTS 4

#ifdef FEATURE_ENABLED
// This code is included
#endif

#if MAX_LIGHTS > 2
// This code is included
#endif

GLSL 原生支持在数组大小等位置使用宏:

#define SAMPLE_COUNT 8
float samples[SAMPLE_COUNT];

WGSL 不支持这种方式,应改用 {NAME} 语法的注入宏。

注入宏(花括号语法)​

注入宏使用 {NAME},在整份着色器源码中直接替换字符串,但排除预处理指令行:

#define {WORKGROUP_SIZE} 64

@compute @workgroup_size({WORKGROUP_SIZE}, 1, 1)
fn main() {
var<workgroup> data: array<f32, {WORKGROUP_SIZE}>;
}

预处理后变为:

@compute @workgroup_size(64, 1, 1)
fn main() {
var<workgroup> data: array<f32, 64>;
}

注入宏特别适合:

  • WGSL 工作组大小,必须是编译期常量。
  • 需要出现在非预处理位置的值。
  • 为不支持 #if 替换的代码进行参数化。

向着色器提供宏​

ShaderMaterial 宏​

对 ShaderMaterial 使用 setDefine():

material.setDefine('USE_TEXTURE', true);
material.setDefine('MAX_LIGHTS', '4');

着色器定义中的宏​

以编程方式创建着色器而非使用 ShaderMaterial 时,也可提供宏。

顶点和片元着色器​

使用 ShaderUtils.createShader() 创建带宏的顶点/片元着色器:

const shader = pc.ShaderUtils.createShader(device, {
uniqueName: 'MyShader',
vertexGLSL: vertexCodeGLSL,
vertexWGSL: vertexCodeWGSL,
fragmentGLSL: fragmentCodeGLSL,
fragmentWGSL: fragmentCodeWGSL,
vertexDefines: definesMap,
fragmentDefines: definesMap
});

计算着色器​

计算着色器直接通过 Shader 类创建:

const shader = new pc.Shader(device, {
name: 'MyComputeShader',
shaderLanguage: pc.SHADERLANGUAGE_WGSL,
cshader: computeCode,
cincludes: includesMap, // Custom includes for compute shader
cdefines: definesMap // Defines for compute shader
});

包含映射​

includes 映射为 #include 指令提供内容:

const includesMap = new Map([
['myChunk', 'float helper() { return 1.0; }'],
['anotherChunk', '// More shader code...']
]);

也可使用引擎提供的代码块:

cincludes: pc.ShaderChunks.get(device, pc.SHADERLANGUAGE_WGSL)

宏映射​

defines 映射的键为宏名称,注入宏需包含花括号:

// Regular defines (for #ifdef, #if)
const definesMap = new Map([
['FEATURE_ENABLED', ''], // Flag define (no value)
['MAX_LIGHTS', '4'] // Value define
]);

// Injection defines (for direct replacement)
const definesMap = new Map([
['{WORKGROUP_SIZE}', '64'],
['{TILE_SIZE}', '16']
]);

最佳实践​

  1. 使用普通宏配合 #ifdef 和 #if 进行条件编译。
  2. 在非预处理位置需要直接字符串替换时,使用 {NAME} 注入宏。
  3. 有现成实现时,优先使用引擎代码块,保证跨平台兼容。