playcanvas
Version:
PlayCanvas WebGL game engine
573 lines (570 loc) • 30 kB
JavaScript
import { Debug } from '../../../core/debug.js';
import { uniformTypeToNameMapWGSL, UNUSED_UNIFORM_NAME, UNIFORMTYPE_FLOAT, BINDGROUP_MESH_UB, SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT, BINDGROUP_MESH, bindGroupNames, uniformTypeToNameWGSL, semanticToLocation, SAMPLETYPE_UINT, SAMPLETYPE_INT, SAMPLETYPE_FLOAT, TEXTUREDIMENSION_CUBE_ARRAY, TEXTUREDIMENSION_2D_ARRAY, TEXTUREDIMENSION_CUBE, TEXTUREDIMENSION_3D, TEXTUREDIMENSION_2D, TEXTUREDIMENSION_1D } from '../constants.js';
import { UniformFormat, UniformBufferFormat } from '../uniform-buffer-format.js';
import { BindTextureFormat, BindStorageBufferFormat, BindGroupFormat } from '../bind-group-format.js';
/**
* @import { GraphicsDevice } from '../graphics-device.js'
* @import { ShaderProcessorOptions } from '../shader-processor-options.js'
* @import { Shader } from '../shader.js'
*/ // matches lines where the keyword is the first non-whitespace content, followed by a whitespace
var KEYWORD = /^[ \t]*(attribute|varying|uniform)[\t ]+/gm;
// match 'attribute' and anything else till ';'
// eslint-disable-next-line
var KEYWORD_LINE = /^[ \t]*(attribute|varying|uniform)[ \t]*([^;]+)(;+)/gm;
// match global variables of type texture, storage buffer, storage texture or external texture
// eslint-disable-next-line
var KEYWORD_RESOURCE = /^[ \t]*var\s*(<[^>]+>)?\s*[\w\d_]+\s*:\s*(texture_.*|storage_texture_.*|storage.*|external_texture|array<.*>|sampler|sampler_comparison).*;\s*$/gm;
// match varying name from string like: '@interpolate(perspective, centroid) smoothColor : vec3f;'
// eslint-disable-next-line
var VARYING = /(?:\([^)]*\)\s*)?([\w]+)\s*:/;
// marker for a place in the source code to be replaced by code
var MARKER = '@@@';
var getTextureDimension = (textureType, isArray)=>{
if (isArray) {
if (textureType === '2d') return TEXTUREDIMENSION_2D_ARRAY;
else if (textureType === 'cube') return TEXTUREDIMENSION_CUBE_ARRAY;
} else {
switch(textureType){
case '1d':
return TEXTUREDIMENSION_1D;
case '2d':
return TEXTUREDIMENSION_2D;
case '3d':
return TEXTUREDIMENSION_3D;
case 'cube':
return TEXTUREDIMENSION_CUBE;
}
}
};
var getTextureTypeCode = (dimension, sampleType)=>{
var sampleFormat = sampleType === SAMPLETYPE_FLOAT ? 'f32' : sampleType === SAMPLETYPE_INT ? 'i32' : 'u32';
switch(dimension){
case TEXTUREDIMENSION_1D:
return "texture_1d<" + sampleFormat + ">";
case TEXTUREDIMENSION_2D:
return "texture_2d<" + sampleFormat + ">";
case TEXTUREDIMENSION_3D:
return "texture_3d<" + sampleFormat + ">";
case TEXTUREDIMENSION_CUBE:
return "texture_cube<" + sampleFormat + ">";
case TEXTUREDIMENSION_2D_ARRAY:
return "texture_2d_array<" + sampleFormat + ">";
case TEXTUREDIMENSION_CUBE_ARRAY:
return "texture_cube_array<" + sampleFormat + ">";
}
};
var textureFormat2SampleType = {
'f32': SAMPLETYPE_FLOAT,
'i32': SAMPLETYPE_INT,
'u32': SAMPLETYPE_UINT
};
var wrappedArrayTypes = {
'f32': 'WrappedF32',
'i32': 'WrappedI32',
'u32': 'WrappedU32',
'vec2f': 'WrappedVec2F',
'vec2i': 'WrappedVec2I',
'vec2u': 'WrappedVec2U'
};
var splitToWords = (line)=>{
// remove any double spaces
line = line.replace(/\s+/g, ' ').trim();
// Split by spaces or ':' symbol
return line.split(/[\s:]+/);
};
// matches: array<f32, 4>;
// eslint-disable-next-line
var UNIFORM_ARRAY_REGEX = /array<([^,]+),\s*([^>]+)>/;
class UniformLine {
constructor(line, shader){
/**
* A name of the ub buffer which this uniform is assigned to.
*
* @type {string|null}
*/ this.ubName = null;
this.arraySize = 0;
// Save the raw line
this.line = line;
// Use splitToWords to split the line into parts
var parts = splitToWords(line);
if (parts.length < 2) {
Debug.error("Invalid uniform line format: " + line, shader);
shader.failed = true;
return;
}
// Extract the name and type
this.name = parts[0];
this.type = parts.slice(1).join(' ');
// array of uniforms (e.g. array<f32, 5>)
if (this.type.includes('array<')) {
var match = UNIFORM_ARRAY_REGEX.exec(this.type);
Debug.assert(match, "Array type on line [" + line + "] is not supported.");
// array type
this.type = match[1].trim();
this.arraySize = Number(match[2]);
if (isNaN(this.arraySize)) {
shader.failed = true;
Debug.error("Only numerically specified uniform array sizes are supported, this uniform is not supported: '" + line + "'", shader);
}
}
}
}
// regex constants for resource lines, for example:
// var diffuseTexture : texture_2d<f32>;
// var textureArray: array<texture_2d<f32>, 5>;
// var diffuseSampler : sampler;
// var<storage, read> particles: array<Particle>;
// var<storage, read_write> storageBuffer : Buffer;
// var storageTexture : texture_storage_2d<rgba8unorm, write>;
// var videoTexture : texture_external;
// eslint-disable-next-line
var ARRAY_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*array<([\w\d_<>]+),\s*(\d+)>;\s*$/;
// eslint-disable-next-line
var TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*texture_(\w+)<([a-zA-Z0-9_,<>]*)>;\s*$/;
// eslint-disable-next-line
var STORAGE_TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*(texture_storage_2d|texture_storage_2d_array)<([\w\d_]+),\s*(\w+)>\s*;\s*$/;
// eslint-disable-next-line
var STORAGE_BUFFER_REGEX = /^\s*var\s*<storage,\s*(read|write)?>\s*([\w\d_]+)\s*:\s*(.*)\s*;\s*$/;
// eslint-disable-next-line
var EXTERNAL_TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*texture_external;\s*$/;
// eslint-disable-next-line
var SAMPLER_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*(sampler|sampler_comparison)\s*;\s*$/;
// ResourceLine class to parse the resource declarations
class ResourceLine {
constructor(line, shader){
// save the raw line
this.originalLine = line;
this.line = line;
// defaults
this.isTexture = false;
this.isSampler = false;
this.isStorageTexture = false;
this.isStorageBuffer = false;
this.isExternalTexture = false;
this.arraySize = 0;
this.type = '';
this.matchedElements = [];
// handle array format like 'array<texture_2d<f32>, 5>'
var arrayMatch = line.match(ARRAY_REGEX);
if (arrayMatch) {
this.name = arrayMatch[1]; // Extract the variable name
this.arraySize = parseInt(arrayMatch[3], 10); // Extract the array size
this.line = "var " + this.name + " : " + arrayMatch[2] + ";"; // Simplify line (remove array part)
this.matchedElements.push(...arrayMatch);
if (isNaN(this.arraySize)) {
Debug.error("Invalid array size in resource line: " + line, shader);
shader.failed = true;
}
}
// handle texture type / simplified line from the array above
var textureMatch = this.line.match(TEXTURE_REGEX);
if (textureMatch) {
this.name = textureMatch[1];
this.type = textureMatch[2]; // texture type (e.g., texture_2d)
this.textureFormat = textureMatch[3]; // texture format (e.g., f32)
this.isTexture = true;
this.matchedElements.push(...textureMatch);
this.textureDimension = getTextureDimension(this.type, this.arraySize > 0);
Debug.assert(this.textureDimension);
this.sampleType = textureFormat2SampleType[this.textureFormat];
Debug.assert(this.sampleType !== undefined);
}
// storage texture (e.g., texture_storage_2d<rgba8unorm, write>)
var storageTextureMatch = this.line.match(STORAGE_TEXTURE_REGEX);
if (storageTextureMatch) {
this.isStorageTexture = true;
this.name = storageTextureMatch[1];
this.textureType = storageTextureMatch[2]; // texture_storage_2d or texture_storage_2d_array
this.format = storageTextureMatch[3]; // format (e.g., rgba8unorm)
this.access = storageTextureMatch[4]; // access mode (e.g., write)
this.matchedElements.push(...storageTextureMatch);
}
// storage buffer (e.g., <storage, read> particles: array<Particle>;)
var storageBufferMatch = this.line.match(STORAGE_BUFFER_REGEX);
if (storageBufferMatch) {
this.isStorageBuffer = true;
this.accessMode = storageBufferMatch[1] || 'none'; // Default to 'none' if no access mode
this.name = storageBufferMatch[2];
this.type = storageBufferMatch[3]; // Everything after ':' (e.g., array<Particle>)
this.matchedElements.push(...storageBufferMatch);
}
// external texture (e.g., texture_external)
var externalTextureMatch = this.line.match(EXTERNAL_TEXTURE_REGEX);
if (externalTextureMatch) {
this.name = externalTextureMatch[1];
this.isExternalTexture = true;
this.matchedElements.push(...storageBufferMatch);
}
// sampler
var samplerMatch = this.line.match(SAMPLER_REGEX);
if (samplerMatch) {
this.name = samplerMatch[1];
this.samplerType = samplerMatch[2]; // sampler type (e.g., sampler or sampler_comparison)
this.isSampler = true;
this.matchedElements.push(...samplerMatch);
}
if (this.matchedElements.length === 0) {
Debug.error("Invalid / unsupported resource line format: " + line, shader);
shader.failed = true;
}
}
}
/**
* Pure static class implementing processing of WGSL shaders. It allocates fixed locations for
* attributes, and handles conversion of uniforms to uniform buffers.
*/ class WebgpuShaderProcessorWGSL {
/**
* Process the shader.
*
* @param {GraphicsDevice} device - The graphics device.
* @param {object} shaderDefinition - The shader definition.
* @param {Shader} shader - The shader.
* @returns {object} - The processed shader data.
*/ static run(device, shaderDefinition, shader) {
/** @type {Map<string, number>} */ var varyingMap = new Map();
// extract lines of interests from both shaders
var vertexExtracted = WebgpuShaderProcessorWGSL.extract(shaderDefinition.vshader);
var fragmentExtracted = WebgpuShaderProcessorWGSL.extract(shaderDefinition.fshader);
// VS - convert a list of attributes to a shader block with fixed locations
var attributesMap = new Map();
var attributesBlock = WebgpuShaderProcessorWGSL.processAttributes(vertexExtracted.attributes, shaderDefinition.attributes, attributesMap, shaderDefinition.processingOptions);
// VS - convert a list of varyings to a shader block
var vertexVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(vertexExtracted.varyings, varyingMap, true);
// FS - convert a list of varyings to a shader block
var fragmentVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(fragmentExtracted.varyings, varyingMap, false);
// uniforms - merge vertex and fragment uniforms, and create shared uniform buffers
// Note that as both vertex and fragment can declare the same uniform, we need to remove duplicates
var concatUniforms = vertexExtracted.uniforms.concat(fragmentExtracted.uniforms);
var uniforms = Array.from(new Set(concatUniforms));
// parse uniform lines
var parsedUniforms = uniforms.map((line)=>new UniformLine(line, shader));
// validation - as uniforms go to a shared uniform buffer, vertex and fragment versions need to match
Debug.call(()=>{
var map = new Map();
parsedUniforms.forEach((uni)=>{
var existing = map.get(uni.name);
Debug.assert(!existing, "Vertex and fragment shaders cannot use the same uniform name with different types: '" + existing + "' and '" + uni.line + "'", shader);
map.set(uni.name, uni.line);
});
});
var uniformsData = WebgpuShaderProcessorWGSL.processUniforms(device, parsedUniforms, shaderDefinition.processingOptions, shader);
// rename references to uniforms to match the uniform buffer
vertexExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(vertexExtracted.src, parsedUniforms);
fragmentExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(fragmentExtracted.src, parsedUniforms);
// parse resource lines
var concatResources = vertexExtracted.resources.concat(fragmentExtracted.resources);
var resources = Array.from(new Set(concatResources));
var parsedResources = resources.map((line)=>new ResourceLine(line, shader));
var resourcesData = WebgpuShaderProcessorWGSL.processResources(device, parsedResources, shaderDefinition.processingOptions, shader);
// generate fragment output struct
var fOutput = WebgpuShaderProcessorWGSL.generateFragmentOutputStruct(fragmentExtracted.src, device.maxColorAttachments);
// VS - insert the blocks to the source
var vBlock = attributesBlock + "\n" + vertexVaryingsBlock + "\n" + uniformsData.code + "\n" + resourcesData.code + "\n";
var vshader = vertexExtracted.src.replace(MARKER, vBlock);
// FS - insert the blocks to the source
var fBlock = fragmentVaryingsBlock + "\n" + fOutput + "\n" + uniformsData.code + "\n" + resourcesData.code + "\n";
var fshader = fragmentExtracted.src.replace(MARKER, fBlock);
return {
vshader: vshader,
fshader: fshader,
attributes: attributesMap,
meshUniformBufferFormat: uniformsData.meshUniformBufferFormat,
meshBindGroupFormat: resourcesData.meshBindGroupFormat
};
}
// Extract required information from the shader source code.
static extract(src) {
// collected data
var attributes = [];
var varyings = [];
var uniforms = [];
var resources = [];
// replacement marker - mark a first replacement place
var replacement = "" + MARKER + "\n";
var match;
// Extract uniforms, attributes, and varyings
while((match = KEYWORD.exec(src)) !== null){
var keyword = match[1];
KEYWORD_LINE.lastIndex = match.index;
var lineMatch = KEYWORD_LINE.exec(src);
if (keyword === 'attribute') {
attributes.push(lineMatch[2]);
} else if (keyword === 'varying') {
varyings.push(lineMatch[2]);
} else if (keyword === 'uniform') {
uniforms.push(lineMatch[2]);
}
// Remove the matched line from source
src = WebgpuShaderProcessorWGSL.cutOut(src, match.index, KEYWORD_LINE.lastIndex, replacement);
KEYWORD.lastIndex = match.index + replacement.length;
replacement = ''; // Only place a single replacement marker
}
// Extract resource declarations
while((match = KEYWORD_RESOURCE.exec(src)) !== null){
resources.push(match[0]); // Store the full line
// Remove the matched line from source
src = WebgpuShaderProcessorWGSL.cutOut(src, match.index, KEYWORD_RESOURCE.lastIndex, replacement);
KEYWORD_RESOURCE.lastIndex = match.index + replacement.length;
replacement = '';
}
return {
src,
attributes,
varyings,
uniforms,
resources
};
}
/**
* Process the lines with uniforms. The function receives the lines containing all numerical
* uniforms. The function also receives the format of uniform buffers for view and material
* level. All uniforms that match any of those are ignored, as those would be supplied by view /
* material level buffers. All leftover uniforms create uniform buffer and bind group for the
* mesh itself, containing uniforms that change on the level of the mesh.
*
* @param {GraphicsDevice} device - The graphics device.
* @param {Array<UniformLine>} uniforms - Lines containing uniforms.
* @param {ShaderProcessorOptions} processingOptions - Uniform formats.
* @param {Shader} shader - The shader definition.
* @returns {object} - The uniform data. Returns a shader code block containing uniforms, to be
* inserted into the shader, as well as generated uniform format structures for the mesh level.
*/ static processUniforms(device, uniforms, processingOptions, shader) {
// build mesh uniform buffer format
var meshUniforms = [];
uniforms.forEach((uniform)=>{
// uniforms not already in supplied uniform buffers go to the mesh buffer
if (!processingOptions.hasUniform(uniform.name)) {
uniform.ubName = 'ub_mesh_ub';
// Find the uniform type index in uniformTypeToNameWGSL
var uniformType = uniformTypeToNameMapWGSL.get(uniform.type);
Debug.assert(uniformType !== undefined, "Uniform type " + uniform.type + " is not recognised on line [" + uniform.line + "]");
var uniformFormat = new UniformFormat(uniform.name, uniformType, uniform.arraySize);
meshUniforms.push(uniformFormat);
} else {
// TODO: when we add material ub, this name will need to be updated
uniform.ubName = 'ub_view';
// Validate types here if needed
Debug.assert(true, "Uniform " + uniform.name + " already processed, skipping additional validation.");
}
});
// if we don't have any uniform, add a dummy uniform to avoid empty uniform buffer - WebGPU rendering does not
// support rendering will NULL bind group as binding a null buffer changes placement of other bindings
if (meshUniforms.length === 0) {
meshUniforms.push(new UniformFormat(UNUSED_UNIFORM_NAME, UNIFORMTYPE_FLOAT));
}
var meshUniformBufferFormat = new UniformBufferFormat(device, meshUniforms);
// generate code for uniform buffers, starts on the slot 0
var code = '';
processingOptions.uniformFormats.forEach((format, bindGroupIndex)=>{
if (format) {
code += WebgpuShaderProcessorWGSL.getUniformShaderDeclaration(format, bindGroupIndex, 0);
}
});
// and also for generated mesh uniform format, which is at the slot 0 of the bind group
if (meshUniformBufferFormat) {
code += WebgpuShaderProcessorWGSL.getUniformShaderDeclaration(meshUniformBufferFormat, BINDGROUP_MESH_UB, 0);
}
return {
code,
meshUniformBufferFormat
};
}
/**
* Source code references uniforms as `uniform.name`, but swap those to reference the actual uniform buffer
* the uniform was assigned to, for example `ub_view.name`.
*
* @param {string} source - The source code.
* @param {Array<UniformLine>} uniforms - Lines containing uniforms.
* @returns {string} - The source code with updated uniform references.
*/ static renameUniformAccess(source, uniforms) {
uniforms.forEach((uniform)=>{
var srcName = "uniform." + uniform.name;
var dstName = uniform.ubName + "." + uniform.name;
// Use a regular expression to match `uniform.name` as a whole word.
var regex = new RegExp("\\b" + srcName + "\\b", 'g');
source = source.replace(regex, dstName);
});
return source;
}
static processResources(device, resources, processingOptions, shader) {
// build mesh bind group format - this contains the textures, but not the uniform buffer as that is a separate binding
var textureFormats = [];
for(var i = 0; i < resources.length; i++){
var resource = resources[i];
if (resource.isTexture) {
// followed by optional sampler uniform
var sampler = resources[i + 1];
var hasSampler = sampler == null ? void 0 : sampler.isSampler;
// TODO: handle depth texture, external, and storage types
var sampleType = resource.sampleType;
var dimension = resource.textureDimension;
// TODO: we could optimize visibility to only stages that use any of the data
textureFormats.push(new BindTextureFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, dimension, sampleType, hasSampler, hasSampler ? sampler.name : null));
// following sampler was already handled
if (hasSampler) i++;
}
if (resource.isStorageBuffer) {
var readOnly = resource.accessMode !== 'read_write';
var bufferFormat = new BindStorageBufferFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, readOnly);
bufferFormat.format = resource.type;
textureFormats.push(bufferFormat);
}
Debug.assert(!resource.isSampler, "Sampler uniform needs to follow a texture uniform, but does not on line [" + resource.originalLine + "]");
Debug.assert(!resource.isStorageTexture, 'TODO: add support for storage textures here');
Debug.assert(!resource.externalTexture, 'TODO: add support for external textures here');
}
var meshBindGroupFormat = new BindGroupFormat(device, textureFormats);
// generate code for textures
var code = '';
processingOptions.bindGroupFormats.forEach((format, bindGroupIndex)=>{
if (format) {
code += WebgpuShaderProcessorWGSL.getTextureShaderDeclaration(format, bindGroupIndex, 1);
}
});
// and also for generated mesh format
code += WebgpuShaderProcessorWGSL.getTextureShaderDeclaration(meshBindGroupFormat, BINDGROUP_MESH, 0);
return {
code,
meshBindGroupFormat
};
}
/**
* Generates a shader code for a uniform buffer, something like:
* ```
* struct ub_view { matrix_viewProjection : mat4x4f }
* @group(0) @binding(0) var<uniform> ubView : ub_view;
* ```
*
* @param {UniformBufferFormat} ubFormat - Format of the uniform buffer.
* @param {number} bindGroup - The bind group index.
* @param {number} bindIndex - The bind index.
* @returns {string} - The shader code for the uniform buffer.
* @private
*/ static getUniformShaderDeclaration(ubFormat, bindGroup, bindIndex) {
var name = bindGroupNames[bindGroup];
var structName = "struct_ub_" + name;
var code = "struct " + structName + " {\n";
ubFormat.uniforms.forEach((uniform)=>{
var typeString = uniformTypeToNameWGSL[uniform.type][0];
Debug.assert(typeString !== undefined, "Uniform type " + uniform.type + " is not handled.");
// array uniforms
if (uniform.count > 0) {
// if the type is one of the ones that are not by default 16byte aligned, which is
// a requirement for uniform buffers, we need to wrap them in a struct
// for example: array<f32, 5> becomes array<WrappedF32, 5>
if (wrappedArrayTypes.hasOwnProperty(typeString)) {
typeString = wrappedArrayTypes[typeString];
}
code += " " + uniform.shortName + ": array<" + typeString + ", " + uniform.count + ">,\n";
} else {
code += " " + uniform.shortName + ": " + typeString + ",\n";
}
});
code += '};\n';
code += "@group(" + bindGroup + ") @binding(" + bindIndex + ") var<uniform> ub_" + name + " : " + structName + ";\n\n";
return code;
}
/**
* Generates a shader code for a bind group, something like:
* ```
* @group(0) @binding(0) var diffuseTexture: texture_2d<f32>;
* @group(0) @binding(1) var diffuseTexture_sampler: sampler; // optional
* ```
* @param {BindGroupFormat} format - The format of the bind group.
* @param {number} bindGroup - The bind group index.
* @param {number} startBindIndex - The starting bind index.
* @returns {string} - The shader code for the bind group.
*/ static getTextureShaderDeclaration(format, bindGroup, startBindIndex) {
var code = '';
var bindIndex = startBindIndex;
format.textureFormats.forEach((format)=>{
// convert TEXTUREDIMENSION_2D to 'texture_2d<f32>' and similar
var typeCode = getTextureTypeCode(format.textureDimension, format.sampleType);
code += "@group(" + bindGroup + ") @binding(" + bindIndex + ") var " + format.name + ": " + typeCode + ";\n";
bindIndex++;
if (format.hasSampler) {
code += "@group(" + bindGroup + ") @binding(" + bindIndex + ") var " + format.samplerName + ": sampler;\n";
bindIndex++;
}
});
format.storageBufferFormats.forEach((format)=>{
var access = format.readOnly ? 'read' : 'read_write';
code += "@group(" + bindGroup + ") @binding(" + bindIndex + ") var<storage, " + access + "> " + format.name + " : " + format.format + ";\n";
bindIndex++;
});
Debug.assert(format.storageTextureFormats.length === 0, 'Implement support for storage textures here');
// TODO: also add external texture support here
return code;
}
static processVaryings(varyingLines, varyingMap, isVertex) {
var block = '';
varyingLines.forEach((line, index)=>{
var match = line.match(VARYING);
Debug.assert(match, "Varying line is not valid: " + line);
if (match) {
var name = match[1];
if (isVertex) {
// store it in the map
varyingMap.set(name, index);
} else {
Debug.assert(varyingMap.has(name), "Fragment shader requires varying [" + name + "] but vertex shader does not generate it.");
index = varyingMap.get(name);
}
// generates: `@location(0) @interpolate(perspective, centroid) smoothColor : vec3f`
block += " @location(" + index + ") " + line + ",\n";
}
});
// add built-in varyings
if (isVertex) {
block += ' @builtin(position) position : vec4f,\n'; // output position
} else {
block += ' @builtin(position) position : vec4f,\n'; // interpolated fragment position
block += ' @builtin(front_facing) frontFacing : bool,\n'; // front-facing
block += ' @builtin(sample_index) sampleIndex : u32\n'; // sample index for MSAA
}
var structName = isVertex ? 'VertexOutput' : 'FragmentInput';
return "struct " + structName + " {\n" + block + "};\n";
}
static generateFragmentOutputStruct(src, numRenderTargets) {
var structCode = 'struct FragmentOutput {\n';
for(var i = 0; i < numRenderTargets; i++){
structCode += " @location(" + i + ") color" + (i > 0 ? i : '') + " : vec4f,\n";
}
// find if the src contains `.fragDepth =`, ignoring whitespace before = sign
var needsFragDepth = src.search(/\.fragDepth\s*=/) !== -1;
if (needsFragDepth) {
structCode += ' @builtin(frag_depth) fragDepth : f32\n';
}
return "" + structCode + "};\n";
}
static processAttributes(attributeLines, shaderDefinitionAttributes, attributesMap, processingOptions) {
if (shaderDefinitionAttributes === void 0) shaderDefinitionAttributes = {};
var block = '';
var usedLocations = {};
attributeLines.forEach((line)=>{
var words = splitToWords(line);
var name = words[0];
if (shaderDefinitionAttributes.hasOwnProperty(name)) {
var semantic = shaderDefinitionAttributes[name];
var location = semanticToLocation[semantic];
Debug.assert(location !== undefined, "Semantic " + semantic + " used by the attribute " + name + " is not known - make sure it's one of the supported semantics.");
Debug.assert(!usedLocations.hasOwnProperty(location), "WARNING: Two vertex attributes are mapped to the same location in a shader: " + usedLocations[location] + " and " + semantic);
usedLocations[location] = semantic;
// build a map of used attributes
attributesMap.set(location, name);
// generates: @location(0) position : vec4f
block += " @location(" + location + ") " + line + ",\n";
} else {
Debug.error("Attribute " + name + " is not defined in the shader definition.", shaderDefinitionAttributes);
}
});
// add built-in attributes
block += ' @builtin(vertex_index) vertexIndex : u32,\n'; // vertex index
block += ' @builtin(instance_index) instanceIndex : u32\n'; // instance index
return "struct VertexInput {\n" + block + "};\n";
}
static cutOut(src, start, end, replacement) {
return src.substring(0, start) + replacement + src.substring(end);
}
}
export { WebgpuShaderProcessorWGSL };