playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
309 lines (308 loc) • 12.3 kB
JavaScript
import { Debug } from "../../core/debug.js";
import {
SEMANTIC_POSITION,
SEMANTIC_NORMAL,
SEMANTIC_TANGENT,
SEMANTIC_TEXCOORD0,
SEMANTIC_TEXCOORD1,
SEMANTIC_TEXCOORD2,
SEMANTIC_TEXCOORD3,
SEMANTIC_TEXCOORD4,
SEMANTIC_TEXCOORD5,
SEMANTIC_TEXCOORD6,
SEMANTIC_TEXCOORD7,
SEMANTIC_COLOR,
SEMANTIC_BLENDINDICES,
SEMANTIC_BLENDWEIGHT,
SHADERLANGUAGE_WGSL,
SHADERLANGUAGE_GLSL,
primitiveGlslToWgslTypeMap
} from "./constants.js";
import gles3FS from "./shader-chunks/frag/gles3.js";
import gles3VS from "./shader-chunks/vert/gles3.js";
import webgpuFS from "./shader-chunks/frag/webgpu.js";
import webgpuVS from "./shader-chunks/vert/webgpu.js";
import wgslFS from "./shader-chunks/frag/webgpu-wgsl.js";
import wgslVS from "./shader-chunks/vert/webgpu-wgsl.js";
import sharedGLSL from "./shader-chunks/frag/shared.js";
import sharedWGSL from "./shader-chunks/frag/shared-wgsl.js";
import halfTypes from "./shader-chunks/frag/half-types.js";
const _attrib2Semantic = {
vertex_position: SEMANTIC_POSITION,
vertex_normal: SEMANTIC_NORMAL,
vertex_tangent: SEMANTIC_TANGENT,
vertex_texCoord0: SEMANTIC_TEXCOORD0,
vertex_texCoord1: SEMANTIC_TEXCOORD1,
vertex_texCoord2: SEMANTIC_TEXCOORD2,
vertex_texCoord3: SEMANTIC_TEXCOORD3,
vertex_texCoord4: SEMANTIC_TEXCOORD4,
vertex_texCoord5: SEMANTIC_TEXCOORD5,
vertex_texCoord6: SEMANTIC_TEXCOORD6,
vertex_texCoord7: SEMANTIC_TEXCOORD7,
vertex_color: SEMANTIC_COLOR,
vertex_boneIndices: SEMANTIC_BLENDINDICES,
vertex_boneWeights: SEMANTIC_BLENDWEIGHT
};
class ShaderDefinitionUtils {
/**
* Creates a shader definition.
*
* @param {GraphicsDevice} device - The graphics device.
* @param {object} options - Object for passing optional arguments.
* @param {string} [options.name] - A name of the shader.
* @param {object} [options.attributes] - Attributes. Will be extracted from the vertexCode if
* not provided.
* @param {string} options.vertexCode - The vertex shader code.
* @param {string} [options.fragmentCode] - The fragment shader code.
* @param {string} [options.fragmentPreamble] - The preamble string for the fragment shader.
* @param {string[]} [options.feedbackVaryings] - A list of shader output variable
* names that will be captured when using transform feedback. This setting is only effective
* if the useTransformFeedback property is enabled.
* @param {boolean} [options.useTransformFeedback] - Whether to use transform feedback. Defaults
* to false.
* @param {Map<string, string>} [options.vertexIncludes] - A map containing key-value pairs of
* include names and their content. These are used for resolving #include directives in the
* vertex shader source.
* @param {Map<string, string>} [options.vertexDefines] - A map containing key-value pairs of
* define names and their values. These are used for resolving #ifdef style of directives in the
* vertex code.
* @param {Map<string, string>} [options.fragmentIncludes] - A map containing key-value pairs
* of include names and their content. These are used for resolving #include directives in the
* fragment shader source.
* @param {Map<string, string>} [options.fragmentDefines] - A map containing key-value pairs of
* define names and their values. These are used for resolving #ifdef style of directives in the
* fragment code.
* @param {string | string[]} [options.fragmentOutputTypes] - Fragment shader output types,
* which default to vec4. Passing a string will set the output type for all color attachments.
* Passing an array will set the output type for each color attachment.
* @returns {object} Returns the created shader definition.
*/
static createDefinition(device, options) {
Debug.assert(options);
Debug.assert(!options.vertexDefines || options.vertexDefines instanceof Map);
Debug.assert(!options.vertexIncludes || options.vertexIncludes instanceof Map);
Debug.assert(!options.fragmentDefines || options.fragmentDefines instanceof Map);
Debug.assert(!options.fragmentIncludes || options.fragmentIncludes instanceof Map);
const normalizedOutputTypes = (options2) => {
let fragmentOutputTypes = options2.fragmentOutputTypes ?? "vec4";
if (!Array.isArray(fragmentOutputTypes)) {
fragmentOutputTypes = [fragmentOutputTypes];
}
return fragmentOutputTypes;
};
const getDefines = (gpu, gl2, isVertex, options2) => {
const deviceIntro = device.isWebGPU ? gpu : gl2;
let attachmentsDefine = "";
if (!isVertex) {
const fragmentOutputTypes = normalizedOutputTypes(options2);
for (let i = 0; i < device.maxColorAttachments; i++) {
attachmentsDefine += `#define COLOR_ATTACHMENT_${i}
`;
const outType = fragmentOutputTypes[i] ?? "vec4";
attachmentsDefine += `#define outType_${i} ${outType}
`;
}
}
return attachmentsDefine + deviceIntro;
};
const getDefinesWgsl = (isVertex, options2) => {
let code = ShaderDefinitionUtils.getWGSLEnables(device, isVertex ? "vertex" : "fragment");
if (!isVertex) {
const fragmentOutputTypes = normalizedOutputTypes(options2);
for (let i = 0; i < device.maxColorAttachments; i++) {
const glslOutType = fragmentOutputTypes[i] ?? "vec4";
const wgslOutType = primitiveGlslToWgslTypeMap.get(glslOutType);
Debug.assert(wgslOutType, `Unknown output type translation: ${glslOutType} -> ${wgslOutType}`);
code += `alias pcOutType${i} = ${wgslOutType};
`;
}
}
return code;
};
const name = options.name ?? "Untitled";
let vertCode;
let fragCode;
const vertexDefinesCode = ShaderDefinitionUtils.getDefinesCode(device, options.vertexDefines);
const fragmentDefinesCode = ShaderDefinitionUtils.getDefinesCode(device, options.fragmentDefines);
const wgsl = options.shaderLanguage === SHADERLANGUAGE_WGSL;
if (wgsl) {
vertCode = `
${getDefinesWgsl(true, options)}
${vertexDefinesCode}
${halfTypes}
${wgslVS}
${sharedWGSL}
${options.vertexCode}
`;
fragCode = `
${getDefinesWgsl(false, options)}
${fragmentDefinesCode}
${halfTypes}
${wgslFS}
${sharedWGSL}
${options.fragmentCode}
`;
} else {
Debug.assert(options.vertexCode);
vertCode = `${ShaderDefinitionUtils.versionCode(device) + getDefines(webgpuVS, gles3VS, true, options) + vertexDefinesCode + ShaderDefinitionUtils.precisionCode(device)}
${sharedGLSL}
${ShaderDefinitionUtils.getShaderNameCode(name)}
${options.vertexCode}`;
Debug.assert(options.fragmentCode);
fragCode = `${(options.fragmentPreamble || "") + ShaderDefinitionUtils.versionCode(device) + getDefines(webgpuFS, gles3FS, false, options) + fragmentDefinesCode + ShaderDefinitionUtils.precisionCode(device)}
${sharedGLSL}
${ShaderDefinitionUtils.getShaderNameCode(name)}
${options.fragmentCode}`;
}
return {
name,
shaderLanguage: options.shaderLanguage ?? SHADERLANGUAGE_GLSL,
attributes: options.attributes,
vshader: vertCode,
vincludes: options.vertexIncludes,
fincludes: options.fragmentIncludes,
fshader: fragCode,
feedbackVaryings: options.feedbackVaryings,
useTransformFeedback: options.useTransformFeedback,
meshUniformBufferFormat: options.meshUniformBufferFormat,
meshBindGroupFormat: options.meshBindGroupFormat
};
}
/**
* Generates WGSL `enable` / `requires` directives based on device capabilities. They must come
* before all global declarations in WGSL shaders.
*
* @param {GraphicsDevice} device - The graphics device.
* @param {'vertex'|'fragment'|'compute'} shaderType - The type of shader.
* @returns {string} The WGSL enable directives code.
* @ignore
*/
static getWGSLEnables(device, shaderType) {
let code = "";
if (device.supportsShaderF16) {
code += "enable f16;\n";
}
if (shaderType === "fragment" && device.supportsPrimitiveIndex) {
code += "enable primitive_index;\n";
}
if (device.supportsSubgroups) {
code += "enable subgroups;\n";
}
if (device.supportsSubgroupId) {
code += "requires subgroup_id;\n";
}
if (shaderType === "compute" && device.supportsLinearIndexing) {
code += "requires linear_indexing;\n";
}
if (device.supportsUnrestrictedPointerParameters) {
code += "requires unrestricted_pointer_parameters;\n";
}
if (device.supportsPointerCompositeAccess) {
code += "requires pointer_composite_access;\n";
}
if (device.supportsPacked4x8IntegerDotProduct) {
code += "requires packed_4x8_integer_dot_product;\n";
}
if (device.supportsTextureAndSamplerLet) {
code += "requires texture_and_sampler_let;\n";
}
return code;
}
/**
* @param {GraphicsDevice} device - The graphics device.
* @param {Map<string, string>} [defines] - A map containing key-value pairs.
* @returns {string} The shader code for the defines.
* @ignore
*/
static getDefinesCode(device, defines) {
let code = "";
device.capsDefines.forEach((value, key) => {
code += `#define ${key} ${value}
`;
});
code += "\n";
defines?.forEach((value, key) => {
code += `#define ${key} ${value}
`;
});
code += "\n";
return code;
}
// SpectorJS integration
static getShaderNameCode(name) {
return `#define SHADER_NAME ${name}
`;
}
static versionCode(device) {
return device.isWebGPU ? "#version 450\n" : "#version 300 es\n";
}
static precisionCode(device, forcePrecision) {
if (forcePrecision && forcePrecision !== "highp" && forcePrecision !== "mediump" && forcePrecision !== "lowp") {
forcePrecision = null;
}
if (forcePrecision) {
if (forcePrecision === "highp" && device.maxPrecision !== "highp") {
forcePrecision = "mediump";
}
if (forcePrecision === "mediump" && device.maxPrecision === "lowp") {
forcePrecision = "lowp";
}
}
const precision = forcePrecision ? forcePrecision : device.precision;
const code = `
precision ${precision} float;
precision ${precision} int;
precision ${precision} usampler2D;
precision ${precision} isampler2D;
precision ${precision} sampler2DShadow;
precision ${precision} samplerCubeShadow;
precision ${precision} sampler2DArray;
`;
return code;
}
/**
* Extract the attributes specified in a vertex shader.
*
* @param {string} vsCode - The vertex shader code.
* @returns {Object<string, string>} The attribute name to semantic map.
* @ignore
*/
static collectAttributes(vsCode) {
const attribs = {};
let attrs = 0;
let found = vsCode.indexOf("attribute");
while (found >= 0) {
if (found > 0 && vsCode[found - 1] === "/") break;
let ignore = false;
if (found > 0) {
let startOfLine = vsCode.lastIndexOf("\n", found);
startOfLine = startOfLine !== -1 ? startOfLine + 1 : 0;
const lineStartString = vsCode.substring(startOfLine, found);
if (lineStartString.includes("#")) {
ignore = true;
}
}
if (!ignore) {
const endOfLine = vsCode.indexOf(";", found);
const startOfAttribName = vsCode.lastIndexOf(" ", endOfLine);
const attribName = vsCode.substring(startOfAttribName + 1, endOfLine);
if (attribs[attribName]) {
Debug.warn(`Attribute [${attribName}] already exists when extracting the attributes from the vertex shader, ignoring.`, { vsCode });
} else {
const semantic = _attrib2Semantic[attribName];
if (semantic !== void 0) {
attribs[attribName] = semantic;
} else {
attribs[attribName] = `ATTR${attrs}`;
attrs++;
}
}
}
found = vsCode.indexOf("attribute", found + 1);
}
return attribs;
}
}
export {
ShaderDefinitionUtils
};