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.

622 lines (619 loc) 26.1 kB
import { Binding } from './Binding.mjs'; import { getBindGroupLayoutBindingType, getBufferLayout, getBindingWGSLVarType } from './utils.mjs'; import { throwWarning, toCamelCase, toKebabCase } from '../../utils/utils.mjs'; import { Vec2 } from '../../math/Vec2.mjs'; import { Vec3 } from '../../math/Vec3.mjs'; import { BufferElement, bytesPerRow } from './bufferElements/BufferElement.mjs'; import { BufferArrayElement } from './bufferElements/BufferArrayElement.mjs'; import { BufferInterleavedArrayElement } from './bufferElements/BufferInterleavedArrayElement.mjs'; import { Buffer } from '../buffers/Buffer.mjs'; var __typeError = (msg) => { throw TypeError(msg); }; var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value); var _parent; const _BufferBinding = class _BufferBinding extends Binding { /** * BufferBinding constructor * @param parameters - {@link BufferBindingParams | parameters} used to create our BufferBindings */ constructor({ label = "Uniform", name = "uniform", bindingType, visibility, useStruct = true, access = "read", usage = [], struct = {}, childrenBindings = [], buffer = null, parent = null, minOffset = 256, offset = 0 }) { bindingType = bindingType ?? "uniform"; super({ label, name, bindingType, visibility }); /** @ignore */ __privateAdd(this, _parent); this.options = { ...this.options, useStruct, access, usage, struct, childrenBindings, buffer, parent, minOffset, offset }; this.cacheKey += `${useStruct},${access},`; this.arrayBufferSize = 0; this.shouldUpdate = false; this.useStruct = useStruct; this.bufferElements = []; this.inputs = {}; this.buffer = this.options.buffer ?? new Buffer(); if (Object.keys(struct).length) { this.setBindings(struct); this.setInputsAlignment(); } this.setChildrenBindings(childrenBindings); if (Object.keys(struct).length || this.childrenBindings.length) { this.setBufferAttributes(); this.setWGSLFragment(); } this.parent = parent; } /** * Clone a {@link BufferBindingParams#struct | struct object} width new default values. * @param struct - New cloned struct object. */ static cloneStruct(struct) { return Object.keys(struct).reduce((acc, bindingKey) => { const binding = struct[bindingKey]; let value; if (Array.isArray(binding.value) || ArrayBuffer.isView(binding.value)) { value = new binding.value.constructor(binding.value.length); } else if (typeof binding.value === "number") { value = 0; } else { value = new binding.value.constructor(); } return { ...acc, [bindingKey]: { type: binding.type, value } }; }, {}); } /** * Get the {@link BufferBinding} parent if any. * @readonly * @returns - The {@link BufferBinding} parent if any. */ get parent() { return __privateGet(this, _parent); } /** * Set the new {@link BufferBinding} parent. * @param value - New {@link BufferBinding} parent to set if any. */ set parent(value) { if (!!value) { this.parentView = new DataView(value.arrayBuffer, this.offset, this.getMinOffsetSize(this.arrayBufferSize)); const getAllBufferElements = (binding) => { const getBufferElements = (binding2) => { return binding2.bufferElements; }; return [ ...getBufferElements(binding), binding.childrenBindings.map((child) => getAllBufferElements(child)).flat() ].flat(); }; const bufferElements = getAllBufferElements(this); this.parentViewSetBufferEls = bufferElements.map((bufferElement) => { switch (bufferElement.bufferLayout.View) { case Int32Array: return { bufferElement, viewSetFunction: this.parentView.setInt32.bind(this.parentView) }; case Uint16Array: return { bufferElement, viewSetFunction: this.parentView.setUint16.bind(this.parentView) }; case Uint32Array: return { bufferElement, viewSetFunction: this.parentView.setUint32.bind(this.parentView) }; case Float32Array: default: return { bufferElement, viewSetFunction: this.parentView.setFloat32.bind(this.parentView) }; } }); if (!this.parent && this.buffer.GPUBuffer && !this.options.buffer) { this.buffer.destroy(); } } else { this.parentView = null; this.parentViewSetBufferEls = null; } __privateSet(this, _parent, value); } /** * Round the given size value to the nearest minimum {@link GPUDevice} buffer offset alignment. * @param value - Size to round. */ getMinOffsetSize(value) { return Math.ceil(value / this.options.minOffset) * this.options.minOffset; } /** * Get this {@link BufferBinding} offset in bytes inside the {@link arrayBuffer | parent arrayBuffer}. * @readonly * @returns - The offset in bytes inside the {@link arrayBuffer | parent arrayBuffer} */ get offset() { return this.getMinOffsetSize(this.options.offset * this.getMinOffsetSize(this.arrayBufferSize)); } /** * Get {@link GPUDevice.createBindGroupLayout().descriptor.entries.resource | GPUBindGroupLayout entry resource}. * @readonly */ get resourceLayout() { return { buffer: { type: getBindGroupLayoutBindingType(this) }, ...this.parent && { offset: this.offset, size: this.arrayBufferSize } }; } /** * Get the resource cache key * @readonly */ get resourceLayoutCacheKey() { return `buffer,${getBindGroupLayoutBindingType(this)},${this.visibility},`; } /** * Get {@link GPUDevice.createBindGroup().descriptor.entries.resource | GPUBindGroup entry resource}. * @readonly */ get resource() { return { buffer: this.parent ? this.parent.buffer.GPUBuffer : this.buffer.GPUBuffer, ...this.parent && { offset: this.offset, size: this.arrayBufferSize } }; } /** * Clone this {@link BufferBinding} into a new one. Allows to skip buffer layout alignment computations. * @param params - params to use for cloning */ clone(params = {}) { let { struct, childrenBindings, parent, ...defaultParams } = params; const { label, name, bindingType, visibility, useStruct, access, usage } = this.options; defaultParams = { ...{ label, name, bindingType, visibility, useStruct, access, usage }, ...defaultParams }; const bufferBindingCopy = new this.constructor(defaultParams); struct = struct || _BufferBinding.cloneStruct(this.options.struct); bufferBindingCopy.options.struct = struct; bufferBindingCopy.setBindings(struct); bufferBindingCopy.arrayBufferSize = this.arrayBufferSize; bufferBindingCopy.arrayBuffer = new ArrayBuffer(bufferBindingCopy.arrayBufferSize); bufferBindingCopy.arrayView = new DataView( bufferBindingCopy.arrayBuffer, 0, bufferBindingCopy.arrayBuffer.byteLength ); if (!bufferBindingCopy.options.buffer) { bufferBindingCopy.buffer.size = bufferBindingCopy.arrayBuffer.byteLength; } this.bufferElements.forEach((bufferElement) => { const newBufferElement = new bufferElement.constructor({ name: bufferElement.name, key: bufferElement.key, type: bufferElement.type, ...bufferElement.arrayLength && { arrayLength: bufferElement.arrayLength } }); newBufferElement.alignment = JSON.parse(JSON.stringify(bufferElement.alignment)); if (bufferElement.arrayStride) { newBufferElement.arrayStride = bufferElement.arrayStride; } newBufferElement.setView(bufferBindingCopy.arrayBuffer, bufferBindingCopy.arrayView); bufferBindingCopy.bufferElements.push(newBufferElement); }); if (this.options.childrenBindings) { bufferBindingCopy.options.childrenBindings = this.options.childrenBindings; bufferBindingCopy.options.childrenBindings.forEach((child) => { const count = child.count ? Math.max(1, child.count) : 1; bufferBindingCopy.cacheKey += `child(count:${count}):${child.binding.cacheKey}`; }); bufferBindingCopy.options.childrenBindings.forEach((child) => { bufferBindingCopy.childrenBindings = [ ...bufferBindingCopy.childrenBindings, Array.from(Array(Math.max(1, child.count || 1)).keys()).map((i) => { return child.binding.clone({ ...child.binding.options, // clone struct with new arrays struct: _BufferBinding.cloneStruct(child.binding.options.struct) }); }) ].flat(); }); bufferBindingCopy.childrenBindings.forEach((binding, index) => { let offset = this.arrayView.byteLength; for (let i = 0; i < index; i++) { offset += this.childrenBindings[i].arrayBuffer.byteLength; } binding.bufferElements.forEach((bufferElement, i) => { bufferElement.alignment.start.row = this.childrenBindings[index].bufferElements[i].alignment.start.row; bufferElement.alignment.end.row = this.childrenBindings[index].bufferElements[i].alignment.end.row; }); binding.arrayView = new DataView(bufferBindingCopy.arrayBuffer, offset, binding.arrayBuffer.byteLength); for (const bufferElement of binding.bufferElements) { bufferElement.setView(bufferBindingCopy.arrayBuffer, binding.arrayView); } }); } bufferBindingCopy.setWGSLFragment(); if (parent) { bufferBindingCopy.parent = parent; } bufferBindingCopy.shouldUpdate = bufferBindingCopy.arrayBufferSize > 0; return bufferBindingCopy; } /** * Format bindings struct and set our {@link inputs} * @param bindings - bindings inputs */ setBindings(bindings) { for (const bindingKey of Object.keys(bindings)) { const binding = {}; for (const key in bindings[bindingKey]) { if (key !== "value") { binding[key] = bindings[bindingKey][key]; } } binding.name = bindingKey; Object.defineProperty(binding, "value", { get() { return binding._value; }, set(v) { binding._value = v; binding.shouldUpdate = true; } }); binding.value = bindings[bindingKey].value; if (binding.value instanceof Vec2 || binding.value instanceof Vec3) { const _onChangeCallback = binding.value._onChangeCallback; binding.value._onChangeCallback = () => { if (_onChangeCallback) { _onChangeCallback(); } binding.shouldUpdate = true; }; } this.inputs[bindingKey] = binding; this.cacheKey += `${bindingKey},${bindings[bindingKey].type},`; } } /** * Set this {@link BufferBinding} optional {@link BufferBinding.childrenBindings | childrenBindings}. * @param childrenBindings - Array of {@link BufferBindingChildrenBinding} to use as {@link BufferBinding.childrenBindings | childrenBindings}. */ setChildrenBindings(childrenBindings) { this.childrenBindings = []; if (childrenBindings && childrenBindings.length) { const childrenArray = []; childrenBindings.sort((a, b) => { const countA = a.count ? Math.max(a.count) : a.forceArray ? 1 : 0; const countB = b.count ? Math.max(b.count) : b.forceArray ? 1 : 0; return countA - countB; }).forEach((child) => { if (child.count && child.count > 1 || child.forceArray) { childrenArray.push(child.binding); } }); if (childrenArray.length > 1) { childrenArray.shift(); throwWarning( `BufferBinding: "${this.label}" contains multiple children bindings arrays. These children bindings cannot be added to the BufferBinding: "${childrenArray.map((child) => child.label).join(", ")}"` ); childrenArray.forEach((removedChildBinding) => { childrenBindings = childrenBindings.filter((child) => child.binding.name !== removedChildBinding.name); }); } this.options.childrenBindings = childrenBindings; childrenBindings.forEach((child) => { const count = child.count ? Math.max(1, child.count) : 1; this.cacheKey += `child(count:${count}):${child.binding.cacheKey}`; this.childrenBindings = [ ...this.childrenBindings, Array.from(Array(count).keys()).map((i) => { return child.binding.clone({ ...child.binding.options, // clone struct with new arrays struct: _BufferBinding.cloneStruct(child.binding.options.struct) }); }) ].flat(); }); } } /** * Set the buffer alignments from {@link inputs}. */ setInputsAlignment() { let orderedBindings = Object.keys(this.inputs); const arrayBindings = orderedBindings.filter((bindingKey) => { return this.inputs[bindingKey].type.includes("array"); }); if (arrayBindings.length) { orderedBindings.sort((bindingKeyA, bindingKeyB) => { const isBindingAArray = Math.min(0, this.inputs[bindingKeyA].type.indexOf("array")); const isBindingBArray = Math.min(0, this.inputs[bindingKeyB].type.indexOf("array")); return isBindingAArray - isBindingBArray; }); if (arrayBindings.length > 1) { orderedBindings = orderedBindings.filter((bindingKey) => !arrayBindings.includes(bindingKey)); } } for (const bindingKey of orderedBindings) { const binding = this.inputs[bindingKey]; const bufferElementOptions = { name: toCamelCase(binding.name ?? bindingKey), key: bindingKey, type: binding.type }; const isArray = binding.type.includes("array") && (Array.isArray(binding.value) || ArrayBuffer.isView(binding.value)); this.bufferElements.push( isArray ? new BufferArrayElement({ ...bufferElementOptions, arrayLength: binding.value.length }) : new BufferElement(bufferElementOptions) ); } this.bufferElements.forEach((bufferElement, index) => { const startOffset = index === 0 ? 0 : this.bufferElements[index - 1].endOffset + 1; bufferElement.setAlignment(startOffset); }); if (arrayBindings.length > 1) { const arraySizes = arrayBindings.map((bindingKey) => { const binding = this.inputs[bindingKey]; const bufferLayout = getBufferLayout(BufferElement.getBaseType(binding.type)); return Math.ceil(binding.value.length / bufferLayout.numElements); }); const equalSize = arraySizes.every((size, i, array) => size === array[0]); if (equalSize) { const interleavedBufferElements = arrayBindings.map((bindingKey) => { const binding = this.inputs[bindingKey]; return new BufferInterleavedArrayElement({ name: toCamelCase(binding.name ?? bindingKey), key: bindingKey, type: binding.type, arrayLength: binding.value.length }); }); const tempBufferElements = arrayBindings.map((bindingKey) => { const binding = this.inputs[bindingKey]; return new BufferElement({ name: toCamelCase(binding.name ?? bindingKey), key: bindingKey, type: BufferElement.getType(binding.type) }); }); tempBufferElements.forEach((bufferElement, index) => { if (index === 0) { if (this.bufferElements.length) { bufferElement.setAlignmentFromPosition({ row: this.bufferElements[this.bufferElements.length - 1].alignment.end.row + 1, byte: 0 }); } else { bufferElement.setAlignment(0); } } else { bufferElement.setAlignment(tempBufferElements[index - 1].endOffset + 1); } }); const totalStride = tempBufferElements[tempBufferElements.length - 1].endOffset + 1 - tempBufferElements[0].startOffset; interleavedBufferElements.forEach((bufferElement, index) => { bufferElement.setAlignment( tempBufferElements[index].startOffset, Math.ceil(totalStride / bytesPerRow) * bytesPerRow ); }); this.bufferElements = [...this.bufferElements, ...interleavedBufferElements]; } else { throwWarning( `BufferBinding: "${this.label}" contains multiple array inputs that should use an interleaved array, but their sizes do not match. These inputs cannot be added to the BufferBinding: "${arrayBindings.join( ", " )}"` ); } } } /** * Set our buffer attributes: * Takes all the {@link inputs} and adds them to the {@link bufferElements} array with the correct start and end offsets (padded), then fill our {@link arrayBuffer} typed array accordingly. */ setBufferAttributes() { const bufferElementsArrayBufferSize = this.bufferElements.length ? this.bufferElements[this.bufferElements.length - 1].paddedByteCount : 0; this.arrayBufferSize = bufferElementsArrayBufferSize; this.childrenBindings.forEach((binding) => { this.arrayBufferSize += binding.arrayBufferSize; }); this.arrayBuffer = new ArrayBuffer(this.arrayBufferSize); this.arrayView = new DataView(this.arrayBuffer, 0, bufferElementsArrayBufferSize); this.childrenBindings.forEach((binding, index) => { let offset = bufferElementsArrayBufferSize; for (let i = 0; i < index; i++) { offset += this.childrenBindings[i].arrayBuffer.byteLength; } const bufferElLastRow = this.bufferElements.length ? this.bufferElements[this.bufferElements.length - 1].alignment.end.row + 1 : 0; const bindingLastRow = index > 0 ? this.childrenBindings[index - 1].bufferElements.length ? this.childrenBindings[index - 1].bufferElements[this.childrenBindings[index - 1].bufferElements.length - 1].alignment.end.row + 1 : 0 : 0; binding.bufferElements.forEach((bufferElement) => { const rowOffset = index === 0 ? bufferElLastRow + bindingLastRow : bindingLastRow; bufferElement.alignment.start.row += rowOffset; bufferElement.alignment.end.row += rowOffset; }); binding.arrayView = new DataView(this.arrayBuffer, offset, binding.arrayBuffer.byteLength); for (const bufferElement of binding.bufferElements) { bufferElement.setView(this.arrayBuffer, binding.arrayView); } }); if (!this.options.buffer) { this.buffer.size = this.arrayBuffer.byteLength; } for (const bufferElement of this.bufferElements) { bufferElement.setView(this.arrayBuffer, this.arrayView); } this.shouldUpdate = this.arrayBufferSize > 0; } /** * Set the WGSL code snippet to append to the shaders code. It consists of variable (and Struct structures if needed) declarations. */ setWGSLFragment() { if (!this.bufferElements.length && !this.childrenBindings.length) return; const kebabCaseLabel = toKebabCase(this.label); if (this.useStruct) { const structs = {}; structs[kebabCaseLabel] = {}; const bufferElements = this.bufferElements.filter( (bufferElement) => !(bufferElement instanceof BufferInterleavedArrayElement) ); const interleavedBufferElements = this.bufferElements.filter( (bufferElement) => bufferElement instanceof BufferInterleavedArrayElement ); if (interleavedBufferElements.length) { const arrayLength = this.bindingType === "uniform" ? `, ${interleavedBufferElements[0].numElements}` : ""; if (bufferElements.length) { structs[`${kebabCaseLabel}Element`] = {}; interleavedBufferElements.forEach((binding) => { structs[`${kebabCaseLabel}Element`][binding.name] = BufferElement.getType(binding.type); }); bufferElements.forEach((binding) => { structs[kebabCaseLabel][binding.name] = binding.type; }); const interleavedBufferName = this.bufferElements.find((bufferElement) => bufferElement.name === "elements") ? `${this.name}Elements` : "elements"; structs[kebabCaseLabel][interleavedBufferName] = `array<${kebabCaseLabel}Element${arrayLength}>`; const varType = getBindingWGSLVarType(this); this.wgslGroupFragment = [`${varType} ${this.name}: ${kebabCaseLabel};`]; } else { this.bufferElements.forEach((binding) => { structs[kebabCaseLabel][binding.name] = BufferElement.getType(binding.type); }); const varType = getBindingWGSLVarType(this); this.wgslGroupFragment = [`${varType} ${this.name}: array<${kebabCaseLabel}${arrayLength}>;`]; } } else { bufferElements.forEach((binding) => { const bindingType = this.bindingType === "uniform" && "numElements" in binding ? `array<${BufferElement.getType(binding.type)}, ${binding.numElements}>` : binding.type; structs[kebabCaseLabel][binding.name] = bindingType; }); const varType = getBindingWGSLVarType(this); this.wgslGroupFragment = [`${varType} ${this.name}: ${kebabCaseLabel};`]; } if (this.childrenBindings.length) { this.options.childrenBindings.forEach((child) => { structs[kebabCaseLabel][child.binding.name] = child.count && child.count > 1 || child.forceArray ? this.bindingType === "uniform" && child.binding.bindingType === "uniform" ? `array<${toKebabCase(child.binding.label)}, ${child.count}>` : `array<${toKebabCase(child.binding.label)}>` : toKebabCase(child.binding.label); }); } const additionalBindings = this.childrenBindings.length ? this.options.childrenBindings.map((child) => child.binding.wgslStructFragment).join("\n\n") + "\n\n" : ""; this.wgslStructFragment = additionalBindings + Object.keys(structs).reverse().map((struct) => { return `struct ${struct} { ${Object.keys(structs[struct]).map((binding) => `${binding}: ${structs[struct][binding]}`).join(",\n ")} };`; }).join("\n\n"); } else { this.wgslStructFragment = ""; this.wgslGroupFragment = this.bufferElements.map((binding) => { const varType = getBindingWGSLVarType(this); return `${varType} ${binding.name}: ${binding.type};`; }); } } /** * Set a {@link BufferBinding#shouldUpdate | binding shouldUpdate} flag to `true` to update our {@link arrayBuffer} array during next render. * @param bindingName - the binding name/key to update */ shouldUpdateBinding(bindingName = "") { if (this.inputs[bindingName]) { this.inputs[bindingName].shouldUpdate = true; } } /** * Executed at the beginning of a Material render call. * If any of the {@link inputs} has changed, run its `onBeforeUpdate` callback then updates our {@link arrayBuffer} array. * Also sets the {@link shouldUpdate} property to true so the {@link core/bindGroups/BindGroup.BindGroup | BindGroup} knows it will need to update the {@link GPUBuffer}. */ update() { if (this.options.buffer) { this.shouldUpdate = false; return; } const inputs = Object.values(this.inputs); for (const binding of inputs) { const bufferElement = this.bufferElements.find((bufferEl) => bufferEl.key === binding.name); if (binding.shouldUpdate && bufferElement) { binding.onBeforeUpdate && binding.onBeforeUpdate(); bufferElement.update(binding.value); this.shouldUpdate = true; binding.shouldUpdate = false; } } this.childrenBindings.forEach((binding) => { binding.update(); if (binding.shouldUpdate) { this.shouldUpdate = true; } binding.shouldUpdate = false; }); if (this.shouldUpdate && this.parent && this.parentViewSetBufferEls) { let index = 0; this.parentViewSetBufferEls.forEach((viewSetBuffer, i) => { const { bufferElement, viewSetFunction } = viewSetBuffer; bufferElement.view.forEach((value) => { viewSetFunction(index * bufferElement.view.BYTES_PER_ELEMENT, value, true); index++; }); }); this.parent.shouldUpdate = true; this.shouldUpdate = false; } } /** * Extract the data corresponding to a specific {@link BufferElement} from a {@link Float32Array} holding the {@link BufferBinding#buffer | GPU buffer} data of this {@link BufferBinding} * @param parameters - parameters used to extract the data * @param parameters.result - {@link Float32Array} holding {@link GPUBuffer} data * @param parameters.bufferElementName - name of the {@link BufferElement} to use to extract the data * @returns - extracted data from the {@link Float32Array} */ extractBufferElementDataFromBufferResult({ result, bufferElementName }) { const bufferElement = this.bufferElements.find((bufferElement2) => bufferElement2.name === bufferElementName); if (bufferElement) { return bufferElement.extractDataFromBufferResult(result); } else { return result; } } }; _parent = new WeakMap(); let BufferBinding = _BufferBinding; export { BufferBinding };