diff --git a/src/rendering/VertexState.js b/src/rendering/VertexState.js index a3170b94..4fbe2f07 100644 --- a/src/rendering/VertexState.js +++ b/src/rendering/VertexState.js @@ -42,7 +42,7 @@ export class VertexState { * @param {object} options * @param {PreferredShaderLocation[]} [options.preferredShaderLocations] If the vertex state * has "auto", null or -1 for an attribute, this will be used to determine the shader location. - * If this an attribute with automatic shader location is not present in this list, + * If an attribute with automatic shader location is not present in this list, * a location will be assigned that hasn't been used yet. * If this list contains the same attribute type multiple times, an error will be thrown. * If the list contains a shader location that has already been taken by the vertex state, an error will be thrown. diff --git a/src/rendering/renderers/webGl/CachedMeshBufferData.js b/src/rendering/renderers/webGl/CachedMeshBufferData.js index 401ee036..7612a6b4 100644 --- a/src/rendering/renderers/webGl/CachedMeshBufferData.js +++ b/src/rendering/renderers/webGl/CachedMeshBufferData.js @@ -2,6 +2,7 @@ import { Mesh } from "../../../core/Mesh.js"; export class CachedMeshBufferData { #meshBuffer; + #vertexStateBuffer; #cachedMeshData; #bufferDirty = true; @@ -10,10 +11,12 @@ export class CachedMeshBufferData { /** * @param {import("../../../core/MeshAttributeBuffer.js").MeshAttributeBuffer} meshBuffer + * @param {import("../../VertexStateBuffer.js").VertexStateBuffer} vertexStateBuffer * @param {import("./CachedMeshData.js").CachedMeshData} meshData */ - constructor(meshBuffer, meshData) { + constructor(meshBuffer, vertexStateBuffer, meshData) { this.#meshBuffer = meshBuffer; + this.#vertexStateBuffer = vertexStateBuffer; this.#cachedMeshData = meshData; meshBuffer.onBufferChanged(this.#onBufferChanged); @@ -54,6 +57,7 @@ export class CachedMeshBufferData { } const attributes = []; + let i = 0; for (const attributeSettings of this.#meshBuffer.attributeSettings) { let type; const normalized = false; @@ -66,12 +70,19 @@ export class CachedMeshBufferData { } else { throw new Error("Mesh has an unsupported attribute format"); } + const vertexStateAttribute = this.#vertexStateBuffer.attributes[i]; + const shaderLocation = vertexStateAttribute.shaderLocation; + if (shaderLocation == null || shaderLocation == "auto" || shaderLocation < 0) { + throw new Error("Automatic shader locations are not supported in the webgl renderer."); + } attributes.push({ componentCount: attributeSettings.componentCount, type, normalized, offset: 0, // TODO + shaderLocation, }); + i++; } return { diff --git a/src/rendering/renderers/webGl/CachedMeshData.js b/src/rendering/renderers/webGl/CachedMeshData.js index b05325ba..6281435f 100644 --- a/src/rendering/renderers/webGl/CachedMeshData.js +++ b/src/rendering/renderers/webGl/CachedMeshData.js @@ -21,12 +21,19 @@ export class CachedMeshData { constructor(mesh, renderer) { this.#mesh = mesh; this.#renderer = renderer; + const vertexState = mesh.vertexState; + if (!vertexState) { + throw new Error("Assertion failed, mesh has no vertex state"); + } // todo: remove old bufferdata when the list of buffers changes this.#buffers = []; + let i = 0; for (const meshBuffer of mesh.getAttributeBuffers(false)) { - const bufferData = new CachedMeshBufferData(meshBuffer, this); + const vertexStateBuffer = vertexState.buffers[i]; + const bufferData = new CachedMeshBufferData(meshBuffer, vertexStateBuffer, this); this.#buffers.push(bufferData); + i++; } this.createIndexGpuBuffer(); diff --git a/src/rendering/renderers/webGl/CachedProgramData.js b/src/rendering/renderers/webGl/CachedProgramData.js index c5ca2dd6..65b4804e 100644 --- a/src/rendering/renderers/webGl/CachedProgramData.js +++ b/src/rendering/renderers/webGl/CachedProgramData.js @@ -7,8 +7,13 @@ * @property {WebGLUniformLocation?} mvpMatrix */ +import { parseAttributeLocations as parseTaggedAttributeLocations } from "./glslParsing.js"; + export class CachedProgramData { #program; + get program() { + return this.#program; + } /** @type {ViewUniformLocations?} */ #viewUniformLocations = null; @@ -18,10 +23,35 @@ export class CachedProgramData { /** @type {Map} */ #materialUniformLocations = new Map(); + /** @type {Map} */ + #taggedAttributeLocations = new Map(); + /** - * @param {WebGLProgram} program + * @param {WebGLRenderingContext} gl + * @param {import("../../ShaderSource.js").ShaderSource} vertexShaderSource + * @param {import("../../ShaderSource.js").ShaderSource} fragmentShaderSource + * @param {WebGLShader} vertexShader + * @param {WebGLShader} fragmentShader */ - constructor(program) { + constructor(gl, vertexShaderSource, fragmentShaderSource, vertexShader, fragmentShader) { + const program = gl.createProgram(); + if (!program) throw new Error("Failed to create program"); + + const taggedAttributeLocations = parseTaggedAttributeLocations(vertexShaderSource.source); + for (const {identifier, location} of taggedAttributeLocations) { + if (this.#taggedAttributeLocations.has(location)) { + throw new Error(`Shader contains multiple attributes tagged with @location(${location})`); + } + this.#taggedAttributeLocations.set(location, identifier); + } + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error(`Failed to link shader program: ${gl.getProgramInfoLog(program)}`); + } + this.#program = program; } @@ -62,4 +92,24 @@ export class CachedProgramData { this.#materialUniformLocations.set(name, location); return location; } + + /** + * @param {WebGLRenderingContext} gl + * @param {number} taggedShaderLocation The id that the attribute was tagged + * with in the shader using a `@location` comment. + */ + getAttribLocation(gl, taggedShaderLocation) { + const identifier = this.#taggedAttributeLocations.get(taggedShaderLocation); + if (!identifier) { + // If no identifier with this shader location was found in the vertex shader, this could either be because: + // - the user forgot to tag it with a @location comment + // - or because the attribute is not used at all. + // In the first case we should ideally throw an error, in the second case we should do nothing. + // However, there's no easy way for us to detect if an attribute is unused, so we'll just + // return -1, this will cause the renderer to not bind the attribute buffer. + return -1; + } + + return gl.getAttribLocation(this.#program, identifier); + } } diff --git a/src/rendering/renderers/webGl/WebGlRenderer.js b/src/rendering/renderers/webGl/WebGlRenderer.js index f81568f0..1a189028 100644 --- a/src/rendering/renderers/webGl/WebGlRenderer.js +++ b/src/rendering/renderers/webGl/WebGlRenderer.js @@ -10,6 +10,7 @@ import { CachedMeshData } from "./CachedMeshData.js"; import { MultiKeyWeakMap } from "../../../util/MultiKeyWeakMap.js"; import { Mesh } from "../../../core/Mesh.js"; import { CachedProgramData } from "./CachedProgramData.js"; +import { parseAttributeLocations } from "./glslParsing.js"; /** * @extends {Renderer} @@ -42,16 +43,13 @@ export class WebGlRenderer extends Renderer { /** @type {WeakMap} */ #cachedMaterialData = new WeakMap(); - /** @type {WeakMap} */ - #cachedProgramData = new WeakMap(); - /** @type {WeakMap} */ #cachedMeshDatas = new WeakMap(); /** @type {MultiKeyWeakMap<[number, import("../../ShaderSource.js").ShaderSource], WebGLShader>} */ #cachedShaders = new MultiKeyWeakMap([], { allowNonObjects: true }); - /** @type {MultiKeyWeakMap<[import("../../ShaderSource.js").ShaderSource, import("../../ShaderSource.js").ShaderSource], WebGLProgram>} */ + /** @type {MultiKeyWeakMap<[import("../../ShaderSource.js").ShaderSource, import("../../ShaderSource.js").ShaderSource], CachedProgramData>} */ #cachedPrograms = new MultiKeyWeakMap(); /** @type {OES_element_index_uint?} */ @@ -197,7 +195,7 @@ export class WebGlRenderer extends Renderer { /** * @typedef MaterialConfigRenderData - * @property {Map} materialRenderDatas + * @property {Map} materialRenderDatas */ // Group all meshes by material config @@ -212,7 +210,7 @@ export class WebGlRenderer extends Renderer { const materialConfig = materialData.getMaterialConfig(); if (!materialConfig || !materialConfig.vertexShader || !materialConfig.fragmentShader) continue; - const program = this.#getProgram(materialConfig.vertexShader, materialConfig.fragmentShader); + const program = this.#getCachedProgramData(materialConfig.vertexShader, materialConfig.fragmentShader); let programRenderData = materialConfigRenderDatas.get(materialConfig); if (!programRenderData) { @@ -247,9 +245,8 @@ export class WebGlRenderer extends Renderer { }); for (const [materialConfig, programRenderData] of sortedProgramRenderDatas) { - for (const [program, materialRenderData] of programRenderData.materialRenderDatas) { - gl.useProgram(program); - const programData = this.#getCachedProgramData(program); + for (const [programData, materialRenderData] of programRenderData.materialRenderDatas) { + gl.useProgram(programData.program); const viewUniformLocations = programData.getViewUniformLocations(gl); const modelUniformLocations = programData.getModelUniformLocations(gl); @@ -295,6 +292,7 @@ Material.setProperty("${mappedData.mappedName}", customData)`; for (const { component: meshComponent, worldMatrix } of meshRenderDatas) { const mesh = meshComponent.mesh; if (!mesh) continue; + if (!mesh.vertexState) continue; if (modelUniformLocations.mvpMatrix) { const mvpMatrix = Mat4.multiplyMatrices(worldMatrix, viewProjectionMatrix); @@ -317,19 +315,20 @@ Material.setProperty("${mappedData.mappedName}", customData)`; } gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBufferData.buffer); - let i = 0; for (const { buffer, attributes, stride } of meshData.getAttributeBufferData()) { gl.bindBuffer(gl.ARRAY_BUFFER, buffer); - for (const { componentCount, type, normalized, offset } of attributes) { - gl.vertexAttribPointer(i, componentCount, type, normalized, stride, offset); - gl.enableVertexAttribArray(i); - i++; + for (const { shaderLocation, componentCount, type, normalized, offset } of attributes) { + const index = programData.getAttribLocation(gl, shaderLocation); + if (index >= 0) { + gl.vertexAttribPointer(index, componentCount, type, normalized, stride, offset); + gl.enableVertexAttribArray(index); + } } } gl.drawElements(gl.TRIANGLES, indexBufferData.count, indexFormat, 0); } else { - // TODO + // TODO } } } @@ -354,18 +353,6 @@ Material.setProperty("${mappedData.mappedName}", customData)`; return data; } - /** - * @param {WebGLProgram} program - */ - #getCachedProgramData(program) { - let data = this.#cachedProgramData.get(program); - if (!data) { - data = new CachedProgramData(program); - this.#cachedProgramData.set(program, data); - } - return data; - } - /** * @param {import("../../../core/Mesh.js").Mesh} mesh */ @@ -405,7 +392,7 @@ Material.setProperty("${mappedData.mappedName}", customData)`; * @param {import("../../ShaderSource.js").ShaderSource} vertexShaderSource * @param {import("../../ShaderSource.js").ShaderSource} fragmentShaderSource */ - #getProgram(vertexShaderSource, fragmentShaderSource) { + #getCachedProgramData(vertexShaderSource, fragmentShaderSource) { const existing = this.#cachedPrograms.get([vertexShaderSource, fragmentShaderSource]); if (existing) return existing; @@ -415,18 +402,9 @@ Material.setProperty("${mappedData.mappedName}", customData)`; const vertexShader = this.#getShader(vertexShaderSource, gl.VERTEX_SHADER); const fragmentShader = this.#getShader(fragmentShaderSource, gl.FRAGMENT_SHADER); - const program = gl.createProgram(); - if (!program) throw new Error("Failed to create program"); - - gl.attachShader(program, vertexShader); - gl.attachShader(program, fragmentShader); - gl.linkProgram(program); - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - throw new Error(`Failed to link shader program: ${gl.getProgramInfoLog(program)}`); - } - - this.#cachedPrograms.set([vertexShaderSource, fragmentShaderSource], program); - return program; + const cachedProgramData = new CachedProgramData(gl, vertexShaderSource, fragmentShaderSource, vertexShader, fragmentShader); + this.#cachedPrograms.set([vertexShaderSource, fragmentShaderSource], cachedProgramData); + return cachedProgramData; } /** diff --git a/src/rendering/renderers/webGl/glslParsing.js b/src/rendering/renderers/webGl/glslParsing.js new file mode 100644 index 00000000..28a2be67 --- /dev/null +++ b/src/rendering/renderers/webGl/glslParsing.js @@ -0,0 +1,61 @@ +/** + * Regex string for matching glsl identifiers according to the glsl spec: + * https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#identifiers + * @param {string} group + */ +export const identifierRegex = "(?:[a-zA-Z_][0-9a-zA-Z_]*)"; + +/** + * @typedef ParsedAttributeLocation + * @property {string} identifier The name of the attribute as it appears in the shader. + * @property {number} location The shader location that the identifier was tagged with. + */ + +/** + * Finds all attributes in a shader and the value of the `@location` comment they are tagged with. + * @param {string} shaderSource + * @returns {ParsedAttributeLocation[]} + */ +export function parseAttributeLocations(shaderSource) { + // This loosely follows + // https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#shading-language-grammar:~:text=conditional_expression-,declaration%20%3A,-function_prototype%20SEMICOLON%0Ainit_declarator_list + let attributesRegex = ""; + // Capture the location tag + attributesRegex += "@location\\s*\\(\\s*(?\\d+)\\s*\\)"; + // Allow whitespace or any other tags after the line that contains the location tag + attributesRegex += ".*"; + // Only one new line allowed + attributesRegex += "\\n"; + // Allow whitespace before the attribute keyword + attributesRegex += "\\s*"; + // Attribute storage qualifier + attributesRegex += "attribute"; + // any additional `type_qualifier`s + attributesRegex += ".*"; + // at least one whitespace + attributesRegex += "\\s"; + // Capture the IDENTIFIER + attributesRegex += `(?${identifierRegex})`; + // whitespace + attributesRegex += "\\s*"; + // SEMICOLON + attributesRegex += ";" + + + /** @type {ParsedAttributeLocation[]} */ + const parsedLocations = []; + + for (const match of shaderSource.matchAll(new RegExp(attributesRegex, "g"))) { + if (!match.groups) continue; + const identifier = match.groups.identifier; + if (!identifier) continue; + const location = match.groups.location; + if (!location) continue; + parsedLocations.push({ + identifier, + location: parseInt(location, 10), + }); + } + + return parsedLocations; +} diff --git a/src/util/wgslParsing.js b/src/util/wgslParsing.js index 69d084c2..8a00a03c 100644 --- a/src/util/wgslParsing.js +++ b/src/util/wgslParsing.js @@ -200,8 +200,7 @@ export function parseBindings(shaderSource) { /** * @typedef ParsedVertexInputProperty * @property {string} identifier The name of the binding as it appears in the shader. - * @property {number} location The shader location that should be used when the vertex state - * has a shader location set to 'auto'. + * @property {number} location The shader location that the identifier was tagged with. */ /** diff --git a/test/unit/src/rendering/renderers/webGl/glslParsing/parseAttributeLocations.test.js b/test/unit/src/rendering/renderers/webGl/glslParsing/parseAttributeLocations.test.js new file mode 100644 index 00000000..3a540b18 --- /dev/null +++ b/test/unit/src/rendering/renderers/webGl/glslParsing/parseAttributeLocations.test.js @@ -0,0 +1,78 @@ +import { assertEquals } from "std/testing/asserts.ts" +import { parseAttributeLocations } from "../../../../../../../src/rendering/renderers/webGl/glslParsing.js" + +Deno.test({ + name: "two basic attributes", + fn() { + const code = ` + // @location(0) + attribute vec3 a_position; + // @location(1) + attribute vec3 a_color; + ` + const locations = parseAttributeLocations(code) + assertEquals(locations, [ + { + identifier: "a_position", + location: 0, + }, + { + identifier: "a_color", + location: 1, + } + ]) + } +}) + +Deno.test({ + name: "One location tag is missing", + fn() { + const code = ` + // @location(0) + attribute vec3 a_position; + attribute vec3 a_missing; + // @location(1) + attribute vec3 a_color; + ` + const locations = parseAttributeLocations(code) + assertEquals(locations, [ + { + identifier: "a_position", + location: 0, + }, + { + identifier: "a_color", + location: 1, + } + ]) + } +}) + +Deno.test({ + name: "Some edge cases", + fn() { + const code = ` + // @location(0) some extra comment and @another tag +attribute vec3 a_position; + // @location(1) + attribute highp vec3 a_color; + // @location ( 22 ) lots of spaces + attribute float a_brightness ; + ` + const locations = parseAttributeLocations(code) + assertEquals(locations, [ + { + identifier: "a_position", + location: 0, + }, + { + identifier: "a_color", + location: 1, + }, + { + identifier: "a_brightness", + location: 22, + } + ]) + } +})