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.

294 lines (293 loc) 10.9 kB
import { throwWarning } from "../../../utils/utils.mjs"; import { getBufferLayout } from "../utils.mjs"; /** * Used to handle each {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | buffer binding array} view and data layout alignment. * Compute the exact alignment offsets needed to fill an {@link ArrayBuffer} that will be sent to a {@link GPUBuffer}, based on an input type and value. * Also update the view array at the correct offset. * * So all our struct need to be packed into our arrayBuffer using a precise layout. * They will be stored in rows, each row made of 4 slots and each slots made of 4 bytes. Depending on the binding element type, its row and slot may vary and we may have to insert empty padded values. * All in all it looks like that:<br> * <pre> * slot 0 slot 1 slot 2 slot 3 * row 0 | _ _ _ _ | _ _ _ _ | _ _ _ _ | _ _ _ _ | * row 1 | _ _ _ _ | _ _ _ _ | _ _ _ _ | _ _ _ _ | * row 2 | _ _ _ _ | _ _ _ _ | _ _ _ _ | _ _ _ _ | * </pre> * see https://webgpufundamentals.org/webgpu/lessons/resources/wgsl-offset-computer.html */ var BufferElement = class BufferElement { /** * BufferElement constructor * @param parameters - {@link BufferElementParams | parameters} used to create our {@link BufferElement}. */ constructor({ name, key, type = "f32" }) { this.name = name; this.key = key; this.type = type; this.baseType = BufferElement.getBaseType(this.type); this.bufferLayout = getBufferLayout(this.baseType); this.alignment = { start: { row: 0, byte: 0 }, end: { row: 0, byte: 0 } }; this.setValue = null; } /** * Get the {@link BufferElement} {@link WGSLVariableType | WGSL type}. * @param type - Original type passed. * @returns - The {@link BufferElement} {@link WGSLVariableType | WGSL type}. */ static getType(type) { return type.replace("array", "").replace("<", "").replace(">", ""); } /** * Get the {@link BufferElement} {@link WGSLBaseVariableType | WGSL base type}. * @param type - Original type passed. * @returns - The {@link BufferElement} {@link WGSLBaseVariableType | WGSL base type}. */ static getBaseType(type) { return BufferElement.getType(type.replace("atomic", "").replace("array", "").replaceAll("<", "").replaceAll(">", "")); } /** * Get the total number of rows used by this {@link BufferElement}. * @readonly */ get rowCount() { return this.alignment.end.row - this.alignment.start.row + 1; } /** * Get the total number of bytes used by this {@link BufferElement} based on {@link BufferElementAlignment | alignment} start and end offsets. * @readonly */ get byteCount() { return Math.abs(this.endOffset - this.startOffset) + 1; } /** * Get the total number of bytes used by this {@link BufferElement}, including final padding. * @readonly */ get paddedByteCount() { return (this.alignment.end.row + 1) * 16; } /** * Get the offset (i.e. byte index) at which our {@link BufferElement} starts. * @readonly */ get startOffset() { return this.getByteCountAtPosition(this.alignment.start); } /** * Get the array offset (i.e. array index) at which our {@link BufferElement} starts. * @readonly */ get startOffsetToIndex() { return this.startOffset / 4; } /** * Get the offset (i.e. byte index) at which our {@link BufferElement} ends. * @readonly */ get endOffset() { return this.getByteCountAtPosition(this.alignment.end); } /** * Get the array offset (i.e. array index) at which our {@link BufferElement} ends. * @readonly */ get endOffsetToIndex() { return Math.floor(this.endOffset / 4); } /** * Get the position at given offset (i.e. byte index). * @param offset - Byte index to use. */ getPositionAtOffset(offset = 0) { return { row: Math.floor(offset / 16), byte: offset % 16 }; } /** * Get the number of bytes at a given {@link BufferElementAlignmentPosition | position}. * @param position - {@link BufferElementAlignmentPosition | Position} from which to count. * @returns - Byte count at the given {@link BufferElementAlignmentPosition | position}. */ getByteCountAtPosition(position = { row: 0, byte: 0 }) { return position.row * 16 + position.byte; } /** * Check that a {@link BufferElementAlignmentPosition#byte | byte position} does not overflow its max value (16). * @param position - {@link BufferElementAlignmentPosition | Position}. * @returns - Updated {@link BufferElementAlignmentPosition | position}. */ applyOverflowToPosition(position = { row: 0, byte: 0 }) { if (position.byte > 15) { const overflow = position.byte % 16; position.row += Math.floor(position.byte / 16); position.byte = overflow; } return position; } /** * Get the number of bytes between two {@link BufferElementAlignmentPosition | positions}. * @param p1 - First {@link BufferElementAlignmentPosition | position}. * @param p2 - Second {@link BufferElementAlignmentPosition | position}. * @returns - Number of bytes. */ getByteCountBetweenPositions(p1 = { row: 0, byte: 0 }, p2 = { row: 0, byte: 0 }) { return Math.abs(this.getByteCountAtPosition(p2) - this.getByteCountAtPosition(p1)); } /** * Compute the right alignment (i.e. start and end rows and bytes) given the size and align properties and the next available {@link BufferElementAlignmentPosition | position}. * @param nextPositionAvailable - next {@link BufferElementAlignmentPosition | position} at which we should insert this element. * @returns - Computed {@link BufferElementAlignment | alignment}. */ getElementAlignment(nextPositionAvailable = { row: 0, byte: 0 }) { const alignment = { start: nextPositionAvailable, end: nextPositionAvailable }; const { size, align } = this.bufferLayout; if (nextPositionAvailable.byte % align !== 0) nextPositionAvailable.byte += nextPositionAvailable.byte % align; if (size <= 16 && nextPositionAvailable.byte + size > 16) { nextPositionAvailable.row += 1; nextPositionAvailable.byte = 0; } else if (size > 16 && (nextPositionAvailable.byte > 16 || nextPositionAvailable.byte > 0)) { nextPositionAvailable.row += 1; nextPositionAvailable.byte = 0; } alignment.end = { row: nextPositionAvailable.row + Math.ceil(size / 16) - 1, byte: nextPositionAvailable.byte + (size % 16 === 0 ? 15 : size % 16 - 1) }; alignment.end = this.applyOverflowToPosition(alignment.end); return alignment; } /** * Set the {@link BufferElementAlignment | alignment} from a {@link BufferElementAlignmentPosition | position}. * @param position - {@link BufferElementAlignmentPosition | position} at which to start inserting the values in the {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | BufferBinding arrayBuffer}. */ setAlignmentFromPosition(position = { row: 0, byte: 0 }) { this.alignment = this.getElementAlignment(position); } /** * Set the {@link BufferElementAlignment | alignment} from an offset (byte count). * @param startOffset - Offset at which to start inserting the values in the parent {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | BufferBinding arrayBuffer}. * @param minStride - Minimum stride to use for the values in the parent {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | BufferBinding arrayBuffer}. */ setAlignment(startOffset = 0, minStride = 0) { this.setAlignmentFromPosition(this.getPositionAtOffset(startOffset)); } /** * Set this {@link BufferElement} {@link view} into a parent {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | BufferBinding arrayBuffer}. * @param arrayBuffer - The parent {@link core/bindings/BufferBinding.BufferBinding#arrayBuffer | BufferBinding arrayBuffer}. * @param arrayView - The parent {@link core/bindings/BufferBinding.BufferBinding#arrayView | BufferBinding arrayView}. */ setView(arrayBuffer, arrayView) { this.view = new this.bufferLayout.View(arrayBuffer, this.startOffset, this.byteCount / this.bufferLayout.View.BYTES_PER_ELEMENT); } /** * Set the {@link view} value from a float or an int. * @param value - Float or int to use. */ setValueFromNumber(value) { this.view[0] = value; } /** * Set the {@link view} value from a {@link Vec2} or an array. * @param value - {@link Vec2} or array to use. */ setValueFromVec2(value) { this.view[0] = value.x ?? value[0] ?? 0; this.view[1] = value.y ?? value[1] ?? 0; } /** * Set the {@link view} value from a {@link Vec3} or an array. * @param value - {@link Vec3} or array to use. */ setValueFromVec3(value) { this.view[0] = value.x ?? value[0] ?? 0; this.view[1] = value.y ?? value[1] ?? 0; this.view[2] = value.z ?? value[2] ?? 0; } /** * Set the {@link view} value from a {@link Mat4} or {@link Quat}. * @param value - {@link Mat4} or {@link Quat} to use. */ setValueFromMat4OrQuat(value) { this.view.set(value.elements); } /** * Set the {@link view} value from a {@link Mat3}. * @param value - {@link Mat3} to use. */ setValueFromMat3(value) { this.setValueFromArrayWithPad(value.elements); } /** * Set the {@link view} value from an array. * @param value - Array to use. */ setValueFromArray(value) { this.view.set(value); } /** * Set the {@link view} value from an array with pad applied. * @param value - Array to use. */ setValueFromArrayWithPad(value) { for (let i = 0, offset = 0; i < this.view.length; i += this.bufferLayout.pad[0] + this.bufferLayout.pad[1], offset++) for (let j = 0; j < this.bufferLayout.pad[0]; j++) this.view[i + j] = value[i + j - offset]; } /** * Update the {@link view} based on the new value. * @param value - New value to use. */ update(value) { if (!this.setValue) this.setValue = ((value) => { if (typeof value === "number") return this.setValueFromNumber; else if (this.type === "vec2f") return this.setValueFromVec2; else if (this.type === "vec3f") return this.setValueFromVec3; else if (this.type === "mat3x3f") return value.elements ? this.setValueFromMat3 : this.setValueFromArrayWithPad; else if (value.elements) return this.setValueFromMat4OrQuat; else if (ArrayBuffer.isView(value) || Array.isArray(value)) if (!this.bufferLayout.pad) return this.setValueFromArray; else return this.setValueFromArrayWithPad; else throwWarning(`${this.constructor.name}: value passed to ${this.name} cannot be used: ${value}`); })(value); this.setValue(value); } /** * Extract the data corresponding to this specific {@link BufferElement} from a {@link Float32Array} holding the {@link GPUBuffer} data of the parentMesh {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}. * @param result - {@link Float32Array} holding {@link GPUBuffer} data. * @returns - Extracted data from the {@link Float32Array}. */ extractDataFromBufferResult(result) { return result.slice(this.startOffsetToIndex, this.endOffsetToIndex); } }; //#endregion export { BufferElement };