着色器预处理器
编译着色器之前,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']
]);
最佳实践
- 使用普通宏配合
#ifdef和#if进行条件编译。 - 在非预处理位置需要直接字符串替换时,使用
{NAME}注入宏。 - 有现成实现时,优先使用引擎代码块,保证跨平台兼容。