UNPKG

lume

Version:

Build next-level interactive web applications.

464 lines 25.1 kB
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; import { batch, untrack } from 'solid-js'; import { element, numberAttribute, stringAttribute } from '@lume/element'; import { InstancedMesh as ThreeInstancedMesh } from 'three/src/objects/InstancedMesh.js'; import { BoxGeometry } from 'three/src/geometries/BoxGeometry.js'; import { MeshPhongMaterial } from 'three/src/materials/MeshPhongMaterial.js'; import { DynamicDrawUsage } from 'three/src/constants.js'; import { Quaternion } from 'three/src/math/Quaternion.js'; import { Vector3 } from 'three/src/math/Vector3.js'; import { Color } from 'three/src/math/Color.js'; import { Matrix4 } from 'three/src/math/Matrix4.js'; import { Euler } from 'three/src/math/Euler.js'; import { Mesh } from './Mesh.js'; import { autoDefineElements } from '../LumeConfig.js'; import { stringToNumberArray } from './utils.js'; import { queueMicrotaskOnceOnly } from '../utils/queueMicrotaskOnceOnly.js'; const quat = new Quaternion(); const pos = new Vector3(); const scale = new Vector3(); const pivot = new Vector3(); const mat = new Matrix4(); const rot = new Euler(); const color = new Color(); // const threeJsPostAdjustment = [0, 0, 0] // const alignAdjustment = [0, 0, 0] // const mountPointAdjustment = [0, 0, 0] const appliedPosition = [0, 0, 0]; /** * @element lume-instanced-mesh * @class InstancedMesh - This is similar to Mesh, but renders multiple * "instances" of a geometry (insead of only one) with a single draw call to * the GPU, as if all the instances were a single geometry. This is more * efficient in cases where multiple objects to be rendered are similar * (share the same geometry and material). Rendering multiple similar objects * as separate Mesh instances would otherwise incur one draw call to the GPU * per mesh which will be slower. * * For sake of simplicity, `<lume-instanced-mesh>` has a box-geometry and * phong-material by default. * * ## Example * * <live-code id="liveExample"></live-code> * <script> * liveExample.content = instancedMeshExample * </script> * * @extends Mesh * */ let InstancedMesh = (() => { let _classDecorators = [element('lume-instanced-mesh', autoDefineElements)]; let _classDescriptor; let _classExtraInitializers = []; let _classThis; let _classSuper = Mesh; let _instanceExtraInitializers = []; let _count_decorators; let _count_initializers = []; let _count_extraInitializers = []; let _get_rotations_decorators; let _set_rotations_decorators; let _get_positions_decorators; let _set_positions_decorators; let _get_scales_decorators; let _set_scales_decorators; let _get_colors_decorators; let _set_colors_decorators; var InstancedMesh = class extends _classSuper { static { _classThis = this; } static { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; _count_decorators = [numberAttribute]; _get_rotations_decorators = [stringAttribute]; _set_rotations_decorators = [stringAttribute]; _get_positions_decorators = [stringAttribute]; _set_positions_decorators = [stringAttribute]; _get_scales_decorators = [stringAttribute]; _set_scales_decorators = [stringAttribute]; _get_colors_decorators = [stringAttribute]; _set_colors_decorators = [stringAttribute]; __esDecorate(this, null, _get_rotations_decorators, { kind: "getter", name: "rotations", static: false, private: false, access: { has: obj => "rotations" in obj, get: obj => obj.rotations }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _set_rotations_decorators, { kind: "setter", name: "rotations", static: false, private: false, access: { has: obj => "rotations" in obj, set: (obj, value) => { obj.rotations = value; } }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _get_positions_decorators, { kind: "getter", name: "positions", static: false, private: false, access: { has: obj => "positions" in obj, get: obj => obj.positions }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _set_positions_decorators, { kind: "setter", name: "positions", static: false, private: false, access: { has: obj => "positions" in obj, set: (obj, value) => { obj.positions = value; } }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _get_scales_decorators, { kind: "getter", name: "scales", static: false, private: false, access: { has: obj => "scales" in obj, get: obj => obj.scales }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _set_scales_decorators, { kind: "setter", name: "scales", static: false, private: false, access: { has: obj => "scales" in obj, set: (obj, value) => { obj.scales = value; } }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _get_colors_decorators, { kind: "getter", name: "colors", static: false, private: false, access: { has: obj => "colors" in obj, get: obj => obj.colors }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _set_colors_decorators, { kind: "setter", name: "colors", static: false, private: false, access: { has: obj => "colors" in obj, set: (obj, value) => { obj.colors = value; } }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(null, null, _count_decorators, { kind: "field", name: "count", static: false, private: false, access: { has: obj => "count" in obj, get: obj => obj.count, set: (obj, value) => { obj.count = value; } }, metadata: _metadata }, _count_initializers, _count_extraInitializers); __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); InstancedMesh = _classThis = _classDescriptor.value; if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); __runInitializers(_classThis, _classExtraInitializers); } /** * @property {number} count - The number of instances to render. */ count = (__runInitializers(this, _instanceExtraInitializers), __runInitializers(this, _count_initializers, 10)); #biggestCount = (__runInitializers(this, _count_extraInitializers), this.count); /** * @property {number[]} rotations - The rotations for each instance. * Generally the array should have a length of `this.count * 3` because * each rotation consists of three numbers for X, Y, and Z axes. Every three * numbers is one X,Y,Z triplet. If the array has less rotations than * `this.count`, the missing rotations will be considered to have * values of zero. If it has more than `this.count` rotations, those * rotations are ignored. */ get rotations() { return this.#rotations; } set rotations(v) { this.#rotations = stringToNumberArray(v, 'rotations'); } #rotations = []; /** * @property {number[]} positions - The positions for each instance. * Generally the array should have a length of `this.count * 3` because * each rotation consists of three numbers for X, Y, and Z axes. Every three * numbers is one X,Y,Z triplet. If the array has less positions than * `this.count`, the missing positions will be considered to have * values of zero. If it has more than `this.count` positions, those * positions are ignored. */ get positions() { return this.#positions; } set positions(v) { this.#positions = stringToNumberArray(v, 'positions'); } #positions = []; /** * @property {number[]} scales - The scales for each instance. * Generally the array should have a length of `this.count * 3` because * each rotation consists of three numbers for X, Y, and Z axes. Every three * numbers is one X,Y,Z triplet. If the array has less scales than * `this.count`, the missing scales will be considered to have * values of zero. If it has more than `this.count` scales, those * scales are ignored. */ get scales() { return this.#scales; } set scales(v) { this.#scales = stringToNumberArray(v, 'scales'); } #scales = []; /** * @property {number[]} colors - The colors for each instance. * Generally the array should have a length of `this.count * 3` because * each rotation consists of three numbers for R, G, and B color components. Every three * numbers is one R,G,B triplet. If the array has less colors than * `this.count`, the missing colors will be considered to have * values of zero (black). If it has more than `this.count` colors, those * colors are ignored. */ get colors() { return this.#colors; } set colors(v) { this.#colors = stringToNumberArray(v, 'colors'); } #colors = []; initialBehaviors = { geometry: 'box', material: 'physical' }; // This class will have a THREE.InstancedMesh for its .three property. makeThreeObject3d() { let geometryBehavior = null; let materialBehavior = null; for (const [name, behavior] of this.behaviors) { if (name.endsWith('-geometry')) geometryBehavior = behavior; else if (name.endsWith('-material')) materialBehavior = behavior; } // Use the existing geometry and material from the behaviors in case we are in the recreateThree process. const mesh = new ThreeInstancedMesh(geometryBehavior?.meshComponent || new BoxGeometry(), materialBehavior?.meshComponent || new MeshPhongMaterial(), this.#biggestCount); // TODO make this configurable. Most people probably won't care about this. mesh.instanceMatrix.setUsage(DynamicDrawUsage); const original = mesh.setColorAt; mesh.setColorAt = function (index, color) { // This creates the instanceColor buffer if it doesn't exist. original.call(mesh, index, color); // TODO make this configurable. Most people probably won't care about this. mesh.instanceColor.setUsage(DynamicDrawUsage); }; return mesh; } #allMatricesNeedUpdate = false; #allColorsNeedUpdate = false; #updateSingleInstanceOnly = false; setInstancePosition(index, x, y, z) { const arrIndex = index * 3; // Untrack because the purpose of the method is to update this, not read it. untrack(() => { this.positions[arrIndex] = x; this.positions[arrIndex + 1] = y; this.positions[arrIndex + 2] = z; this.#setMatrix(arrIndex); this.three.instanceMatrix.needsUpdate = true; }); queueMicrotaskOnceOnly(this.#triggerPositions); } #triggerPositions = () => { this.#updateSingleInstanceOnly = true; this.positions = this.positions; // trigger reactivity }; setInstanceScale(index, x, y, z) { const arrIndex = index * 3; // Untrack because the purpose of the method is to update this, not read it. untrack(() => { this.scales[arrIndex] = x; this.scales[arrIndex + 1] = y; this.scales[arrIndex + 2] = z; this.#setMatrix(arrIndex); this.three.instanceMatrix.needsUpdate = true; }); queueMicrotaskOnceOnly(this.#triggerScales); } #triggerScales = () => { this.#updateSingleInstanceOnly = true; this.scales = this.scales; // trigger reactivity }; setInstanceRotation(index, x, y, z) { const arrIndex = index * 3; // Untrack because the purpose of the method is to update this, not read it. untrack(() => { this.rotations[arrIndex] = x; this.rotations[arrIndex + 1] = y; this.rotations[arrIndex + 2] = z; this.#setMatrix(arrIndex); this.three.instanceMatrix.needsUpdate = true; }); queueMicrotaskOnceOnly(this.#triggerRotations); } #triggerRotations = () => { this.#updateSingleInstanceOnly = true; this.rotations = this.rotations; // trigger reactivity }; setInstanceColor(index, r, g, b) { const arrIndex = index * 3; // Untrack because the purpose of the method is to update this, not read it. untrack(() => { this.colors[arrIndex] = r; this.colors[arrIndex + 1] = g; this.colors[arrIndex + 2] = b; }); this.#setColor(index, r, g, b); this.three.instanceColor.needsUpdate = true; queueMicrotaskOnceOnly(this.#triggerColors); } #triggerColors = () => { this.#updateSingleInstanceOnly = true; this.colors = this.colors; // trigger reactivity }; // TODO Might just be able to set individual components of the matrix // without recalculating other components (f.e. if only an instance position // changed but not rotation) #setMatrix(index) { rot.set(this.rotations[index + 0] ?? 0, this.rotations[index + 1] ?? 0, this.rotations[index + 2] ?? 0); quat.setFromEuler(rot); pos.set(this.positions[index + 0] ?? 0, this.positions[index + 1] ?? 0, this.positions[index + 2] ?? 0); scale.set(this.scales[index + 0] ?? 1, this.scales[index + 1] ?? 1, this.scales[index + 2] ?? 1); // Modifies _mat in place. this._calculateInstanceMatrix(pos, quat, scale, pivot, mat); this.three.setMatrixAt(index / 3, mat); } // TODO a colorMode variable can specify whether colors are RGB triplets, or CSS string/hex values. // TODO Set an update range so that if we're updating only one instance, we're not uploading the whole array each time. #setColor(index, r, g, b) { color.setRGB(r, g, b); this.three.setColorAt(index, color); } updateAllMatrices() { for (let i = 0, l = this.count; i < l; i += 1) this.#setMatrix(i * 3); this.three.instanceMatrix.needsUpdate = true; } updateAllColors() { for (let i = 0, l = this.count; i < l; i += 1) { const j = i * 3; const r = this.colors[j + 0] ?? 1; const g = this.colors[j + 1] ?? 1; const b = this.colors[j + 2] ?? 1; this.#setColor(i, r, g, b); } this.three.instanceColor.needsUpdate = true; } /** * This is very similar to SharedAPI._calculateMatrix, without the threeCSS parts. */ _calculateInstanceMatrix(pos, quat, scale, pivot, result) { // const align = new Vector3(0, 0, 0) // TODO // const mountPoint = new Vector3(0, 0, 0) // TODO const position = pos; const origin = new Vector3(0.5, 0.5, 0.5); // TODO const size = this.calculatedSize; // In the following commented code, we ignore the // threeJsPostAdjustment, alignAdjustment, and mountPointAdjustment // because the align point and mount point of the instances are // inherited from the IntancedMesh element's positioning. In other // words, the instances are positioned relative to the element's // position, which already has alignPoint and mountPoint factored into // it. // TODO Should we provide the same alignment and mount point API for // instances, and would align point be relative to the InstancedMesh // element (as if instances are sub nodes of the InstancedMesh // element), or to the InstancedMesh element's parent (as if instances // are sub nodes of the parent, just like a single mesh would be)? // THREE-COORDS-TO-DOM-COORDS // translate the "mount point" back to the top/left/back of the object // (in Three.js it is in the center of the object). // threeJsPostAdjustment[0] = size.x / 2 // threeJsPostAdjustment[1] = size.y / 2 // threeJsPostAdjustment[2] = size.z / 2 // const parentSize = this._getParentSize() // THREE-COORDS-TO-DOM-COORDS // translate the "align" back to the top/left/back of the parent element. // We offset this in ElementOperations#applyTransform. The Y // value is inverted because we invert it below. // threeJsPostAdjustment[0] += -parentSize.x / 2 // threeJsPostAdjustment[1] += -parentSize.y / 2 // threeJsPostAdjustment[2] += -parentSize.z / 2 // alignAdjustment[0] = parentSize.x * align.x // alignAdjustment[1] = parentSize.y * align.y // alignAdjustment[2] = parentSize.z * align.z // mountPointAdjustment[0] = size.x * mountPoint.x // mountPointAdjustment[1] = size.y * mountPoint.y // mountPointAdjustment[2] = size.z * mountPoint.z appliedPosition[0] = position.x; /*+ alignAdjustment[0] - mountPointAdjustment[0]*/ appliedPosition[1] = position.y; /*+ alignAdjustment[1] - mountPointAdjustment[1]*/ appliedPosition[2] = position.z; /*+ alignAdjustment[2] - mountPointAdjustment[2]*/ // NOTE We negate Y translation in several places below so that Y // goes downward like in DOM's CSS transforms. position.set(appliedPosition[0] /*+ threeJsPostAdjustment[0]*/, // THREE-COORDS-TO-DOM-COORDS negate the Y value so that // Three.js' positive Y is downward like DOM. -(appliedPosition[1] /*+ threeJsPostAdjustment[1]*/), appliedPosition[2] /*+ threeJsPostAdjustment[2]*/); if (origin.x !== 0.5 || origin.y !== 0.5 || origin.z !== 0.5) { // Here we multiply by size to convert from a ratio to a range // of units, then subtract half because Three.js origin is // centered around (0,0,0) meaning Three.js origin goes from // -0.5 to 0.5 instead of from 0 to 1. pivot.set(origin.x * size.x - size.x / 2, // THREE-COORDS-TO-DOM-COORDS negate the Y value so that // positive Y means down instead of up (because Three,js Y // values go up). -(origin.y * size.y - size.y / 2), origin.z * size.z - size.z / 2); } // otherwise, use default Three.js origin of (0,0,0) which is // equivalent to our (0.5,0.5,0.5), by removing the pivot value. else { pivot.set(0, 0, 0); } // effectively the same as Object3DWithPivot.updateMatrix() { result.compose(position, quat, scale); if (pivot.x !== 0 || pivot.y !== 0 || pivot.z !== 0) { const px = pivot.x, py = pivot.y, pz = pivot.z; const te = result.elements; te[12] += px - te[0] * px - te[4] * py - te[8] * pz; te[13] += py - te[1] * px - te[5] * py - te[9] * pz; te[14] += pz - te[2] * px - te[6] * py - te[10] * pz; } // } } connectedCallback() { super.connectedCallback(); this.createEffect(() => { // Increase the InstancedMesh size (by making a new one) as needed. if (this.count > this.#biggestCount) { this.#biggestCount = this.count; this.recreateThree(); // Be sure to trigger all the instance components so that the new // InstancedMesh will be up-to-date. untrack(() => { batch(() => { this.rotations = this.rotations; this.positions = this.positions; this.scales = this.scales; this.colors = this.colors; }); }); } untrack(() => (this.three.count = this.count)); this.needsUpdate(); }); this.createEffect(() => { this.rotations; if (!this.#updateSingleInstanceOnly) this.#allMatricesNeedUpdate = true; this.#updateSingleInstanceOnly = false; this.needsUpdate(); }); this.createEffect(() => { this.positions; if (!this.#updateSingleInstanceOnly) this.#allMatricesNeedUpdate = true; this.#updateSingleInstanceOnly = false; this.needsUpdate(); }); this.createEffect(() => { this.scales; if (!this.#updateSingleInstanceOnly) this.#allMatricesNeedUpdate = true; this.#updateSingleInstanceOnly = false; this.needsUpdate(); }); this.createEffect(() => { this.colors; if (!this.#updateSingleInstanceOnly) this.#allColorsNeedUpdate = true; this.#updateSingleInstanceOnly = false; this.needsUpdate(); }); } update(t, dt) { super.update(t, dt); if (this.#allMatricesNeedUpdate) { this.#allMatricesNeedUpdate = false; this.updateAllMatrices(); } if (this.#allColorsNeedUpdate) { this.#allColorsNeedUpdate = false; this.updateAllColors(); } } }; return InstancedMesh = _classThis; })(); export { InstancedMesh }; //# sourceMappingURL=InstancedMesh.js.map