跳到主要内容

着色器宏

如果说 ShaderData 串联了用户侧的资源组件和渲染侧的Subpass,那么ShaderMacroCollection 则串联了用户侧的资源组件和渲染侧的WGSLEncoder。 WGSL 编码器根据宏来判断是否需要生成特定的着色器片段,例如:

void WGSLWorldPosShare::operator()(WGSLEncoder& encoder,
const ShaderMacroCollection& macros, size_t counterIndex) {
if (macros.contains(NEED_WORLDPOS)) {
encoder.addInoutType(_outputStructName, WGSLEncoder::getCounterNumber(counterIndex),
"v_pos", UniformType::Vec3f32);
}
}

宏由 ShaderMacroCollection 进行管理,本质就是一个 map:

std::map<size_t, double> _value{};
note

实际上宏是对 wgpu::ConstantEntry 的一种模拟:

struct ConstantEntry {
ChainedStruct const * nextInChain = nullptr;
char const * key;
double value;
};

但是一方面目前 wgpu::ConstantEntry 还不可用,另外一方面对于例如结构体可选变量之类的场景,wgpu::ConstantEntry 无法像 MSL 一样发挥作用。更重要的是,结合宏和 WGSLEncoder 也可以在构造着色器代码的时候,直接拿到一些数据绑定的反射信息,例如 wgpu::BindGroupLayoutEntry,这样一来不需要额外的反射工具就能够搭建灵活的渲染管线。

map 当中存储了键值和一个 double 类型的数据,实际上宏只有两种,布尔宏变量宏。大多数宏只是一个true 或者 false 的开关,因此有两类实现:

void ShaderMacroCollection::enableMacro(const std::string& macroName) {
_value.insert(std::make_pair(std::hash<std::string>{}(macroName), 1));
}

void ShaderMacroCollection::enableMacro(const std::string& macroName, double value) {
_value.insert(std::make_pair(std::hash<std::string>{}(macroName), value));
}

合并顺序

对于渲染而言,宏主要存在于四种类型:SceneCameraRendererMaterial,对应于:

/**
* Shader data grouping.
*/
enum class ShaderDataGroup {
/** Scene group. */
Scene,
/** Camera group. */
Camera,
/** Renderer group. */
Renderer,
/** material group. */
Material
};

首先 Scene 当中保存的 LightManager 会将光源数量合并到 Scene 的宏当中:

void LightManager::updateShaderData(wgpu::Device& device, ShaderData &shaderData) {
...
if (directLightCount) {
shaderData.enableMacro(DIRECT_LIGHT_COUNT, directLightCount);
shaderData.setData(LightManager::_directLightProperty, _directLightDatas);
} else {
shaderData.disableMacro(DIRECT_LIGHT_COUNT);
}

if (pointLightCount) {
shaderData.enableMacro(POINT_LIGHT_COUNT, pointLightCount);
shaderData.setData(LightManager::_pointLightProperty, _pointLightDatas);
} else {
shaderData.disableMacro(POINT_LIGHT_COUNT);
}

if (spotLightCount) {
shaderData.enableMacro(SPOT_LIGHT_COUNT, spotLightCount);
shaderData.setData(LightManager::_spotLightProperty, _spotLightDatas);
} else {
shaderData.disableMacro(SPOT_LIGHT_COUNT);
}
}

接着在 ForwardSubpass 中会合并 SceneCamera 当中的宏:

void ForwardSubpass::_drawMeshes(wgpu::RenderPassEncoder &passEncoder) {
auto compileMacros = ShaderMacroCollection();
_scene->shaderData.mergeMacro(compileMacros, compileMacros);
_camera->shaderData.mergeMacro(compileMacros, compileMacros);

...
}

最后在循环 RenerElement 时,会将 MaterialRenderer 的宏合并进来:

void ForwardSubpass::_drawElement(wgpu::RenderPassEncoder &passEncoder,
const std::vector<RenderElement> &items,
const ShaderMacroCollection& compileMacros) {
for (auto &element : items) {
auto macros = compileMacros;
auto& renderer = element.renderer;
renderer->shaderData.mergeMacro(macros, macros);
auto& material = element.material;
material->shaderData.mergeMacro(macros, macros);

...
}
}

得到的宏会被发送给 Shader 用来查找缓存或者生成新的着色器代码:

std::pair<const std::string&, const WGSL::BindGroupInfo&> 
WGSLCache::compile(const ShaderMacroCollection& macros) {
size_t hash = macros.hash();
auto iter = _sourceCache.find(hash);
if (iter == _sourceCache.end()) {
_createShaderSource(hash, macros);
}
return {_sourceCache[hash], _infoCache[hash]};
}

内部宏

为了在编写 WGSLEncoder 时更加方便,以及方便开发者使用宏,字符串形式的宏并不是特别方便,因此将内置宏收拢到一个枚举类型当中,例如:

