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.
289 lines (286 loc) • 10.8 kB
JavaScript
import { getBufferLayout } from '../utils.mjs';
import { throwWarning } from '../../../utils/utils.mjs';
const slotsPerRow = 4;
const bytesPerSlot = 4;
const bytesPerRow = slotsPerRow * bytesPerSlot;
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) * bytesPerRow;
}
/**
* 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 / bytesPerSlot;
}
/**
* 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 / bytesPerSlot);
}
/**
* Get the position at given offset (i.e. byte index).
* @param offset - Byte index to use.
*/
getPositionAtOffset(offset = 0) {
return {
row: Math.floor(offset / bytesPerRow),
byte: offset % bytesPerRow
};
}
/**
* 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 * bytesPerRow + 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 > bytesPerRow - 1) {
const overflow = position.byte % bytesPerRow;
position.row += Math.floor(position.byte / bytesPerRow);
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 <= bytesPerRow && nextPositionAvailable.byte + size > bytesPerRow) {
nextPositionAvailable.row += 1;
nextPositionAvailable.byte = 0;
} else if (size > bytesPerRow && (nextPositionAvailable.byte > bytesPerRow || nextPositionAvailable.byte > 0)) {
nextPositionAvailable.row += 1;
nextPositionAvailable.byte = 0;
}
alignment.end = {
row: nextPositionAvailable.row + Math.ceil(size / bytesPerRow) - 1,
byte: nextPositionAvailable.byte + (size % bytesPerRow === 0 ? bytesPerRow - 1 : size % bytesPerRow - 1)
// end of row ? then it ends on slot (bytesPerRow - 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}.
*/
setAlignment(startOffset = 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 = ((value2) => {
if (typeof value2 === "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 value2.elements ? this.setValueFromMat3 : this.setValueFromArrayWithPad;
} else if (value2.elements) {
return this.setValueFromMat4OrQuat;
} else if (ArrayBuffer.isView(value2) || Array.isArray(value2)) {
if (!this.bufferLayout.pad) {
return this.setValueFromArray;
} else {
return this.setValueFromArrayWithPad;
}
} else {
throwWarning(`${this.constructor.name}: value passed to ${this.name} cannot be used: ${value2}`);
}
})(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);
}
}
export { BufferElement, bytesPerRow, bytesPerSlot, slotsPerRow };