UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

577 lines (571 loc) 22.8 kB
import { uniformTypeToNameMapWGSL, UNUSED_UNIFORM_NAME, UNIFORMTYPE_FLOAT, BINDGROUP_MESH_UB, SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT, BINDGROUP_MESH, bindGroupNames, uniformTypeToNameWGSL, SAMPLETYPE_DEPTH, semanticToLocation, TYPE_FLOAT32, TYPE_FLOAT16, TEXTUREDIMENSION_CUBE_ARRAY, TEXTUREDIMENSION_CUBE, TEXTUREDIMENSION_2D_ARRAY, TEXTUREDIMENSION_2D, SAMPLETYPE_UNFILTERABLE_FLOAT, SAMPLETYPE_FLOAT, TEXTUREDIMENSION_3D, TEXTUREDIMENSION_1D, SAMPLETYPE_INT, SAMPLETYPE_UINT, TYPE_INT8, TYPE_INT16, TYPE_INT32 } from '../constants.js'; import { UniformFormat, UniformBufferFormat } from '../uniform-buffer-format.js'; import { BindTextureFormat, BindStorageBufferFormat, BindGroupFormat } from '../bind-group-format.js'; const KEYWORD = /^[ \t]*(attribute|varying|uniform)[\t ]+/gm; const KEYWORD_LINE = /^[ \t]*(attribute|varying|uniform)[ \t]*([^;]+)(;+)/gm; const KEYWORD_RESOURCE = /^[ \t]*var\s*(?:(<storage,[^>]*>)\s*([\w\d_]+)\s*:\s*(.*?)\s*;|(<(?!storage,)[^>]*>)?\s*([\w\d_]+)\s*:\s*(texture_.*|storage_texture_.*|storage\w.*|external_texture|sampler(?:_comparison)?)\s*;)\s*$/gm; const VARYING = /(?:@interpolate\([^)]*\)\s*)?([\w]+)\s*:\s*([\w<>]+)/; const MARKER = '@@@'; const ENTRY_FUNCTION = /(@vertex|@fragment)\s*fn\s+\w+\s*\(\s*(\w+)\s*:[\s\S]*?\{/; const textureBaseInfo = { 'texture_1d': { viewDimension: TEXTUREDIMENSION_1D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_2d_array': { viewDimension: TEXTUREDIMENSION_2D_ARRAY, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_3d': { viewDimension: TEXTUREDIMENSION_3D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_cube': { viewDimension: TEXTUREDIMENSION_CUBE, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_cube_array': { viewDimension: TEXTUREDIMENSION_CUBE_ARRAY, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_multisampled_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_FLOAT }, 'texture_depth_2d': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_2d_array': { viewDimension: TEXTUREDIMENSION_2D_ARRAY, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_cube': { viewDimension: TEXTUREDIMENSION_CUBE, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_depth_cube_array': { viewDimension: TEXTUREDIMENSION_CUBE_ARRAY, baseSampleType: SAMPLETYPE_DEPTH }, 'texture_external': { viewDimension: TEXTUREDIMENSION_2D, baseSampleType: SAMPLETYPE_UNFILTERABLE_FLOAT } }; const getTextureInfo = (baseType, componentType)=>{ const baseInfo = textureBaseInfo[baseType]; let finalSampleType = baseInfo.baseSampleType; if (baseInfo.baseSampleType === SAMPLETYPE_FLOAT && baseType !== 'texture_multisampled_2d') { switch(componentType){ case 'u32': finalSampleType = SAMPLETYPE_UINT; break; case 'i32': finalSampleType = SAMPLETYPE_INT; break; case 'f32': finalSampleType = SAMPLETYPE_FLOAT; break; case 'uff': finalSampleType = SAMPLETYPE_UNFILTERABLE_FLOAT; break; } } return { viewDimension: baseInfo.viewDimension, sampleType: finalSampleType }; }; const getTextureDeclarationType = (viewDimension, sampleType)=>{ if (sampleType === SAMPLETYPE_DEPTH) { switch(viewDimension){ case TEXTUREDIMENSION_2D: return 'texture_depth_2d'; case TEXTUREDIMENSION_2D_ARRAY: return 'texture_depth_2d_array'; case TEXTUREDIMENSION_CUBE: return 'texture_depth_cube'; case TEXTUREDIMENSION_CUBE_ARRAY: return 'texture_depth_cube_array'; } } let baseTypeString; switch(viewDimension){ case TEXTUREDIMENSION_1D: baseTypeString = 'texture_1d'; break; case TEXTUREDIMENSION_2D: baseTypeString = 'texture_2d'; break; case TEXTUREDIMENSION_2D_ARRAY: baseTypeString = 'texture_2d_array'; break; case TEXTUREDIMENSION_3D: baseTypeString = 'texture_3d'; break; case TEXTUREDIMENSION_CUBE: baseTypeString = 'texture_cube'; break; case TEXTUREDIMENSION_CUBE_ARRAY: baseTypeString = 'texture_cube_array'; break; } let coreFormatString; switch(sampleType){ case SAMPLETYPE_FLOAT: case SAMPLETYPE_UNFILTERABLE_FLOAT: coreFormatString = 'f32'; break; case SAMPLETYPE_UINT: coreFormatString = 'u32'; break; case SAMPLETYPE_INT: coreFormatString = 'i32'; break; } return `${baseTypeString}<${coreFormatString}>`; }; const wrappedArrayTypes = { 'f32': 'WrappedF32', 'i32': 'WrappedI32', 'u32': 'WrappedU32', 'vec2f': 'WrappedVec2F', 'vec2i': 'WrappedVec2I', 'vec2u': 'WrappedVec2U' }; const splitToWords = (line)=>{ line = line.replace(/\s+/g, ' ').trim(); return line.split(/[\s:]+/); }; const UNIFORM_ARRAY_REGEX = /array<([^,]+),\s*([^>]+)>/; class UniformLine { constructor(line, shader){ this.ubName = null; this.arraySize = 0; this.line = line; const parts = splitToWords(line); if (parts.length < 2) { shader.failed = true; return; } this.name = parts[0]; this.type = parts.slice(1).join(' '); if (this.type.includes('array<')) { const match = UNIFORM_ARRAY_REGEX.exec(this.type); this.type = match[1].trim(); this.arraySize = Number(match[2]); if (isNaN(this.arraySize)) { shader.failed = true; } } } } const TEXTURE_REGEX = /^\s*var\s+(\w+)\s*:\s*(texture_\w+)(?:<(\w+)>)?;\s*$/; const STORAGE_TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*(texture_storage_2d|texture_storage_2d_array)<([\w\d_]+),\s*(\w+)>\s*;\s*$/; const STORAGE_BUFFER_REGEX = /^\s*var\s*<storage,\s*(read|write)?>\s*([\w\d_]+)\s*:\s*(.*)\s*;\s*$/; const EXTERNAL_TEXTURE_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*texture_external;\s*$/; const SAMPLER_REGEX = /^\s*var\s+([\w\d_]+)\s*:\s*(sampler|sampler_comparison)\s*;\s*$/; class ResourceLine { constructor(line, shader){ this.originalLine = line; this.line = line; this.isTexture = false; this.isSampler = false; this.isStorageTexture = false; this.isStorageBuffer = false; this.isExternalTexture = false; this.type = ''; this.matchedElements = []; const textureMatch = this.line.match(TEXTURE_REGEX); if (textureMatch) { this.name = textureMatch[1]; this.type = textureMatch[2]; this.textureFormat = textureMatch[3]; this.isTexture = true; this.matchedElements.push(...textureMatch); const info = getTextureInfo(this.type, this.textureFormat); this.textureDimension = info.viewDimension; this.sampleType = info.sampleType; } const storageTextureMatch = this.line.match(STORAGE_TEXTURE_REGEX); if (storageTextureMatch) { this.isStorageTexture = true; this.name = storageTextureMatch[1]; this.textureType = storageTextureMatch[2]; this.format = storageTextureMatch[3]; this.access = storageTextureMatch[4]; this.matchedElements.push(...storageTextureMatch); } const storageBufferMatch = this.line.match(STORAGE_BUFFER_REGEX); if (storageBufferMatch) { this.isStorageBuffer = true; this.accessMode = storageBufferMatch[1] || 'none'; this.name = storageBufferMatch[2]; this.type = storageBufferMatch[3]; this.matchedElements.push(...storageBufferMatch); } const externalTextureMatch = this.line.match(EXTERNAL_TEXTURE_REGEX); if (externalTextureMatch) { this.name = externalTextureMatch[1]; this.isExternalTexture = true; this.matchedElements.push(...storageBufferMatch); } const samplerMatch = this.line.match(SAMPLER_REGEX); if (samplerMatch) { this.name = samplerMatch[1]; this.samplerType = samplerMatch[2]; this.isSampler = true; this.matchedElements.push(...samplerMatch); } if (this.matchedElements.length === 0) { shader.failed = true; } } equals(other) { if (this.name !== other.name) return false; if (this.type !== other.type) return false; if (this.isTexture !== other.isTexture) return false; if (this.isSampler !== other.isSampler) return false; if (this.isStorageTexture !== other.isStorageTexture) return false; if (this.isStorageBuffer !== other.isStorageBuffer) return false; if (this.isExternalTexture !== other.isExternalTexture) return false; if (this.textureFormat !== other.textureFormat) return false; if (this.textureDimension !== other.textureDimension) return false; if (this.sampleType !== other.sampleType) return false; if (this.textureType !== other.textureType) return false; if (this.format !== other.format) return false; if (this.access !== other.access) return false; if (this.accessMode !== other.accessMode) return false; if (this.samplerType !== other.samplerType) return false; return true; } } class WebgpuShaderProcessorWGSL { static run(device, shaderDefinition, shader) { const varyingMap = new Map(); const vertexExtracted = WebgpuShaderProcessorWGSL.extract(shaderDefinition.vshader); const fragmentExtracted = WebgpuShaderProcessorWGSL.extract(shaderDefinition.fshader); const attributesMap = new Map(); const attributesBlock = WebgpuShaderProcessorWGSL.processAttributes(vertexExtracted.attributes, shaderDefinition.attributes, attributesMap, shaderDefinition.processingOptions, shader); const vertexVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(vertexExtracted.varyings, varyingMap, true); const fragmentVaryingsBlock = WebgpuShaderProcessorWGSL.processVaryings(fragmentExtracted.varyings, varyingMap, false); const concatUniforms = vertexExtracted.uniforms.concat(fragmentExtracted.uniforms); const uniforms = Array.from(new Set(concatUniforms)); const parsedUniforms = uniforms.map((line)=>new UniformLine(line, shader)); const uniformsData = WebgpuShaderProcessorWGSL.processUniforms(device, parsedUniforms, shaderDefinition.processingOptions, shader); vertexExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(vertexExtracted.src, parsedUniforms); fragmentExtracted.src = WebgpuShaderProcessorWGSL.renameUniformAccess(fragmentExtracted.src, parsedUniforms); const parsedResources = WebgpuShaderProcessorWGSL.mergeResources(vertexExtracted.resources, fragmentExtracted.resources, shader); const resourcesData = WebgpuShaderProcessorWGSL.processResources(device, parsedResources, shaderDefinition.processingOptions, shader); const fOutput = WebgpuShaderProcessorWGSL.generateFragmentOutputStruct(fragmentExtracted.src, device.maxColorAttachments); vertexExtracted.src = WebgpuShaderProcessorWGSL.copyInputs(vertexExtracted.src, shader); fragmentExtracted.src = WebgpuShaderProcessorWGSL.copyInputs(fragmentExtracted.src, shader); const vBlock = `${attributesBlock}\n${vertexVaryingsBlock}\n${uniformsData.code}\n${resourcesData.code}\n`; const vshader = vertexExtracted.src.replace(MARKER, vBlock); const fBlock = `${fragmentVaryingsBlock}\n${fOutput}\n${uniformsData.code}\n${resourcesData.code}\n`; const fshader = fragmentExtracted.src.replace(MARKER, fBlock); return { vshader: vshader, fshader: fshader, attributes: attributesMap, meshUniformBufferFormat: uniformsData.meshUniformBufferFormat, meshBindGroupFormat: resourcesData.meshBindGroupFormat }; } static extract(src) { const attributes = []; const varyings = []; const uniforms = []; const resources = []; let replacement = `${MARKER}\n`; let match; while((match = KEYWORD.exec(src)) !== null){ const keyword = match[1]; KEYWORD_LINE.lastIndex = match.index; const 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]); } src = WebgpuShaderProcessorWGSL.cutOut(src, match.index, KEYWORD_LINE.lastIndex, replacement); KEYWORD.lastIndex = match.index + replacement.length; replacement = ''; } while((match = KEYWORD_RESOURCE.exec(src)) !== null){ resources.push(match[0]); src = WebgpuShaderProcessorWGSL.cutOut(src, match.index, KEYWORD_RESOURCE.lastIndex, replacement); KEYWORD_RESOURCE.lastIndex = match.index + replacement.length; replacement = ''; } return { src, attributes, varyings, uniforms, resources }; } static processUniforms(device, uniforms, processingOptions, shader) { const meshUniforms = []; uniforms.forEach((uniform)=>{ if (!processingOptions.hasUniform(uniform.name)) { uniform.ubName = 'ub_mesh_ub'; const uniformType = uniformTypeToNameMapWGSL.get(uniform.type); const uniformFormat = new UniformFormat(uniform.name, uniformType, uniform.arraySize); meshUniforms.push(uniformFormat); } else { uniform.ubName = 'ub_view'; } }); if (meshUniforms.length === 0) { meshUniforms.push(new UniformFormat(UNUSED_UNIFORM_NAME, UNIFORMTYPE_FLOAT)); } const meshUniformBufferFormat = new UniformBufferFormat(device, meshUniforms); let code = ''; processingOptions.uniformFormats.forEach((format, bindGroupIndex)=>{ if (format) { code += WebgpuShaderProcessorWGSL.getUniformShaderDeclaration(format, bindGroupIndex, 0); } }); if (meshUniformBufferFormat) { code += WebgpuShaderProcessorWGSL.getUniformShaderDeclaration(meshUniformBufferFormat, BINDGROUP_MESH_UB, 0); } return { code, meshUniformBufferFormat }; } static renameUniformAccess(source, uniforms) { uniforms.forEach((uniform)=>{ const srcName = `uniform.${uniform.name}`; const dstName = `${uniform.ubName}.${uniform.name}`; const regex = new RegExp(`\\b${srcName}\\b`, 'g'); source = source.replace(regex, dstName); }); return source; } static mergeResources(vertex, fragment, shader) { const resources = vertex.map((line)=>new ResourceLine(line, shader)); const fragmentResources = fragment.map((line)=>new ResourceLine(line, shader)); fragmentResources.forEach((fragmentResource)=>{ const existing = resources.find((resource)=>resource.name === fragmentResource.name); if (existing) { if (!existing.equals(fragmentResource)) { shader.failed = true; } } else { resources.push(fragmentResource); } }); return resources; } static processResources(device, resources, processingOptions, shader) { const textureFormats = []; for(let i = 0; i < resources.length; i++){ const resource = resources[i]; if (resource.isTexture) { const sampler = resources[i + 1]; const hasSampler = sampler?.isSampler; const sampleType = resource.sampleType; const dimension = resource.textureDimension; textureFormats.push(new BindTextureFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, dimension, sampleType, hasSampler, hasSampler ? sampler.name : null)); if (hasSampler) i++; } if (resource.isStorageBuffer) { const readOnly = resource.accessMode !== 'read_write'; const bufferFormat = new BindStorageBufferFormat(resource.name, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT, readOnly); bufferFormat.format = resource.type; textureFormats.push(bufferFormat); } } const meshBindGroupFormat = new BindGroupFormat(device, textureFormats); let code = ''; processingOptions.bindGroupFormats.forEach((format, bindGroupIndex)=>{ if (format) { code += WebgpuShaderProcessorWGSL.getTextureShaderDeclaration(format, bindGroupIndex); } }); code += WebgpuShaderProcessorWGSL.getTextureShaderDeclaration(meshBindGroupFormat, BINDGROUP_MESH); return { code, meshBindGroupFormat }; } static getUniformShaderDeclaration(ubFormat, bindGroup, bindIndex) { const name = bindGroupNames[bindGroup]; const structName = `struct_ub_${name}`; let code = `struct ${structName} {\n`; ubFormat.uniforms.forEach((uniform)=>{ let typeString = uniformTypeToNameWGSL[uniform.type][0]; if (uniform.count > 0) { 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; } static getTextureShaderDeclaration(format, bindGroup) { let code = ''; format.textureFormats.forEach((format)=>{ const textureTypeName = getTextureDeclarationType(format.textureDimension, format.sampleType); code += `@group(${bindGroup}) @binding(${format.slot}) var ${format.name}: ${textureTypeName};\n`; if (format.hasSampler) { const samplerName = format.sampleType === SAMPLETYPE_DEPTH ? 'sampler_comparison' : 'sampler'; code += `@group(${bindGroup}) @binding(${format.slot + 1}) var ${format.samplerName}: ${samplerName};\n`; } }); format.storageBufferFormats.forEach((format)=>{ const access = format.readOnly ? 'read' : 'read_write'; code += `@group(${bindGroup}) @binding(${format.slot}) var<storage, ${access}> ${format.name} : ${format.format};\n`; }); return code; } static processVaryings(varyingLines, varyingMap, isVertex) { let block = ''; let blockPrivates = ''; let blockCopy = ''; varyingLines.forEach((line, index)=>{ const match = line.match(VARYING); if (match) { const name = match[1]; const type = match[2]; if (isVertex) { varyingMap.set(name, index); } else { index = varyingMap.get(name); } block += ` @location(${index}) ${line},\n`; if (!isVertex) { blockPrivates += ` var<private> ${name}: ${type};\n`; blockCopy += ` ${name} = input.${name};\n`; } } }); if (isVertex) { block += ' @builtin(position) position : vec4f,\n'; } else { block += ' @builtin(position) position : vec4f,\n'; block += ' @builtin(front_facing) frontFacing : bool,\n'; block += ' @builtin(sample_index) sampleIndex : u32\n'; } const fragmentGlobals = isVertex ? '' : ` var<private> pcPosition: vec4f; var<private> pcFrontFacing: bool; var<private> pcSampleIndex: u32; ${blockPrivates} // function to copy inputs (varyings) to private global variables fn _pcCopyInputs(input: FragmentInput) { ${blockCopy} pcPosition = input.position; pcFrontFacing = input.frontFacing; pcSampleIndex = input.sampleIndex; } `; const structName = isVertex ? 'VertexOutput' : 'FragmentInput'; return ` struct ${structName} { ${block} }; ${fragmentGlobals} `; } static generateFragmentOutputStruct(src, numRenderTargets) { let structCode = 'struct FragmentOutput {\n'; for(let i = 0; i < numRenderTargets; i++){ structCode += ` @location(${i}) color${i > 0 ? i : ''} : pcOutType${i},\n`; } const needsFragDepth = src.search(/\.fragDepth\s*=/) !== -1; if (needsFragDepth) { structCode += ' @builtin(frag_depth) fragDepth : f32\n'; } return `${structCode}};\n`; } static floatAttributeToInt(type, signed) { const longToShortMap = { 'f32': 'f32', 'vec2<f32>': 'vec2f', 'vec3<f32>': 'vec3f', 'vec4<f32>': 'vec4f' }; const shortType = longToShortMap[type] || type; const floatToIntShort = { 'f32': signed ? 'i32' : 'u32', 'vec2f': signed ? 'vec2i' : 'vec2u', 'vec3f': signed ? 'vec3i' : 'vec3u', 'vec4f': signed ? 'vec4i' : 'vec4u' }; return floatToIntShort[shortType] || null; } static processAttributes(attributeLines, shaderDefinitionAttributes = {}, attributesMap, processingOptions, shader) { let blockAttributes = ''; let blockPrivates = ''; let blockCopy = ''; attributeLines.forEach((line)=>{ const words = splitToWords(line); const name = words[0]; let type = words[1]; const originalType = type; if (shaderDefinitionAttributes.hasOwnProperty(name)) { const semantic = shaderDefinitionAttributes[name]; const location = semanticToLocation[semantic]; attributesMap.set(location, name); const element = processingOptions.getVertexElement(semantic); if (element) { const dataType = element.dataType; if (dataType !== TYPE_FLOAT32 && dataType !== TYPE_FLOAT16 && !element.normalize && !element.asInt) { const isSignedType = dataType === TYPE_INT8 || dataType === TYPE_INT16 || dataType === TYPE_INT32; type = WebgpuShaderProcessorWGSL.floatAttributeToInt(type, isSignedType); } } blockAttributes += ` @location(${location}) ${name}: ${type},\n`; blockPrivates += ` var<private> ${line};\n`; blockCopy += ` ${name} = ${originalType}(input.${name});\n`; } }); return ` struct VertexInput { ${blockAttributes} @builtin(vertex_index) vertexIndex : u32, // built-in vertex index @builtin(instance_index) instanceIndex : u32 // built-in instance index }; ${blockPrivates} var<private> pcVertexIndex: u32; var<private> pcInstanceIndex: u32; fn _pcCopyInputs(input: VertexInput) { ${blockCopy} pcVertexIndex = input.vertexIndex; pcInstanceIndex = input.instanceIndex; } `; } static copyInputs(src, shader) { const match = src.match(ENTRY_FUNCTION); if (!match || !match[2]) { return src; } const inputName = match[2]; const braceIndex = match.index + match[0].length - 1; const beginning = src.slice(0, braceIndex + 1); const end = src.slice(braceIndex + 1); const lineToInject = `\n _pcCopyInputs(${inputName});`; return beginning + lineToInject + end; } static cutOut(src, start, end, replacement) { return src.substring(0, start) + replacement + src.substring(end); } } export { WebgpuShaderProcessorWGSL };