shader/internal_macro_name.h
// int have no verb, other will use:
// HAS_ : Resouce
// OMMIT_ : Omit Resouce
// NEED_ : Shader Operation
// IS_ : Shader control flow
// _COUNT: type int constant
enum MacroName {
HAS_UV = 0,
HAS_NORMAL,
HAS_TANGENT,
HAS_VERTEXCOLOR,

// Blend Shape
HAS_BLENDSHAPE,
HAS_BLENDSHAPE_TEXTURE,
HAS_BLENDSHAPE_NORMAL,
HAS_BLENDSHAPE_TANGENT,

// Skin
HAS_SKIN,
HAS_JOINT_TEXTURE,
JOINTS_COUNT,

...
}

并且在 ShaderMacroCollection 中将其转换成字符串生成的哈希值,作为宏 map 的键值:

std::vector<size_t> ShaderMacroCollection::_internalMacroHashValue = {
std::hash<std::string>{}("HAS_UV"),
std::hash<std::string>{}("HAS_NORMAL"),
std::hash<std::string>{}("HAS_TANGENT"),
std::hash<std::string>{}("HAS_VERTEXCOLOR"),

// Blend Shape
std::hash<std::string>{}("HAS_BLENDSHAPE"),
std::hash<std::string>{}("HAS_BLENDSHAPE_TEXTURE"),
std::hash<std::string>{}("HAS_BLENDSHAPE_NORMAL"),
std::hash<std::string>{}("HAS_BLENDSHAPE_TANGENT"),

// Skin
std::hash<std::string>{}("HAS_SKIN"),
std::hash<std::string>{}("HAS_JOINT_TEXTURE"),
std::hash<std::string>{}("JOINTS_COUNT"),

...
}

Arche.js 中的实践

Arche.js 中宏的配置和上述类似,但是为了兼容 Oasis Engine 中的宏因此所区别。 首先宏名并不是直接通过 map 存储在 ShaderMacroCollection 当中的,而是在 Shader 中统一声明的:

/**
* Shader containing vertex and fragment source.
*/
export class Shader {
private static _macroMap: Record<string, ShaderMacro> = Object.create(null);

static getMacroByName(name: MacroName): ShaderMacro;

static getMacroByName(name: string): ShaderMacro;

/**
* Get shader macro by name.
* @param name - Name of the shader macro
* @returns Shader macro
*/
static getMacroByName(name: string): ShaderMacro {
let macro = Shader._macroMap[name];
if (!macro) {
const maskMap = Shader._macroMaskMap;
const counter = Shader._macroCounter;
const index = Math.floor(counter / 32);
const bit = counter % 32;
macro = new ShaderMacro(name, index, 1 << bit);
Shader._macroMap[name] = macro;
if (index == maskMap.length) {
maskMap.length++;
maskMap[index] = new Array<string>(32);
}
maskMap[index][bit] = name;
Shader._macroCounter++;
}
return macro;
}
}

ShaderMacroCollection 负责存储变量宏和布尔宏的值:

/**
* Shader macro collection.
* @internal
*/
export class ShaderMacroCollection {
/** @internal */
_variableMacros: Map<string, string> = new Map<string, string>();
/** @internal */
_mask: number[] = [];
/** @internal */
_length: number = 0;
}

尽管这样的做法看似繁琐,实际上兼容了 GLSL 宏的使用,因为在 GLSL 中,宏是通过例如:

#define HAS_UV

这样方式在着色器代码的开头声明的,因此需要保存宏的字符串,通过上述方式,可以减少字符串存储所需的空间。

同时,TypeScript 中可以使用字符串定义类型,且该类型和字符串等价转换,因此对于内部宏可以定义:

export type MacroName =
"HAS_UV"
| "HAS_NORMAL"
| "HAS_TANGENT"
| "HAS_VERTEXCOLOR"

// Blend Shape
| "HAS_BLENDSHAPE"
| "HAS_BLENDSHAPE_TEXTURE"
| "HAS_BLENDSHAPE_NORMAL"
| "HAS_BLENDSHAPE_TANGENT"

// Skin
| "HAS_SKIN"
| "HAS_JOINT_TEXTURE"
| "JOINTS_COUNT"

只需要为相关宏函数添加重载声明即可:

export class ShaderMacroCollection {
isEnable(macroName: MacroName): boolean;

isEnable(macroName: string): boolean;

isEnable(macroName: string): boolean {
const variableValue = this._variableMacros.get(macroName);
if (variableValue !== undefined) {
const macro = Shader.getMacroByName(`${macroName} ${variableValue}`);
return this._isEnable(macro);
} else {
const macro = Shader.getMacroByName(macroName);
return this._isEnable(macro);
}
}
}