UNPKG

gpu-curtains

Version:

gpu-curtains is a 3D WebGPU rendering engine. It can be used as a standalone 3D engine, but also includes extra classes focused on mapping 3d objects to DOM elements; It allows users to synchronize values such as position, sizing, or scale between them.

424 lines (423 loc) 17.5 kB
import { generateUUID, throwError } from "../../utils/utils.mjs"; import { Vec3 } from "../../math/Vec3.mjs"; import { Buffer } from "../buffers/Buffer.mjs"; import { Box3 } from "../../math/Box3.mjs"; import { getVertexBufferAttributeLayout, vertexBufferViewSetFunction } from "./utils.mjs"; //#region src/core/geometries/Geometry.ts /** * Used to create a {@link Geometry} from given parameters like instances count or geometry attributes (vertices, uvs, normals).<br> * Holds all attributes arrays, bounding box and create as WGSL code snippet for the vertex shader input attributes. * * During the {@link Geometry#render | render}, the {@link Geometry} is responsible for setting the {@link Geometry#vertexBuffers | vertexBuffers} and drawing the vertices. * * @example * ```javascript * const vertices = new Float32Array([ * // first triangle * 1, 1, 0, * 1, -1, 0, * -1, -1, 0, * * // second triangle * 1, 1, 0, * -1, -1, 0, * -1, 1, 0 * ]) * * // create a quad geometry made of 2 triangles * const geometry = new Geometry() * * geometry.setAttribute({ * name: 'position', * type: 'vec3f', * bufferFormat: 'float32x3', * size: 3, * bufferLength: vertices.length, * array: vertices, * }) * ``` */ var Geometry = class Geometry { /** * Geometry constructor * @param parameters - {@link GeometryParams | parameters} used to create our Geometry. */ constructor({ verticesOrder = "ccw", topology = "triangle-list", instancesCount = 1, vertexBuffers = [], mapBuffersAtCreation = true } = {}) { this.verticesCount = 0; this.verticesOrder = verticesOrder; this.topology = topology; this.instancesCount = instancesCount; this.ready = false; this.boundingBox = new Box3(); this.type = "Geometry"; this.uuid = generateUUID(); this.indirectDraw = null; this.vertexBuffers = []; this.consumers = /* @__PURE__ */ new Set(); this.options = { verticesOrder, topology, instancesCount, vertexBuffers, mapBuffersAtCreation }; const attributesBuffer = vertexBuffers.find((vertexBuffer) => vertexBuffer.name === "attributes"); if (!vertexBuffers.length || !attributesBuffer) this.addVertexBuffer({ name: "attributes" }); else if (attributesBuffer) vertexBuffers.sort((a, b) => { return (a.name !== "attributes" ? Infinity : -1) - (b.name !== "attributes" ? Infinity : -1); }); for (const vertexBuffer of vertexBuffers) this.addVertexBuffer({ stepMode: vertexBuffer.stepMode ?? "vertex", name: vertexBuffer.name, attributes: vertexBuffer.attributes, ...vertexBuffer.array && { array: vertexBuffer.array }, ...vertexBuffer.arrayBuffer && { arrayBuffer: vertexBuffer.arrayBuffer }, ...vertexBuffer.buffer && { buffer: vertexBuffer.buffer }, ...vertexBuffer.bufferOffset && { bufferOffset: vertexBuffer.bufferOffset }, ...vertexBuffer.bufferSize && { bufferSize: vertexBuffer.bufferSize } }); if (attributesBuffer) this.setWGSLFragment(); } /** * Helper to decode and normalize integer values to float values based on the data type. * @param typedArrayConstructor - {@link TypedArrayConstructor} used to know the data type. * @returns - Decoded and normalized value. */ static dequantize(typedArrayConstructor) { switch (typedArrayConstructor) { case Int8Array: return (v) => Math.max(v / 127, -1); case Uint8Array: return (v) => v / 255; case Int16Array: return (v) => Math.max(v / 32767, -1); case Uint16Array: return (v) => v / 65535; default: return (v) => v; } } /** * Reset all the {@link vertexBuffers | vertex buffers} when the device is lost. */ loseContext() { this.ready = false; for (const vertexBuffer of this.vertexBuffers) vertexBuffer.buffer.destroy(); } /** * Restore the {@link Geometry} buffers on context restoration. * @param renderer - The {@link Renderer} used to recreate the buffers. */ restoreContext(renderer) { if (this.ready) return; for (const vertexBuffer of this.vertexBuffers) { if (!vertexBuffer.buffer.GPUBuffer && vertexBuffer.buffer.consumers.size === 0) { vertexBuffer.buffer.createBuffer(renderer); this.uploadBuffer(renderer, vertexBuffer); } vertexBuffer.buffer.consumers.add(this.uuid); } this.ready = true; } /** * Add a vertex buffer to our Geometry, set its attributes and return it. * @param parameters - Vertex buffer {@link VertexBufferParams | parameters}. * @returns - Newly created {@link VertexBuffer | vertex buffer}. */ addVertexBuffer({ stepMode = "vertex", name, attributes = [], buffer = null, array = null, arrayBuffer = null, bufferOffset = 0, bufferSize = null } = {}) { buffer = buffer || new Buffer(); const vertexBuffer = { name: name ?? "attributes" + this.vertexBuffers.length, stepMode, arrayStride: 0, bufferLength: 0, attributes: [], array, arrayBuffer, buffer, bufferOffset, bufferSize }; attributes?.forEach((attribute) => { this.setAttribute({ vertexBuffer, ...attribute }); }); this.vertexBuffers.push(vertexBuffer); return vertexBuffer; } /** * Get a vertex buffer by name. * @param name - Our vertex buffer name. * @returns - Found {@link VertexBuffer | vertex buffer} or null if not found. */ getVertexBufferByName(name = "") { return this.vertexBuffers.find((vertexBuffer) => vertexBuffer.name === name); } /** * Set a vertex buffer attribute. * @param parameters - Attributes {@link VertexBufferAttributeParams | parameters}. */ setAttribute({ vertexBuffer = this.vertexBuffers[0], name, type, bufferFormat, size = 3, array = new Float32Array(this.verticesCount * size), normalized = false, verticesStride = 1 }) { const attributes = vertexBuffer.attributes; const attributesLength = attributes.length; if (!name) name = "geometryAttribute" + attributesLength; const attributeLayout = getVertexBufferAttributeLayout({ size, array, normalized }); bufferFormat = bufferFormat ?? attributeLayout.format; type = type ?? attributeLayout.type; let arrayLength = array.length; const attributeCount = arrayLength / size; if (name === "position") this.verticesCount = attributeCount; if (vertexBuffer.stepMode === "vertex" && this.verticesCount && this.verticesCount !== attributeCount * verticesStride) throwError(`Geometry vertex attribute error. Attribute ${name} array of size ${size} must be of length: ${this.verticesCount * size}, current given: ${array.length}. (${this.verticesCount} vertices).`); else if (vertexBuffer.stepMode === "instance" && attributeCount !== this.instancesCount) if (vertexBuffer.buffer) arrayLength = this.instancesCount * size; else throwError(`Geometry instance attribute error. Attribute array of size ${size} must be of length: ${this.instancesCount * size}, current given: ${array.length}. (${this.instancesCount} instances).`); const bufferOffset = attributesLength ? attributes[attributesLength - 1].bufferOffset + attributes[attributesLength - 1].size * attributes[attributesLength - 1].array.BYTES_PER_ELEMENT : 0; const attribute = { name, type, bufferFormat, size, bufferOffset, array, verticesStride, normalized }; vertexBuffer.bufferLength += arrayLength * array.BYTES_PER_ELEMENT * verticesStride; vertexBuffer.arrayStride += attribute.size * array.BYTES_PER_ELEMENT; vertexBuffer.attributes.push(attribute); } /** * Get whether this Geometry is ready to compute, i.e. if its first vertex buffer array has not been created yet. * @readonly */ get shouldCompute() { return this.vertexBuffers.length && !this.vertexBuffers[0].arrayBuffer; } /** * Get an attribute by name. * @param name - Name of the attribute to find. * @returns - Found {@link VertexBufferAttribute | attribute} or null if not found. */ getAttributeByName(name) { let attribute; for (const vertexBuffer of this.vertexBuffers) { attribute = vertexBuffer.attributes.find((attribute) => attribute.name === name); if (attribute) break; } return attribute; } /** * Compute the normal {@link Vec3} from a triangle defined by three {@link Vec3} by computing edges {@link Vec3}. * @param vertex1 - First triangle position. * @param vertex2 - Second triangle position. * @param vertex3 - Third triangle position. * @param edge1 - First edge. * @param edge2 - Second edge. * @param normal - Flat normal generated. */ computeNormalFromTriangle(vertex1, vertex2, vertex3, edge1, edge2, normal) { edge1.copy(vertex2).sub(vertex1); edge2.copy(vertex3).sub(vertex1); normal.crossVectors(edge1, edge2).normalize(); } /** * Compute {@link Geometry} flat normals in case the `normal` attribute is missing. */ computeFlatNormals() { const positionAttribute = this.getAttributeByName("position"); const vertex1 = new Vec3(); const vertex2 = new Vec3(); const vertex3 = new Vec3(); const edge1 = new Vec3(); const edge2 = new Vec3(); const normal = new Vec3(); const posLength = positionAttribute.array.length; const normalArray = new Float32Array(posLength); for (let i = 0; i < posLength; i += positionAttribute.size * 3) { vertex1.set(positionAttribute.array[i], positionAttribute.array[i + 1], positionAttribute.array[i + 2]); vertex2.set(positionAttribute.array[i + 3], positionAttribute.array[i + 4], positionAttribute.array[i + 5]); vertex3.set(positionAttribute.array[i + 6], positionAttribute.array[i + 7], positionAttribute.array[i + 8]); this.computeNormalFromTriangle(vertex1, vertex2, vertex3, edge1, edge2, normal); for (let j = 0; j < 3; j++) { normalArray[i + j * 3] = normal.x; normalArray[i + 1 + j * 3] = normal.y; normalArray[i + 2 + j * 3] = normal.z; } } this.setAttribute({ name: "normal", type: "vec3f", bufferFormat: "float32x3", size: 3, array: normalArray }); } /** * Compute a Geometry, which means iterate through all vertex buffers and create the attributes array that will be sent as buffers. * Also compute the Geometry bounding box. */ computeGeometry() { if (this.ready) return; this.vertexBuffers.forEach((vertexBuffer, index) => { const hasPositionAttribute = vertexBuffer.attributes.find((attribute) => attribute.name === "position"); if (index === 0) { if (!hasPositionAttribute) throwError(`Geometry must have a 'position' attribute`); if (!vertexBuffer.attributes.find((attribute) => attribute.name === "normal")) { this.computeFlatNormals(); this.setWGSLFragment(); } } if (vertexBuffer.array && vertexBuffer.array.byteLength === vertexBuffer.bufferLength) { vertexBuffer.arrayBuffer = new ArrayBuffer(vertexBuffer.bufferLength); new Uint8Array(vertexBuffer.arrayBuffer).set(new Uint8Array(vertexBuffer.array.buffer, vertexBuffer.array.byteOffset, vertexBuffer.array.byteLength)); } else if (!vertexBuffer.arrayBuffer || vertexBuffer.arrayBuffer.byteLength !== vertexBuffer.bufferLength) { vertexBuffer.arrayBuffer = new ArrayBuffer(vertexBuffer.bufferLength); const arrayView = new DataView(vertexBuffer.arrayBuffer); for (let a = 0; a < vertexBuffer.attributes.length; a++) { const { name, array, size, bufferOffset, verticesStride } = vertexBuffer.attributes[a]; const setFunction = vertexBufferViewSetFunction(arrayView, array); const arrayLength = array.length; for (let i = 0; i < arrayLength; i += size) for (let s = 0; s < size; s++) { const attrValue = array[i + s]; if (name === "position") { if (s % 3 === 0) { this.boundingBox.min.x = Math.min(this.boundingBox.min.x, attrValue); this.boundingBox.max.x = Math.max(this.boundingBox.max.x, attrValue); } else if (s % 3 === 1) { this.boundingBox.min.y = Math.min(this.boundingBox.min.y, attrValue); this.boundingBox.max.y = Math.max(this.boundingBox.max.y, attrValue); } else if (s % 3 === 2) { this.boundingBox.min.z = Math.min(this.boundingBox.min.z, attrValue); this.boundingBox.max.z = Math.max(this.boundingBox.max.z, attrValue); } } const attrIndex = i / size; for (let vs = 0; vs < verticesStride; vs++) { const attrOffset = s * array.BYTES_PER_ELEMENT; const attrStrideOffset = attrIndex * vertexBuffer.arrayStride; setFunction((vs + (verticesStride - 1) * attrIndex) * vertexBuffer.arrayStride + attrStrideOffset + bufferOffset + attrOffset, attrValue, true); } } } if (hasPositionAttribute && hasPositionAttribute.array && hasPositionAttribute.normalized) { const dequantizePositions = Geometry.dequantize(hasPositionAttribute.array.constructor); this.boundingBox.min.x = dequantizePositions(this.boundingBox.min.x); this.boundingBox.min.y = dequantizePositions(this.boundingBox.min.y); this.boundingBox.min.z = dequantizePositions(this.boundingBox.min.z); this.boundingBox.max.x = dequantizePositions(this.boundingBox.max.x); this.boundingBox.max.y = dequantizePositions(this.boundingBox.max.y); this.boundingBox.max.z = dequantizePositions(this.boundingBox.max.z); } } }); if (!this.wgslStructFragment) this.setWGSLFragment(); } /** * Set the {@link layoutCacheKey} and WGSL code snippet that will be appended to the vertex shader. */ setWGSLFragment() { let locationIndex = -1; this.wgslStructFragment = `struct Attributes {\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,${this.vertexBuffers.map((vertexBuffer) => { return vertexBuffer.attributes.map((attribute) => { locationIndex++; return `\n @location(${locationIndex}) ${attribute.name}: ${attribute.type}`; }); }).join(",")}\n};`; this.layoutCacheKey = this.vertexBuffers.map((vertexBuffer) => { return vertexBuffer.name + "," + vertexBuffer.attributes.map((attribute) => { return `${attribute.name},${attribute.size}`; }); }).join(",") + ","; } /** * Create the {@link Geometry} {@link vertexBuffers | vertex buffers}. * @param parameters - Parameters used to create the vertex buffers. * @param parameters.renderer - {@link Renderer} used to create the vertex buffers. * @param parameters.label - Label to use for the vertex buffers. */ createBuffers({ renderer, label = this.type }) { if (this.ready) return; for (const vertexBuffer of this.vertexBuffers) { if (!vertexBuffer.bufferSize) vertexBuffer.bufferSize = vertexBuffer.arrayBuffer.byteLength; if (!vertexBuffer.buffer.GPUBuffer && !vertexBuffer.buffer.consumers.size) { vertexBuffer.buffer.createBuffer(renderer, { label: label + ": " + vertexBuffer.name + " buffer", size: vertexBuffer.bufferSize, usage: this.options.mapBuffersAtCreation ? ["vertex"] : ["copyDst", "vertex"], mappedAtCreation: this.options.mapBuffersAtCreation }); this.uploadBuffer(renderer, vertexBuffer); } vertexBuffer.buffer.consumers.add(this.uuid); } this.ready = true; } /** * Upload a {@link GeometryBuffer} to the GPU. * @param renderer - {@link Renderer} used to upload the buffer. * @param buffer - {@link GeometryBuffer} holding a {@link Buffer} and a typed array to upload. */ uploadBuffer(renderer, buffer) { if (this.options.mapBuffersAtCreation) { if (buffer.arrayBuffer) { const src = new Uint8Array(buffer.arrayBuffer); const mappedRange = buffer.buffer.GPUBuffer.getMappedRange(); new Uint8Array(mappedRange).set(src); } else if (buffer.array) new buffer.array.constructor(buffer.buffer.GPUBuffer.getMappedRange()).set(buffer.array); buffer.buffer.GPUBuffer.unmap(); } else renderer.queueWriteBuffer(buffer.buffer.GPUBuffer, 0, buffer.arrayBuffer ?? buffer.array); } /** * Set the {@link indirectDraw} parameters to draw this {@link Geometry} with an {@link extras/buffers/IndirectBuffer.IndirectBuffer | IndirectBuffer}. * @param parameters - {@link IndirectDrawParams | indirect draw parameters} to use for this {@link Geometry}. */ useIndirectBuffer({ buffer, offset = 0 }) { this.indirectDraw = { buffer, offset }; } /** RENDER **/ /** * Set our render pass geometry vertex buffers. * @param pass - Current render pass. */ setGeometryBuffers(pass) { this.vertexBuffers.forEach((vertexBuffer, index) => { pass.setVertexBuffer(index, vertexBuffer.buffer.GPUBuffer, vertexBuffer.bufferOffset, vertexBuffer.bufferSize); }); } /** * Draw our geometry. Can use indirect drawing if {@link indirectDraw} is set up. * @param pass - current render pass. */ drawGeometry(pass) { if (this.indirectDraw && this.indirectDraw.buffer && this.indirectDraw.buffer.GPUBuffer) pass.drawIndirect(this.indirectDraw.buffer.GPUBuffer, this.indirectDraw.offset); else pass.draw(this.verticesCount, this.instancesCount); } /** * Set our vertex buffers then draw the geometry. * @param pass - current render pass. */ render(pass) { if (!this.ready) return; this.setGeometryBuffers(pass); this.drawGeometry(pass); } /** * Destroy our geometry vertex buffers. * @param renderer - current {@link Renderer}, in case we want to remove the {@link VertexBuffer#buffer | buffers} from the cache. */ destroy(renderer = null) { this.ready = false; for (const vertexBuffer of this.vertexBuffers) { vertexBuffer.buffer.consumers.delete(this.uuid); if (!vertexBuffer.buffer.consumers.size) vertexBuffer.buffer.destroy(); vertexBuffer.array = null; vertexBuffer.arrayBuffer = null; if (renderer) renderer.removeBuffer(vertexBuffer.buffer); } } }; //#endregion export { Geometry };