UNPKG

@luma.gl/engine

Version:

3D Engine Components for luma.gl

1,530 lines (1,503 loc) 274 kB
(function webpackUniversalModuleDefinition(root, factory) { if (typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if (typeof define === 'function' && define.amd) define([], factory); else if (typeof exports === 'object') exports['luma'] = factory(); else root['luma'] = factory();})(globalThis, function () { "use strict"; var __exports__ = (() => { var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; // external-global-plugin:@luma.gl/core var require_core = __commonJS({ "external-global-plugin:@luma.gl/core"(exports, module) { module.exports = globalThis.luma; } }); // external-global-plugin:@luma.gl/shadertools var require_shadertools = __commonJS({ "external-global-plugin:@luma.gl/shadertools"(exports, module) { module.exports = globalThis.luma; } }); // bundle.ts var bundle_exports = {}; __export(bundle_exports, { AnimationLoop: () => AnimationLoop, AnimationLoopTemplate: () => AnimationLoopTemplate, AsyncTexture: () => AsyncTexture, BackgroundTextureModel: () => BackgroundTextureModel, BufferTransform: () => BufferTransform, ClipSpace: () => ClipSpace, Computation: () => Computation, ConeGeometry: () => ConeGeometry, CubeGeometry: () => CubeGeometry, CylinderGeometry: () => CylinderGeometry, DirectionalLightModel: () => DirectionalLightModel, DynamicTexture: () => DynamicTexture, GPUGeometry: () => GPUGeometry, Geometry: () => Geometry, GroupNode: () => GroupNode, IcoSphereGeometry: () => IcoSphereGeometry, KeyFrames: () => KeyFrames, LegacyPickingManager: () => LegacyPickingManager, Material: () => Material, MaterialFactory: () => MaterialFactory, Model: () => Model, ModelNode: () => ModelNode, PickingManager: () => PickingManager, PlaneGeometry: () => PlaneGeometry, PointLightModel: () => PointLightModel, ScenegraphNode: () => ScenegraphNode, ShaderInputs: () => ShaderInputs, ShaderPassRenderer: () => ShaderPassRenderer, SphereGeometry: () => SphereGeometry, SpotLightModel: () => SpotLightModel, Swap: () => Swap, SwapBuffers: () => SwapBuffers, SwapFramebuffers: () => SwapFramebuffers, TextureTransform: () => TextureTransform, Timeline: () => Timeline, TruncatedConeGeometry: () => TruncatedConeGeometry, cancelAnimationFramePolyfill: () => cancelAnimationFramePolyfill, colorPicking: () => picking, indexPicking: () => picking2, legacyColorPicking: () => legacyColorPicking, loadImage: () => loadImage, loadImageBitmap: () => loadImageBitmap, makeAnimationLoop: () => makeAnimationLoop, makeRandomGenerator: () => makeRandomGenerator, picking: () => picking3, requestAnimationFramePolyfill: () => requestAnimationFramePolyfill, resolvePickingBackend: () => resolvePickingBackend, resolvePickingMode: () => resolvePickingMode, setPathPrefix: () => setPathPrefix, supportsIndexPicking: () => supportsIndexPicking }); __reExport(bundle_exports, __toESM(require_core(), 1)); // src/animation/timeline.ts var channelHandles = 1; var animationHandles = 1; var Timeline = class { time = 0; channels = /* @__PURE__ */ new Map(); animations = /* @__PURE__ */ new Map(); playing = false; lastEngineTime = -1; constructor() { } addChannel(props) { const { delay = 0, duration = Number.POSITIVE_INFINITY, rate = 1, repeat = 1 } = props; const channelId = channelHandles++; const channel = { time: 0, delay, duration, rate, repeat }; this._setChannelTime(channel, this.time); this.channels.set(channelId, channel); return channelId; } removeChannel(channelId) { this.channels.delete(channelId); for (const [animationHandle, animation] of this.animations) { if (animation.channel === channelId) { this.detachAnimation(animationHandle); } } } isFinished(channelId) { const channel = this.channels.get(channelId); if (channel === void 0) { return false; } return this.time >= channel.delay + channel.duration * channel.repeat; } getTime(channelId) { if (channelId === void 0) { return this.time; } const channel = this.channels.get(channelId); if (channel === void 0) { return -1; } return channel.time; } setTime(time) { this.time = Math.max(0, time); const channels = this.channels.values(); for (const channel of channels) { this._setChannelTime(channel, this.time); } const animations = this.animations.values(); for (const animationData of animations) { const { animation, channel } = animationData; animation.setTime(this.getTime(channel)); } } play() { this.playing = true; } pause() { this.playing = false; this.lastEngineTime = -1; } reset() { this.setTime(0); } attachAnimation(animation, channelHandle) { const animationHandle = animationHandles++; this.animations.set(animationHandle, { animation, channel: channelHandle }); animation.setTime(this.getTime(channelHandle)); return animationHandle; } detachAnimation(channelId) { this.animations.delete(channelId); } update(engineTime) { if (this.playing) { if (this.lastEngineTime === -1) { this.lastEngineTime = engineTime; } this.setTime(this.time + (engineTime - this.lastEngineTime)); this.lastEngineTime = engineTime; } } _setChannelTime(channel, time) { const offsetTime = time - channel.delay; const totalDuration = channel.duration * channel.repeat; if (offsetTime >= totalDuration) { channel.time = channel.duration * channel.rate; } else { channel.time = Math.max(0, offsetTime) % channel.duration; channel.time *= channel.rate; } } }; // src/animation/key-frames.ts var KeyFrames = class { startIndex = -1; endIndex = -1; factor = 0; times = []; values = []; _lastTime = -1; constructor(keyFrames) { this.setKeyFrames(keyFrames); this.setTime(0); } setKeyFrames(keyFrames) { const numKeys = keyFrames.length; this.times.length = numKeys; this.values.length = numKeys; for (let i = 0; i < numKeys; ++i) { this.times[i] = keyFrames[i][0]; this.values[i] = keyFrames[i][1]; } this._calculateKeys(this._lastTime); } setTime(time) { time = Math.max(0, time); if (time !== this._lastTime) { this._calculateKeys(time); this._lastTime = time; } } getStartTime() { return this.times[this.startIndex]; } getEndTime() { return this.times[this.endIndex]; } getStartData() { return this.values[this.startIndex]; } getEndData() { return this.values[this.endIndex]; } _calculateKeys(time) { let index = 0; const numKeys = this.times.length; for (index = 0; index < numKeys - 2; ++index) { if (this.times[index + 1] > time) { break; } } this.startIndex = index; this.endIndex = index + 1; const startTime = this.times[this.startIndex]; const endTime = this.times[this.endIndex]; this.factor = Math.min(Math.max(0, (time - startTime) / (endTime - startTime)), 1); } }; // src/animation-loop/animation-loop-template.ts var AnimationLoopTemplate = class { constructor(animationProps) { } async onInitialize(animationProps) { return null; } }; // src/animation-loop/animation-loop.ts var import_core = __toESM(require_core(), 1); // src/animation-loop/request-animation-frame.ts function requestAnimationFramePolyfill(callback) { const browserRequestAnimationFrame = typeof window !== "undefined" ? window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame : null; if (browserRequestAnimationFrame) { return browserRequestAnimationFrame.call(window, callback); } return setTimeout( () => callback(typeof performance !== "undefined" ? performance.now() : Date.now()), 1e3 / 60 ); } function cancelAnimationFramePolyfill(timerId) { const browserCancelAnimationFrame = typeof window !== "undefined" ? window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame : null; if (browserCancelAnimationFrame) { browserCancelAnimationFrame.call(window, timerId); return; } clearTimeout(timerId); } // ../../node_modules/@probe.gl/stats/dist/utils/hi-res-timestamp.js function getHiResTimestamp() { let timestamp; if (typeof window !== "undefined" && window.performance) { timestamp = window.performance.now(); } else if (typeof process !== "undefined" && process.hrtime) { const timeParts = process.hrtime(); timestamp = timeParts[0] * 1e3 + timeParts[1] / 1e6; } else { timestamp = Date.now(); } return timestamp; } // ../../node_modules/@probe.gl/stats/dist/lib/stat.js var Stat = class { constructor(name, type) { this.sampleSize = 1; this.time = 0; this.count = 0; this.samples = 0; this.lastTiming = 0; this.lastSampleTime = 0; this.lastSampleCount = 0; this._count = 0; this._time = 0; this._samples = 0; this._startTime = 0; this._timerPending = false; this.name = name; this.type = type; this.reset(); } reset() { this.time = 0; this.count = 0; this.samples = 0; this.lastTiming = 0; this.lastSampleTime = 0; this.lastSampleCount = 0; this._count = 0; this._time = 0; this._samples = 0; this._startTime = 0; this._timerPending = false; return this; } setSampleSize(samples) { this.sampleSize = samples; return this; } /** Call to increment count (+1) */ incrementCount() { this.addCount(1); return this; } /** Call to decrement count (-1) */ decrementCount() { this.subtractCount(1); return this; } /** Increase count */ addCount(value) { this._count += value; this._samples++; this._checkSampling(); return this; } /** Decrease count */ subtractCount(value) { this._count -= value; this._samples++; this._checkSampling(); return this; } /** Add an arbitrary timing and bump the count */ addTime(time) { this._time += time; this.lastTiming = time; this._samples++; this._checkSampling(); return this; } /** Start a timer */ timeStart() { this._startTime = getHiResTimestamp(); this._timerPending = true; return this; } /** End a timer. Adds to time and bumps the timing count. */ timeEnd() { if (!this._timerPending) { return this; } this.addTime(getHiResTimestamp() - this._startTime); this._timerPending = false; this._checkSampling(); return this; } getSampleAverageCount() { return this.sampleSize > 0 ? this.lastSampleCount / this.sampleSize : 0; } /** Calculate average time / count for the previous window */ getSampleAverageTime() { return this.sampleSize > 0 ? this.lastSampleTime / this.sampleSize : 0; } /** Calculate counts per second for the previous window */ getSampleHz() { return this.lastSampleTime > 0 ? this.sampleSize / (this.lastSampleTime / 1e3) : 0; } getAverageCount() { return this.samples > 0 ? this.count / this.samples : 0; } /** Calculate average time / count */ getAverageTime() { return this.samples > 0 ? this.time / this.samples : 0; } /** Calculate counts per second */ getHz() { return this.time > 0 ? this.samples / (this.time / 1e3) : 0; } _checkSampling() { if (this._samples === this.sampleSize) { this.lastSampleTime = this._time; this.lastSampleCount = this._count; this.count += this._count; this.time += this._time; this.samples += this._samples; this._time = 0; this._count = 0; this._samples = 0; } } }; // ../../node_modules/@probe.gl/stats/dist/lib/stats.js var Stats = class { constructor(options) { this.stats = {}; this.id = options.id; this.stats = {}; this._initializeStats(options.stats); Object.seal(this); } /** Acquire a stat. Create if it doesn't exist. */ get(name, type = "count") { return this._getOrCreate({ name, type }); } get size() { return Object.keys(this.stats).length; } /** Reset all stats */ reset() { for (const stat of Object.values(this.stats)) { stat.reset(); } return this; } forEach(fn) { for (const stat of Object.values(this.stats)) { fn(stat); } } getTable() { const table = {}; this.forEach((stat) => { table[stat.name] = { time: stat.time || 0, count: stat.count || 0, average: stat.getAverageTime() || 0, hz: stat.getHz() || 0 }; }); return table; } _initializeStats(stats = []) { stats.forEach((stat) => this._getOrCreate(stat)); } _getOrCreate(stat) { const { name, type } = stat; let result = this.stats[name]; if (!result) { if (stat instanceof Stat) { result = stat; } else { result = new Stat(name, type); } this.stats[name] = result; } return result; } }; // src/animation-loop/animation-loop.ts var statIdCounter = 0; var ANIMATION_LOOP_STATS = "Animation Loop"; var _AnimationLoop = class { device = null; canvas = null; props; animationProps = null; timeline = null; stats; sharedStats; cpuTime; gpuTime; frameRate; display; _needsRedraw = "initialized"; _initialized = false; _running = false; _animationFrameId = null; _nextFramePromise = null; _resolveNextFrame = null; _cpuStartTime = 0; _error = null; _lastFrameTime = 0; /* * @param {HTMLCanvasElement} canvas - if provided, width and height will be passed to context */ constructor(props) { this.props = { ..._AnimationLoop.defaultAnimationLoopProps, ...props }; props = this.props; if (!props.device) { throw new Error("No device provided"); } this.stats = props.stats || new Stats({ id: `animation-loop-${statIdCounter++}` }); this.sharedStats = import_core.luma.stats.get(ANIMATION_LOOP_STATS); this.frameRate = this.stats.get("Frame Rate"); this.frameRate.setSampleSize(1); this.cpuTime = this.stats.get("CPU Time"); this.gpuTime = this.stats.get("GPU Time"); this.setProps({ autoResizeViewport: props.autoResizeViewport }); this.start = this.start.bind(this); this.stop = this.stop.bind(this); this._onMousemove = this._onMousemove.bind(this); this._onMouseleave = this._onMouseleave.bind(this); } destroy() { this.stop(); this._setDisplay(null); this.device?._disableDebugGPUTime(); } /** @deprecated Use .destroy() */ delete() { this.destroy(); } reportError(error) { this.props.onError(error); this._error = error; } /** Flags this animation loop as needing redraw */ setNeedsRedraw(reason) { this._needsRedraw = this._needsRedraw || reason; return this; } /** Query redraw status. Clears the flag. */ needsRedraw() { const reason = this._needsRedraw; this._needsRedraw = false; return reason; } setProps(props) { if ("autoResizeViewport" in props) { this.props.autoResizeViewport = props.autoResizeViewport || false; } return this; } /** Starts a render loop if not already running */ async start() { if (this._running) { return this; } this._running = true; try { let appContext; if (!this._initialized) { this._initialized = true; await this._initDevice(); this._initialize(); if (!this._running) { return null; } await this.props.onInitialize(this._getAnimationProps()); } if (!this._running) { return null; } if (appContext !== false) { this._cancelAnimationFrame(); this._requestAnimationFrame(); } return this; } catch (err) { const error = err instanceof Error ? err : new Error("Unknown error"); this.props.onError(error); throw error; } } /** Stops a render loop if already running, finalizing */ stop() { if (this._running) { if (this.animationProps && !this._error) { this.props.onFinalize(this.animationProps); } this._cancelAnimationFrame(); this._nextFramePromise = null; this._resolveNextFrame = null; this._running = false; this._lastFrameTime = 0; } return this; } /** Explicitly draw a frame */ redraw(time) { if (this.device?.isLost || this._error) { return this; } this._beginFrameTimers(time); this._setupFrame(); this._updateAnimationProps(); this._renderFrame(this._getAnimationProps()); this._clearNeedsRedraw(); if (this._resolveNextFrame) { this._resolveNextFrame(this); this._nextFramePromise = null; this._resolveNextFrame = null; } this._endFrameTimers(); return this; } /** Add a timeline, it will be automatically updated by the animation loop. */ attachTimeline(timeline) { this.timeline = timeline; return this.timeline; } /** Remove a timeline */ detachTimeline() { this.timeline = null; } /** Wait until a render completes */ waitForRender() { this.setNeedsRedraw("waitForRender"); if (!this._nextFramePromise) { this._nextFramePromise = new Promise((resolve) => { this._resolveNextFrame = resolve; }); } return this._nextFramePromise; } /** TODO - should use device.deviceContext */ async toDataURL() { this.setNeedsRedraw("toDataURL"); await this.waitForRender(); if (this.canvas instanceof HTMLCanvasElement) { return this.canvas.toDataURL(); } throw new Error("OffscreenCanvas"); } // PRIVATE METHODS _initialize() { this._startEventHandling(); this._initializeAnimationProps(); this._updateAnimationProps(); this._resizeViewport(); this.device?._enableDebugGPUTime(); } _setDisplay(display) { if (this.display) { this.display.destroy(); this.display.animationLoop = null; } if (display) { display.animationLoop = this; } this.display = display; } _requestAnimationFrame() { if (!this._running) { return; } this._animationFrameId = requestAnimationFramePolyfill(this._animationFrame.bind(this)); } _cancelAnimationFrame() { if (this._animationFrameId === null) { return; } cancelAnimationFramePolyfill(this._animationFrameId); this._animationFrameId = null; } _animationFrame(time) { if (!this._running) { return; } this.redraw(time); this._requestAnimationFrame(); } // Called on each frame, can be overridden to call onRender multiple times // to support e.g. stereoscopic rendering _renderFrame(animationProps) { if (this.display) { this.display._renderFrame(animationProps); return; } this.props.onRender(this._getAnimationProps()); this.device?.submit(); } _clearNeedsRedraw() { this._needsRedraw = false; } _setupFrame() { this._resizeViewport(); } // Initialize the object that will be passed to app callbacks _initializeAnimationProps() { const canvasContext = this.device?.getDefaultCanvasContext(); if (!this.device || !canvasContext) { throw new Error("loop"); } const canvas = canvasContext?.canvas; const useDevicePixels = canvasContext.props.useDevicePixels; this.animationProps = { animationLoop: this, device: this.device, canvasContext, canvas, // @ts-expect-error Deprecated useDevicePixels, timeline: this.timeline, needsRedraw: false, // Placeholders width: 1, height: 1, aspect: 1, // Animation props time: 0, startTime: Date.now(), engineTime: 0, tick: 0, tock: 0, // Experimental _mousePosition: null // Event props }; } _getAnimationProps() { if (!this.animationProps) { throw new Error("animationProps"); } return this.animationProps; } // Update the context object that will be passed to app callbacks _updateAnimationProps() { if (!this.animationProps) { return; } const { width, height, aspect } = this._getSizeAndAspect(); if (width !== this.animationProps.width || height !== this.animationProps.height) { this.setNeedsRedraw("drawing buffer resized"); } if (aspect !== this.animationProps.aspect) { this.setNeedsRedraw("drawing buffer aspect changed"); } this.animationProps.width = width; this.animationProps.height = height; this.animationProps.aspect = aspect; this.animationProps.needsRedraw = this._needsRedraw; this.animationProps.engineTime = Date.now() - this.animationProps.startTime; if (this.timeline) { this.timeline.update(this.animationProps.engineTime); } this.animationProps.tick = Math.floor(this.animationProps.time / 1e3 * 60); this.animationProps.tock++; this.animationProps.time = this.timeline ? this.timeline.getTime() : this.animationProps.engineTime; } /** Wait for supplied device */ async _initDevice() { this.device = await this.props.device; if (!this.device) { throw new Error("No device provided"); } this.canvas = this.device.getDefaultCanvasContext().canvas || null; } _createInfoDiv() { if (this.canvas && this.props.onAddHTML) { const wrapperDiv = document.createElement("div"); document.body.appendChild(wrapperDiv); wrapperDiv.style.position = "relative"; const div = document.createElement("div"); div.style.position = "absolute"; div.style.left = "10px"; div.style.bottom = "10px"; div.style.width = "300px"; div.style.background = "white"; if (this.canvas instanceof HTMLCanvasElement) { wrapperDiv.appendChild(this.canvas); } wrapperDiv.appendChild(div); const html = this.props.onAddHTML(div); if (html) { div.innerHTML = html; } } } _getSizeAndAspect() { if (!this.device) { return { width: 1, height: 1, aspect: 1 }; } const [width, height] = this.device.getDefaultCanvasContext().getDrawingBufferSize(); const aspect = width > 0 && height > 0 ? width / height : 1; return { width, height, aspect }; } /** @deprecated Default viewport setup */ _resizeViewport() { if (this.props.autoResizeViewport && this.device.gl) { this.device.gl.viewport( 0, 0, // @ts-expect-error Expose canvasContext this.device.gl.drawingBufferWidth, // @ts-expect-error Expose canvasContext this.device.gl.drawingBufferHeight ); } } _beginFrameTimers(time) { const now = time ?? (typeof performance !== "undefined" ? performance.now() : Date.now()); if (this._lastFrameTime) { const frameTime = now - this._lastFrameTime; if (frameTime > 0) { this.frameRate.addTime(frameTime); } } this._lastFrameTime = now; if (this.device?._isDebugGPUTimeEnabled()) { this._consumeEncodedGpuTime(); } this.cpuTime.timeStart(); } _endFrameTimers() { if (this.device?._isDebugGPUTimeEnabled()) { this._consumeEncodedGpuTime(); } this.cpuTime.timeEnd(); this._updateSharedStats(); } _consumeEncodedGpuTime() { if (!this.device) { return; } const gpuTimeMs = this.device.commandEncoder._gpuTimeMs; if (gpuTimeMs !== void 0) { this.gpuTime.addTime(gpuTimeMs); this.device.commandEncoder._gpuTimeMs = void 0; } } _updateSharedStats() { if (this.stats === this.sharedStats) { return; } for (const name of Object.keys(this.sharedStats.stats)) { if (!this.stats.stats[name]) { delete this.sharedStats.stats[name]; } } this.stats.forEach((sourceStat) => { const targetStat = this.sharedStats.get(sourceStat.name, sourceStat.type); targetStat.sampleSize = sourceStat.sampleSize; targetStat.time = sourceStat.time; targetStat.count = sourceStat.count; targetStat.samples = sourceStat.samples; targetStat.lastTiming = sourceStat.lastTiming; targetStat.lastSampleTime = sourceStat.lastSampleTime; targetStat.lastSampleCount = sourceStat.lastSampleCount; targetStat._count = sourceStat._count; targetStat._time = sourceStat._time; targetStat._samples = sourceStat._samples; targetStat._startTime = sourceStat._startTime; targetStat._timerPending = sourceStat._timerPending; }); } // Event handling _startEventHandling() { if (this.canvas) { this.canvas.addEventListener("mousemove", this._onMousemove.bind(this)); this.canvas.addEventListener("mouseleave", this._onMouseleave.bind(this)); } } _onMousemove(event) { if (event instanceof MouseEvent) { this._getAnimationProps()._mousePosition = [event.offsetX, event.offsetY]; } } _onMouseleave(event) { this._getAnimationProps()._mousePosition = null; } }; var AnimationLoop = _AnimationLoop; __publicField(AnimationLoop, "defaultAnimationLoopProps", { device: null, onAddHTML: () => "", onInitialize: async () => null, onRender: () => { }, onFinalize: () => { }, onError: (error) => console.error(error), // eslint-disable-line no-console stats: void 0, // view parameters autoResizeViewport: false }); // src/animation-loop/make-animation-loop.ts var import_core2 = __toESM(require_core(), 1); function makeAnimationLoop(AnimationLoopTemplateCtor, props) { let renderLoop = null; const device = props?.device || import_core2.luma.createDevice({ id: "animation-loop", adapters: props?.adapters, createCanvasContext: true }); const animationLoop = new AnimationLoop({ ...props, device, async onInitialize(animationProps) { clearError(animationProps.animationLoop.device); try { renderLoop = new AnimationLoopTemplateCtor(animationProps); return await renderLoop?.onInitialize(animationProps); } catch (error) { console.error(error); setError(animationProps.animationLoop.device, error); return null; } }, onRender: (animationProps) => renderLoop?.onRender(animationProps), onFinalize: (animationProps) => renderLoop?.onFinalize(animationProps) }); animationLoop.getInfo = () => { return this.AnimationLoopTemplateCtor.info; }; return animationLoop; } function setError(device, error) { if (!device) { return; } const canvas = device.getDefaultCanvasContext().canvas; if (canvas instanceof HTMLCanvasElement) { canvas.style.overflow = "visible"; let errorDiv = document.getElementById("animation-loop-error"); errorDiv?.remove(); errorDiv = document.createElement("h1"); errorDiv.id = "animation-loop-error"; errorDiv.innerHTML = error.message; errorDiv.style.position = "absolute"; errorDiv.style.top = "10px"; errorDiv.style.left = "10px"; errorDiv.style.color = "black"; errorDiv.style.backgroundColor = "red"; canvas.parentElement?.appendChild(errorDiv); } } function clearError(device) { if (!device) { return; } const errorDiv = document.getElementById("animation-loop-error"); if (errorDiv) { errorDiv.remove(); } } // src/model/model.ts var import_core8 = __toESM(require_core(), 1); var import_shadertools2 = __toESM(require_shadertools(), 1); // src/geometry/gpu-geometry.ts var import_core3 = __toESM(require_core(), 1); // src/utils/uid.ts var uidCounters = {}; function uid(id = "id") { uidCounters[id] = uidCounters[id] || 1; const count = uidCounters[id]++; return `${id}-${count}`; } // src/geometry/gpu-geometry.ts var GPUGeometry = class { id; userData = {}; /** Determines how vertices are read from the 'vertex' attributes */ topology; bufferLayout = []; vertexCount; indices; attributes; constructor(props) { this.id = props.id || uid("geometry"); this.topology = props.topology; this.indices = props.indices || null; this.attributes = props.attributes; this.vertexCount = props.vertexCount; this.bufferLayout = props.bufferLayout || []; if (this.indices) { if (!(this.indices.usage & import_core3.Buffer.INDEX)) { throw new Error("Index buffer must have INDEX usage"); } } } destroy() { this.indices?.destroy(); for (const attribute of Object.values(this.attributes)) { attribute.destroy(); } } getVertexCount() { return this.vertexCount; } getAttributes() { return this.attributes; } getIndexes() { return this.indices || null; } _calculateVertexCount(positions) { const vertexCount = positions.byteLength / 12; return vertexCount; } }; function makeGPUGeometry(device, geometry) { if (geometry instanceof GPUGeometry) { return geometry; } const indices = getIndexBufferFromGeometry(device, geometry); const { attributes, bufferLayout } = getAttributeBuffersFromGeometry(device, geometry); return new GPUGeometry({ topology: geometry.topology || "triangle-list", bufferLayout, vertexCount: geometry.vertexCount, indices, attributes }); } function getIndexBufferFromGeometry(device, geometry) { if (!geometry.indices) { return void 0; } const data = geometry.indices.value; return device.createBuffer({ usage: import_core3.Buffer.INDEX, data }); } function getAttributeBuffersFromGeometry(device, geometry) { const bufferLayout = []; const attributes = {}; for (const [attributeName, attribute] of Object.entries(geometry.attributes)) { let name = attributeName; switch (attributeName) { case "POSITION": name = "positions"; break; case "NORMAL": name = "normals"; break; case "TEXCOORD_0": name = "texCoords"; break; case "TEXCOORD_1": name = "texCoords1"; break; case "COLOR_0": name = "colors"; break; } if (attribute) { attributes[name] = device.createBuffer({ data: attribute.value, id: `${attributeName}-buffer` }); const { value, size, normalized } = attribute; if (size === void 0) { throw new Error(`Attribute ${attributeName} is missing a size`); } bufferLayout.push({ name, format: import_core3.vertexFormatDecoder.getVertexFormatFromAttribute(value, size, normalized) }); } } const vertexCount = geometry._calculateVertexCount(geometry.attributes, geometry.indices); return { attributes, bufferLayout, vertexCount }; } // src/debug/debug-shader-layout.ts function getDebugTableForShaderLayout(layout, name) { const table = {}; const header = "Values"; if (layout.attributes.length === 0 && !layout.varyings?.length) { return { "No attributes or varyings": { [header]: "N/A" } }; } for (const attributeDeclaration of layout.attributes) { if (attributeDeclaration) { const glslDeclaration = `${attributeDeclaration.location} ${attributeDeclaration.name}: ${attributeDeclaration.type}`; table[`in ${glslDeclaration}`] = { [header]: attributeDeclaration.stepMode || "vertex" }; } } for (const varyingDeclaration of layout.varyings || []) { const glslDeclaration = `${varyingDeclaration.location} ${varyingDeclaration.name}`; table[`out ${glslDeclaration}`] = { [header]: JSON.stringify(varyingDeclaration) }; } return table; } // src/debug/debug-framebuffer.ts var DEBUG_FRAMEBUFFER_STATE_KEY = "__debugFramebufferState"; var DEFAULT_MARGIN_PX = 8; function debugFramebuffer(renderPass, source3, options) { if (renderPass.device.type !== "webgl") { return; } const state = getDebugFramebufferState(renderPass.device); if (state.flushing) { return; } if (isDefaultRenderPass(renderPass)) { flushDebugFramebuffers(renderPass, options, state); return; } if (source3 && isFramebuffer(source3) && source3.handle !== null) { if (!state.queuedFramebuffers.includes(source3)) { state.queuedFramebuffers.push(source3); } } } function flushDebugFramebuffers(renderPass, options, state) { if (state.queuedFramebuffers.length === 0) { return; } const webglDevice = renderPass.device; const { gl } = webglDevice; const previousReadFramebuffer = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING); const previousDrawFramebuffer = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING); const [targetWidth, targetHeight] = renderPass.device.getDefaultCanvasContext().getDrawingBufferSize(); let topPx = parseCssPixel(options.top, DEFAULT_MARGIN_PX); const leftPx = parseCssPixel(options.left, DEFAULT_MARGIN_PX); state.flushing = true; try { for (const framebuffer of state.queuedFramebuffers) { const [targetX0, targetY0, targetX1, targetY1, previewHeight] = getOverlayRect({ framebuffer, targetWidth, targetHeight, topPx, leftPx, minimap: options.minimap }); gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer.handle); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); gl.blitFramebuffer( 0, 0, framebuffer.width, framebuffer.height, targetX0, targetY0, targetX1, targetY1, gl.COLOR_BUFFER_BIT, gl.NEAREST ); topPx += previewHeight + DEFAULT_MARGIN_PX; } } finally { gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousReadFramebuffer); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDrawFramebuffer); state.flushing = false; } } function getOverlayRect(options) { const { framebuffer, targetWidth, targetHeight, topPx, leftPx, minimap } = options; const maxWidth = minimap ? Math.max(Math.floor(targetWidth / 4), 1) : targetWidth; const maxHeight = minimap ? Math.max(Math.floor(targetHeight / 4), 1) : targetHeight; const scale2 = Math.min(maxWidth / framebuffer.width, maxHeight / framebuffer.height); const previewWidth = Math.max(Math.floor(framebuffer.width * scale2), 1); const previewHeight = Math.max(Math.floor(framebuffer.height * scale2), 1); const targetX0 = leftPx; const targetY0 = Math.max(targetHeight - topPx - previewHeight, 0); const targetX1 = targetX0 + previewWidth; const targetY1 = targetY0 + previewHeight; return [targetX0, targetY0, targetX1, targetY1, previewHeight]; } function getDebugFramebufferState(device) { device.userData[DEBUG_FRAMEBUFFER_STATE_KEY] ||= { flushing: false, queuedFramebuffers: [] }; return device.userData[DEBUG_FRAMEBUFFER_STATE_KEY]; } function isFramebuffer(value) { return "colorAttachments" in value; } function isDefaultRenderPass(renderPass) { const framebuffer = renderPass.props.framebuffer; return !framebuffer || framebuffer.handle === null; } function parseCssPixel(value, defaultValue) { if (!value) { return defaultValue; } const parsedValue = Number.parseInt(value, 10); return Number.isFinite(parsedValue) ? parsedValue : defaultValue; } // src/utils/deep-equal.ts function deepEqual(a, b, depth) { if (a === b) { return true; } if (!depth || !a || !b) { return false; } if (Array.isArray(a)) { if (!Array.isArray(b) || a.length !== b.length) { return false; } for (let i = 0; i < a.length; i++) { if (!deepEqual(a[i], b[i], depth - 1)) { return false; } } return true; } if (Array.isArray(b)) { return false; } if (typeof a === "object" && typeof b === "object") { const aKeys = Object.keys(a); const bKeys = Object.keys(b); if (aKeys.length !== bKeys.length) { return false; } for (const key of aKeys) { if (!b.hasOwnProperty(key)) { return false; } if (!deepEqual(a[key], b[key], depth - 1)) { return false; } } return true; } return false; } // src/utils/buffer-layout-helper.ts var import_core4 = __toESM(require_core(), 1); var BufferLayoutHelper = class { bufferLayouts; constructor(bufferLayouts) { this.bufferLayouts = bufferLayouts; } getBufferLayout(name) { return this.bufferLayouts.find((layout) => layout.name === name) || null; } /** Get attribute names from a BufferLayout */ getAttributeNamesForBuffer(bufferLayout) { return bufferLayout.attributes ? bufferLayout.attributes?.map((layout) => layout.attribute) : [bufferLayout.name]; } mergeBufferLayouts(bufferLayouts1, bufferLayouts2) { const mergedLayouts = [...bufferLayouts1]; for (const attribute of bufferLayouts2) { const index = mergedLayouts.findIndex((attribute2) => attribute2.name === attribute.name); if (index < 0) { mergedLayouts.push(attribute); } else { mergedLayouts[index] = attribute; } } return mergedLayouts; } getBufferIndex(bufferName) { const bufferIndex = this.bufferLayouts.findIndex((layout) => layout.name === bufferName); if (bufferIndex === -1) { import_core4.log.warn(`BufferLayout: Missing buffer for "${bufferName}".`)(); } return bufferIndex; } }; // src/utils/buffer-layout-order.ts function getMinLocation(attributeNames, shaderLayoutMap) { let minLocation = Infinity; for (const name of attributeNames) { const location = shaderLayoutMap[name]; if (location !== void 0) { minLocation = Math.min(minLocation, location); } } return minLocation; } function sortedBufferLayoutByShaderSourceLocations(shaderLayout, bufferLayout) { const shaderLayoutMap = Object.fromEntries( shaderLayout.attributes.map((attr) => [attr.name, attr.location]) ); const sortedLayout = bufferLayout.slice(); sortedLayout.sort((a, b) => { const attributeNamesA = a.attributes ? a.attributes.map((attr) => attr.attribute) : [a.name]; const attributeNamesB = b.attributes ? b.attributes.map((attr) => attr.attribute) : [b.name]; const minLocationA = getMinLocation(attributeNamesA, shaderLayoutMap); const minLocationB = getMinLocation(attributeNamesB, shaderLayoutMap); return minLocationA - minLocationB; }); return sortedLayout; } // src/utils/shader-module-utils.ts function mergeShaderModuleBindingsIntoLayout(shaderLayout, modules) { if (!shaderLayout || !modules.some((module) => module.bindingLayout?.length)) { return shaderLayout; } const mergedLayout = { ...shaderLayout, bindings: shaderLayout.bindings.map((binding) => ({ ...binding })) }; if ("attributes" in (shaderLayout || {})) { mergedLayout.attributes = shaderLayout?.attributes || []; } for (const module of modules) { for (const bindingLayout of module.bindingLayout || []) { for (const relatedBindingName of getRelatedBindingNames(bindingLayout.name)) { const binding = mergedLayout.bindings.find( (candidate) => candidate.name === relatedBindingName ); if (binding?.group === 0) { binding.group = bindingLayout.group; } } } } return mergedLayout; } function shaderModuleHasUniforms(module) { return Boolean(module.uniformTypes && !isObjectEmpty(module.uniformTypes)); } function getRelatedBindingNames(bindingName) { const bindingNames = /* @__PURE__ */ new Set([bindingName, `${bindingName}Uniforms`]); if (!bindingName.endsWith("Uniforms")) { bindingNames.add(`${bindingName}Sampler`); } return [...bindingNames]; } function isObjectEmpty(obj) { for (const key in obj) { return false; } return true; } // src/shader-inputs.ts var import_core5 = __toESM(require_core(), 1); var import_shadertools = __toESM(require_shadertools(), 1); // ../../node_modules/@math.gl/types/dist/is-array.js function isTypedArray(value) { return ArrayBuffer.isView(value) && !(value instanceof DataView); } function isNumberArray(value) { if (Array.isArray(value)) { return value.length === 0 || typeof value[0] === "number"; } return false; } function isNumericArray(value) { return isTypedArray(value) || isNumberArray(value); } // src/model/split-uniforms-and-bindings.ts function isUniformValue(value) { return isNumericArray(value) || typeof value === "number" || typeof value === "boolean"; } function splitUniformsAndBindings(uniforms, uniformTypes2 = {}) { const result = { bindings: {}, uniforms: {} }; Object.keys(uniforms).forEach((name) => { const uniform = uniforms[name]; if (Object.prototype.hasOwnProperty.call(uniformTypes2, name) || isUniformValue(uniform)) { result.uniforms[name] = uniform; } else { result.bindings[name] = uniform; } }); return result; } // src/shader-inputs.ts var ShaderInputs = class { options = { disableWarnings: false }; /** * The map of modules * @todo should should this include the resolved dependencies? */ // @ts-ignore Fix typings modules; /** Stores the uniform values for each module */ moduleUniforms; /** Stores the uniform bindings for each module */ moduleBindings; /** Tracks if uniforms have changed */ // moduleUniformsChanged: Record<keyof ShaderPropsT, false | string>; /** * Create a new UniformStore instance * @param modules */ constructor(modules, options) { Object.assign(this.options, options); const resolvedModules = (0, import_shadertools.getShaderModuleDependencies)( Object.values(modules).filter(isShaderInputsModuleWithDependencies) ); for (const resolvedModule of resolvedModules) { modules[resolvedModule.name] = resolvedModule; } import_core5.log.log(1, "Creating ShaderInputs with modules", Object.keys(modules))(); this.modules = modules; this.moduleUniforms = {}; this.moduleBindings = {}; for (const [name, module] of Object.entries(modules)) { if (module) { this._addModule(module); if (module.name && name !== module.name && !this.options.disableWarnings) { import_core5.log.warn(`Module name: ${name} vs ${module.name}`)(); } } } } /** Destroy */ destroy() { } /** * Set module props */ setProps(props) { for (const name of Object.keys(props)) { const moduleName = name; const moduleProps = props[moduleName] || {}; const module = this.modules[moduleName]; if (!module) { if (!this.options.disableWarnings) { import_core5.log.warn(`Module ${name} not found`)(); } } else { const oldUniforms = this.moduleUniforms[moduleName]; const oldBindings = this.moduleBindings[moduleName]; const uniformsAndBindings = module.getUniforms?.(moduleProps, oldUniforms) || moduleProps; const { uniforms, bindings } = splitUniformsAndBindings( uniformsAndBindings, module.uniformTypes ); this.moduleUniforms[moduleName] = mergeModuleUniforms( oldUniforms, uniforms, module.uniformTypes ); this.moduleBindings[moduleName] = { ...oldBindings, ...bindings }; } } } /** * Return the map of modules * @todo should should this include the resolved dependencies? */ getModules() { return Object.values(this.modules); } /** Get all uniform values for all modules */ getUniformValues() { return this.moduleUniforms; } /** Merges all bindings for the shader (from the various modules) */ getBindingValues() { const bindings = {}; for (const moduleBindings of Object.values(this.moduleBindings)) { Object.assign(bindings, moduleBindings); } return bindings; } // INTERNAL /** Return a debug table that can be used for console.table() or log.table() */ getDebugTable() { const table = {}; for (const [moduleName, module] of Object.entries(this.moduleUniforms)) { for (const [key, value] of Object.entries(module)) { table[`${moduleName}.${key}`] = { type: this.modules[moduleName].uniformTypes?.[key], value: String(value) }; } } return table; } _addModule(module) { const moduleName = module.name; this.moduleUniforms[moduleName] = mergeModuleUniforms( {}, module.defaultUniforms || {}, module.uniformTypes ); this.moduleBindings[moduleName] = {}; } }; function mergeModuleUniforms(currentUniforms = {}, nextUniforms = {}, uniformTypes2 = {}) { const mergedUniforms = { ...currentUniforms }; for (const [key, value] of Object.entries(nextUniforms)) { if (value !== void 0) { mergedUniforms[key] = mergeModuleUniformValue(currentUniforms[key], value, uniformTypes2[key]); } } return mergedUniforms; } function mergeModuleUniformValue(currentValue, nextValue, uniformType) { if (!uniformType || typeof uniformType === "string") { return cloneModuleUniformValue(nextValue); } if (Array.isArray(unifor