Shader
Introduced a series of concepts such as ShaderData
, ShaderMacroCollection
, WGSLEncoder
, and finally these can be
summarized into Shader
. Shaders represent code that runs on the GPU. In the engine, construct it using WGSLPtr
, or
call a static method to cache it in a hash table keyed by name strings:
/**
* Shader containing vertex and fragment source.
*/
class Shader {
public:
Shader(const std::string &name, WGSLPtr&& vertexSource, WGSLPtr&& fragmentSource);
/**
* Create a shader.
* @param name - Name of the shader
* @param vertexSource - Vertex source creator
* @param fragmentSource - Fragment source creator
*/
static Shader *create(const std::string &name, WGSLPtr&& vertexSource, WGSLPtr&& fragmentSource);
/**
* Find a shader by name.
* @param name - Name of the shader
*/
static Shader *find(const std::string &name);
};
For the code inside the engine, it is directly initialized in the static function of ShaderPool
:
void ShaderPool::init() {
Shader::create("unlit", std::make_unique<WGSLUnlitVertex>(), std::make_unique<WGSLUnlitFragment>());
Shader::create("blinn-phong", std::make_unique<WGSLBlinnPhongVertex>(), std::make_unique<WGSLBlinnPhongFragment>());
Shader::create("pbr", std::make_unique<WGSLPbrVertex>(), std::make_unique<WGSLPbrFragment>(true));
Shader::create("pbr-specular", std::make_unique<WGSLPbrVertex>(), std::make_unique<WGSLPbrFragment>(false));
}
This way, for example UnlitMaterial
only needs to index the shader named unlit
.
Auxiliary Information for Fused Vertex and Fragment Shadersâ
In WebGPU, the binding of GPU data does not distinguish between vertex and fragment shaders, because
the wgpu::ShaderStage
property itself is stackable. Therefore, the BindGroupLayoutEntryMap
obtained in the WGSL
of
the vertex shader and fragment shader, respectively, needs to be merged into a hash table, and then
a wgpu::BindGroupLayoutDescriptor
can be constructed:
const Shader::BindGroupLayoutDescriptorMap& Shader::bindGroupLayoutDescriptors(const ShaderMacroCollection& macros) {
...
for (const auto& entryVec : _bindGroupLayoutEntryVecMap) {
wgpu::BindGroupLayoutDescriptor desc;
desc.entryCount = static_cast<uint32_t>(entryVec.second.size());
desc.entries = entryVec.second.data();
_bindGroupLayoutDescriptorMap[entryVec.first] = desc;
}
return _bindGroupLayoutDescriptorMap;
}
Shader Programâ
Shader
doesn't actually provide any functionality for compiling shader code, at best it just concatenates WGSL
of
vertex and fragment shaders, and fuses auxiliary information. In the rendering pipeline, it is necessary to use the
final obtained vertex shader and fragment shader code strings to compile into wgpu::ShaderModule
, which requires the
ability to use ShaderProgram
encapsulation:
void ShaderProgram::_createProgram(const std::string& vertexSource,
const std::string& fragmentSource) {
wgpu::ShaderModuleDescriptor desc;
wgpu::ShaderModuleWGSLDescriptor wgslDesc;
desc.nextInChain = &wgslDesc;
wgslDesc.source = vertexSource.c_str();
_vertexShader = _device.CreateShaderModule(&desc);
wgslDesc.source = fragmentSource.c_str();
_fragmentShader = _device.CreateShaderModule(&desc);
}
As you can see in the previous article, WGSLEncoder
is time-consuming to compile the shader code once, so it needs to
be cached according to the hash value of the macro. Similarly, compiling shader strings into shader code is
time-consuming, so the hash value of the shader string is calculated directly in the engine, and then
the ShaderProgram
is cached:
ShaderProgram *ResourceCache::requestShader(const std::string &vertexSource,
const std::string &fragmentSource) {
std::size_t hash{0U};
hash_combine(hash, std::hash<std::string>{}(vertexSource));
hash_combine(hash, std::hash<std::string>{}(fragmentSource));
auto iter = _state.shaders.find(hash);
if (iter == _state.shaders.end()) {
auto shader = std::make_unique<ShaderProgram>(_device, vertexSource, fragmentSource);
_state.shaders[hash] = std::move(shader);
return _state.shaders[hash].get();
} else {
return iter->second.get();
}
}
Practice in Arche.jsâ
In order to be as compatible as possible with the Oasis-Engine architecture, Arche.js adopts a simpler design here:
export class Shader {
/**
* Compile shader variant by macro name list.
*
* @remarks
* Usually a shader contains some macros,any combination of macros is called shader variant.
*
* @param engine - Engine to which the shader variant belongs
* @param macroCollection - Macro name list
*/
getShaderProgram(engine: Engine, macroCollection: ShaderMacroCollection): ShaderProgram {
const shaderProgramPool = engine._getShaderProgramPool(this);
let shaderProgram = shaderProgramPool.get(macroCollection);
if (shaderProgram) {
return shaderProgram;
}
// merge info
const vertexCode = this._vertexSource.compile(macroCollection);
vertexCode[1].forEach(((bindings, group) => {
bindings.forEach((binding => {
if (!this._bindGroupInfo.has(group)) {
this._bindGroupInfo.set(group, new Set<number>());
}
this._bindGroupInfo.get(group).add(binding);
}));
}));
const fragmentCode = this._fragmentSource.compile(macroCollection);
fragmentCode[1].forEach(((bindings, group) => {
bindings.forEach((binding => {
if (!this._bindGroupInfo.has(group)) {
this._bindGroupInfo.set(group, new Set<number>());
}
this._bindGroupInfo.get(group).add(binding);
}));
}));
// move to vecMap
this._bindGroupInfo.forEach(((bindings, group) => {
bindings.forEach((binding => {
if (!this._bindGroupLayoutEntryVecMap.has(group)) {
this._bindGroupLayoutEntryVecMap.set(group, []);
}
this._bindGroupLayoutEntryVecMap.get(group).push(this._findEntry(group, binding));
}));
}));
// generate map
this._bindGroupLayoutEntryVecMap.forEach(((entries, group) => {
const desc = new BindGroupLayoutDescriptor();
desc.entries = entries;
this._bindGroupLayoutDescriptorMap.set(group, desc);
}));
shaderProgram = new ShaderProgram(engine.device, vertexCode[0], fragmentCode[0],
this._bindGroupLayoutDescriptorMap);
shaderProgramPool.cache(shaderProgram);
return shaderProgram;
}
}
In this function, ShaderProgram
is cached into ShaderProgramPool
directly based on the address of the Shader
object and shader macros. At the same time, BindGroupLayoutDescriptorMap
is saved to ShaderProgram
, to avoid the
need to merge vertex shader and fragment shader data in the next run.