// luma.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { VertexArray, getScratchArray, fillArray } from '@luma.gl/core'; import { GL } from '@luma.gl/constants'; import { getBrowser } from '@probe.gl/env'; import { getGLFromVertexType } from "../converters/vertex-formats.js"; // import {getGLFromVertexType} from '../converters/vertex-formats'; /** VertexArrayObject wrapper */ export class WEBGLVertexArray extends VertexArray { get [Symbol.toStringTag]() { return 'VertexArray'; } device; handle; /** Attribute 0 buffer constant */ buffer = null; bufferValue = null; /** * Attribute 0 can not be disable on most desktop OpenGL based browsers */ static isConstantAttributeZeroSupported(device) { return getBrowser() === 'Chrome'; } // Create a VertexArray constructor(device, props) { super(device, props); this.device = device; this.handle = this.device.gl.createVertexArray(); } destroy() { super.destroy(); if (this.buffer) { this.buffer?.destroy(); } if (this.handle) { this.device.gl.deleteVertexArray(this.handle); // @ts-expect-error read-only/undefined this.handle = undefined; } // Auto-delete elements? // return [this.elements]; } /** // Set (bind/unbind) an elements buffer, for indexed rendering. // Must be a Buffer bound to GL.ELEMENT_ARRAY_BUFFER or null. Constants not supported * * @param elementBuffer */ setIndexBuffer(indexBuffer) { const buffer = indexBuffer; // Explicitly allow `null` to support clearing the index buffer if (buffer && buffer.glTarget !== 34963) { throw new Error('Use .setBuffer()'); } // In WebGL The GL.ELEMENT_ARRAY_BUFFER_BINDING is stored on the VertexArrayObject this.device.gl.bindVertexArray(this.handle); this.device.gl.bindBuffer(34963, buffer ? buffer.handle : null); this.indexBuffer = buffer; // Unbind to prevent unintended changes to the VAO. this.device.gl.bindVertexArray(null); } /** Set a location in vertex attributes array to a buffer, enables the location, sets divisor */ setBuffer(location, attributeBuffer) { const buffer = attributeBuffer; // Sanity check target if (buffer.glTarget === 34963) { throw new Error('Use .setIndexBuffer()'); } const { size, type, stride, offset, normalized, integer, divisor } = this._getAccessor(location); this.device.gl.bindVertexArray(this.handle); // A non-zero buffer object must be bound to the GL_ARRAY_BUFFER target this.device.gl.bindBuffer(34962, buffer.handle); // WebGL2 supports *integer* data formats, i.e. GPU will see integer values if (integer) { this.device.gl.vertexAttribIPointer(location, size, type, stride, offset); } else { // Attaches ARRAY_BUFFER with specified buffer format to location this.device.gl.vertexAttribPointer(location, size, type, normalized, stride, offset); } // Clear binding - keeping it may cause [.WebGL-0x12804417100] // GL_INVALID_OPERATION: A transform feedback buffer that would be written to is also bound to a non-transform-feedback target this.device.gl.bindBuffer(34962, null); // Mark as non-constant this.device.gl.enableVertexAttribArray(location); // Set the step mode 0=vertex, 1=instance this.device.gl.vertexAttribDivisor(location, divisor || 0); this.attributes[location] = buffer; // Unbind to prevent unintended changes to the VAO. this.device.gl.bindVertexArray(null); } /** Set a location in vertex attributes array to a constant value, disables the location */ setConstantWebGL(location, value) { this._enable(location, false); this.attributes[location] = value; } bindBeforeRender() { this.device.gl.bindVertexArray(this.handle); this._applyConstantAttributes(); } unbindAfterRender() { // Unbind to prevent unintended changes to the VAO. this.device.gl.bindVertexArray(null); } // Internal methods /** * Constant attributes need to be reset before every draw call * Any attribute that is disabled in the current vertex array object * is read from the context's global constant value for that attribute location. * @note Constant attributes are only supported in WebGL, not in WebGPU */ _applyConstantAttributes() { for (let location = 0; location < this.maxVertexAttributes; ++location) { const constant = this.attributes[location]; // A typed array means this is a constant if (ArrayBuffer.isView(constant)) { this.device.setConstantAttributeWebGL(location, constant); } } } /** * Set a location in vertex attributes array to a buffer, enables the location, sets divisor * @note requires vertex array to be bound */ // protected _setAttributeLayout(location: number): void { // const {size, type, stride, offset, normalized, integer, divisor} = this._getAccessor(location); // // WebGL2 supports *integer* data formats, i.e. GPU will see integer values // if (integer) { // this.device.gl.vertexAttribIPointer(location, size, type, stride, offset); // } else { // // Attaches ARRAY_BUFFER with specified buffer format to location // this.device.gl.vertexAttribPointer(location, size, type, normalized, stride, offset); // } // this.device.gl.vertexAttribDivisor(location, divisor || 0); // } /** Get an accessor from the */ _getAccessor(location) { const attributeInfo = this.attributeInfos[location]; if (!attributeInfo) { throw new Error(`Unknown attribute location ${location}`); } const glType = getGLFromVertexType(attributeInfo.bufferDataType); return { size: attributeInfo.bufferComponents, type: glType, stride: attributeInfo.byteStride, offset: attributeInfo.byteOffset, normalized: attributeInfo.normalized, // it is the shader attribute declaration, not the vertex memory format, // that determines if the data in the buffer will be treated as integers. // // Also note that WebGL supports assigning non-normalized integer data to floating point attributes, // but as far as we can tell, WebGPU does not. integer: attributeInfo.integer, divisor: attributeInfo.stepMode === 'instance' ? 1 : 0 }; } /** * Enabling an attribute location makes it reference the currently bound buffer * Disabling an attribute location makes it reference the global constant value * TODO - handle single values for size 1 attributes? * TODO - convert classic arrays based on known type? */ _enable(location, enable = true) { // Attribute 0 cannot be disabled in most desktop OpenGL based browsers... const canDisableAttributeZero = WEBGLVertexArray.isConstantAttributeZeroSupported(this.device); const canDisableAttribute = canDisableAttributeZero || location !== 0; if (enable || canDisableAttribute) { location = Number(location); this.device.gl.bindVertexArray(this.handle); if (enable) { this.device.gl.enableVertexAttribArray(location); } else { this.device.gl.disableVertexAttribArray(location); } this.device.gl.bindVertexArray(null); } } /** * Provide a means to create a buffer that is equivalent to a constant. * NOTE: Desktop OpenGL cannot disable attribute 0. * https://stackoverflow.com/questions/20305231/webgl-warning-attribute-0-is-disabled- * this-has-significant-performance-penalty */ getConstantBuffer(elementCount, value) { // Create buffer only when needed, and reuse it (avoids inflating buffer creation statistics) const constantValue = normalizeConstantArrayValue(value); const byteLength = constantValue.byteLength * elementCount; const length = constantValue.length * elementCount; if (this.buffer && byteLength !== this.buffer.byteLength) { throw new Error(`Buffer size is immutable, byte length ${byteLength} !== ${this.buffer.byteLength}.`); } let updateNeeded = !this.buffer; this.buffer = this.buffer || this.device.createBuffer({ byteLength }); // Reallocate and update contents if needed updateNeeded = updateNeeded || !compareConstantArrayValues(constantValue, this.bufferValue); if (updateNeeded) { // Create a typed array that is big enough, and fill it with the required data const typedArray = getScratchArray(value.constructor, length); fillArray({ target: typedArray, source: constantValue, start: 0, count: length }); this.buffer.write(typedArray); this.bufferValue = value; } return this.buffer; } } // HELPER FUNCTIONS /** * TODO - convert Arrays based on known type? (read type from accessor, don't assume Float32Array) * TODO - handle single values for size 1 attributes? */ function normalizeConstantArrayValue(arrayValue) { if (Array.isArray(arrayValue)) { return new Float32Array(arrayValue); } return arrayValue; } /** * */ function compareConstantArrayValues(v1, v2) { if (!v1 || !v2 || v1.length !== v2.length || v1.constructor !== v2.constructor) { return false; } for (let i = 0; i < v1.length; ++i) { if (v1[i] !== v2[i]) { return false; } } return true; }