UNPKG

@luma.gl/engine

Version:

3D Engine Components for luma.gl

1,559 lines (1,529 loc) 207 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, GPUGeometry: () => GPUGeometry, Geometry: () => Geometry, GroupNode: () => GroupNode, IcoSphereGeometry: () => IcoSphereGeometry, KeyFrames: () => KeyFrames, LegacyPickingManager: () => LegacyPickingManager, Model: () => Model, ModelNode: () => ModelNode, PickingManager: () => PickingManager, PipelineFactory: () => PipelineFactory, PlaneGeometry: () => PlaneGeometry, ScenegraphNode: () => ScenegraphNode, ShaderFactory: () => ShaderFactory, ShaderInputs: () => ShaderInputs, ShaderPassRenderer: () => ShaderPassRenderer, SphereGeometry: () => SphereGeometry, Swap: () => Swap, SwapBuffers: () => SwapBuffers, SwapFramebuffers: () => SwapFramebuffers, TextureTransform: () => TextureTransform, Timeline: () => Timeline, TruncatedConeGeometry: () => TruncatedConeGeometry, cancelAnimationFramePolyfill: () => cancelAnimationFramePolyfill, colorPicking: () => picking2, indexPicking: () => picking, loadImage: () => loadImage, loadImageBitmap: () => loadImageBitmap, makeAnimationLoop: () => makeAnimationLoop, makeRandomGenerator: () => makeRandomGenerator, requestAnimationFramePolyfill: () => requestAnimationFramePolyfill, setPathPrefix: () => setPathPrefix }); __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) { return typeof window !== "undefined" && window.requestAnimationFrame ? window.requestAnimationFrame(callback) : setTimeout(callback, 1e3 / 60); } function cancelAnimationFramePolyfill(timerId) { return typeof window !== "undefined" && window.cancelAnimationFrame ? window.cancelAnimationFrame(timerId) : 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 DEFAULT_ANIMATION_LOOP_PROPS = { device: null, onAddHTML: () => "", onInitialize: async () => { return null; }, onRender: () => { }, onFinalize: () => { }, onError: (error) => console.error(error), // eslint-disable-line no-console stats: import_core.luma.stats.get(`animation-loop-${statIdCounter++}`), // view parameters useDevicePixels: true, autoResizeViewport: false, autoResizeDrawingBuffer: false }; var AnimationLoop = class { device = null; canvas = null; props; animationProps = null; timeline = null; stats; cpuTime; gpuTime; frameRate; display; needsRedraw = "initialized"; _initialized = false; _running = false; _animationFrameId = null; _nextFramePromise = null; _resolveNextFrame = null; _cpuStartTime = 0; _error = null; // _gpuTimeQuery: Query | null = null; /* * @param {HTMLCanvasElement} canvas - if provided, width and height will be passed to context */ constructor(props) { this.props = { ...DEFAULT_ANIMATION_LOOP_PROPS, ...props }; props = this.props; if (!props.device) { throw new Error("No device provided"); } const { useDevicePixels = true } = this.props; this.stats = props.stats || new Stats({ id: "animation-loop-stats" }); this.cpuTime = this.stats.get("CPU Time"); this.gpuTime = this.stats.get("GPU Time"); this.frameRate = this.stats.get("Frame Rate"); this.setProps({ autoResizeViewport: props.autoResizeViewport, autoResizeDrawingBuffer: props.autoResizeDrawingBuffer, useDevicePixels }); 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); } /** @deprecated Use .destroy() */ delete() { this.destroy(); } setError(error) { this.props.onError(error); this._error = Error(); const canvas2 = this.device?.canvasContext?.canvas; if (canvas2 instanceof HTMLCanvasElement) { const errorDiv = document.createElement("h1"); errorDiv.innerHTML = error.message; errorDiv.style.position = "absolute"; errorDiv.style.top = "20%"; errorDiv.style.left = "10px"; errorDiv.style.color = "black"; errorDiv.style.backgroundColor = "red"; document.body.appendChild(errorDiv); } } /** Flags this animation loop as needing redraw */ setNeedsRedraw(reason) { this.needsRedraw = this.needsRedraw || reason; return this; } /** TODO - move these props to CanvasContext? */ setProps(props) { if ("autoResizeViewport" in props) { this.props.autoResizeViewport = props.autoResizeViewport || false; } if ("autoResizeDrawingBuffer" in props) { this.props.autoResizeDrawingBuffer = props.autoResizeDrawingBuffer || false; } if ("useDevicePixels" in props) { this.props.useDevicePixels = props.useDevicePixels || 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(); 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; } return this; } /** Explicitly draw a frame */ redraw() { if (this.device?.isLost || this._error) { return this; } this._beginFrameTimers(); 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._resizeCanvasDrawingBuffer(); this._resizeViewport(); } _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() { if (!this._running) { return; } this.redraw(); 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._resizeCanvasDrawingBuffer(); this._resizeViewport(); } // Initialize the object that will be passed to app callbacks _initializeAnimationProps() { const canvas2 = this.device?.canvasContext?.canvas; if (!this.device || !canvas2) { throw new Error("loop"); } this.animationProps = { animationLoop: this, device: this.device, canvas: canvas2, timeline: this.timeline, // Initial values useDevicePixels: this.props.useDevicePixels, 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.canvasContext?.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?.canvasContext?.getPixelSize() || [1, 1]; let aspect = 1; const canvas2 = this.device?.canvasContext?.canvas; if (canvas2 && canvas2.clientHeight) { aspect = canvas2.clientWidth / canvas2.clientHeight; } else if (width > 0 && height > 0) { aspect = width / height; } return { width, height, aspect }; } /** 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 ); } } /** * Resize the render buffer of the canvas to match canvas client size * Optionally multiplying with devicePixel ratio */ _resizeCanvasDrawingBuffer() { if (this.props.autoResizeDrawingBuffer) { this.device?.canvasContext?.resize({ useDevicePixels: this.props.useDevicePixels }); } } _beginFrameTimers() { this.frameRate.timeEnd(); this.frameRate.timeStart(); this.cpuTime.timeStart(); } _endFrameTimers() { this.cpuTime.timeEnd(); } // 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; } }; // 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) { renderLoop = new AnimationLoopTemplateCtor(animationProps); return await renderLoop?.onInitialize(animationProps); }, onRender: (animationProps) => renderLoop?.onRender(animationProps), onFinalize: (animationProps) => renderLoop?.onFinalize(animationProps) }); animationLoop.getInfo = () => { return this.AnimationLoopTemplateCtor.info; }; return animationLoop; } // src/model/model.ts var import_core7 = __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 "COLOR_0": name = "colors"; break; } if (attribute) { attributes[name] = device.createBuffer({ data: attribute.value, id: `${attributeName}-buffer` }); const { value, size, normalized } = attribute; bufferLayout.push({ name, format: (0, import_core3.getVertexFormatFromAttribute)(value, size, normalized) }); } } const vertexCount = geometry._calculateVertexCount(geometry.attributes, geometry.indices); return { attributes, bufferLayout, vertexCount }; } // src/factories/pipeline-factory.ts var import_core4 = __toESM(require_core(), 1); var _PipelineFactory = class { /** Get the singleton default pipeline factory for the specified device */ static getDefaultPipelineFactory(device) { device._lumaData.defaultPipelineFactory = device._lumaData.defaultPipelineFactory || new _PipelineFactory(device); return device._lumaData.defaultPipelineFactory; } device; destroyPolicy; _hashCounter = 0; _hashes = {}; _renderPipelineCache = {}; _computePipelineCache = {}; constructor(device) { this.device = device; this.destroyPolicy = device.props._factoryDestroyPolicy; } /** Return a RenderPipeline matching props. Reuses a similar pipeline if already created. */ createRenderPipeline(props) { const allProps = { ...import_core4.RenderPipeline.defaultProps, ...props }; const hash = this._hashRenderPipeline(allProps); if (!this._renderPipelineCache[hash]) { const pipeline = this.device.createRenderPipeline({ ...allProps, id: allProps.id ? `${allProps.id}-cached` : void 0 }); pipeline.hash = hash; this._renderPipelineCache[hash] = { pipeline, useCount: 0 }; } this._renderPipelineCache[hash].useCount++; return this._renderPipelineCache[hash].pipeline; } createComputePipeline(props) { const allProps = { ...import_core4.ComputePipeline.defaultProps, ...props }; const hash = this._hashComputePipeline(allProps); if (!this._computePipelineCache[hash]) { const pipeline = this.device.createComputePipeline({ ...allProps, id: allProps.id ? `${allProps.id}-cached` : void 0 }); pipeline.hash = hash; this._computePipelineCache[hash] = { pipeline, useCount: 0 }; } this._computePipelineCache[hash].useCount++; return this._computePipelineCache[hash].pipeline; } release(pipeline) { const hash = pipeline.hash; const cache = pipeline instanceof import_core4.ComputePipeline ? this._computePipelineCache : this._renderPipelineCache; cache[hash].useCount--; if (cache[hash].useCount === 0) { if (this.destroyPolicy === "unused") { cache[hash].pipeline.destroy(); delete cache[hash]; } } } // PRIVATE _hashComputePipeline(props) { const shaderHash = this._getHash(props.shader.source); return `${shaderHash}`; } /** Calculate a hash based on all the inputs for a render pipeline */ _hashRenderPipeline(props) { const vsHash = props.vs ? this._getHash(props.vs.source) : 0; const fsHash = props.fs ? this._getHash(props.fs.source) : 0; const varyingHash = "-"; const bufferLayoutHash = this._getHash(JSON.stringify(props.bufferLayout)); switch (this.device.type) { case "webgl": return `${vsHash}/${fsHash}V${varyingHash}BL${bufferLayoutHash}`; default: const parameterHash = this._getHash(JSON.stringify(props.parameters)); return `${vsHash}/${fsHash}V${varyingHash}T${props.topology}P${parameterHash}BL${bufferLayoutHash}`; } } _getHash(key) { if (this._hashes[key] === void 0) { this._hashes[key] = this._hashCounter++; } return this._hashes[key]; } }; var PipelineFactory = _PipelineFactory; __publicField(PipelineFactory, "defaultProps", { ...import_core4.RenderPipeline.defaultProps }); // src/factories/shader-factory.ts var import_core5 = __toESM(require_core(), 1); var _ShaderFactory = class { /** Returns the default ShaderFactory for the given {@link Device}, creating one if necessary. */ static getDefaultShaderFactory(device) { device._lumaData.defaultShaderFactory ||= new _ShaderFactory(device); return device._lumaData.defaultShaderFactory; } device; destroyPolicy; _cache = {}; /** @internal */ constructor(device) { this.device = device; this.destroyPolicy = device.props._factoryDestroyPolicy; } /** Requests a {@link Shader} from the cache, creating a new Shader only if necessary. */ createShader(props) { const key = this._hashShader(props); let cacheEntry = this._cache[key]; if (!cacheEntry) { const shader = this.device.createShader({ ...props, id: props.id ? `${props.id}-cached` : void 0 }); this._cache[key] = cacheEntry = { shader, useCount: 0 }; } cacheEntry.useCount++; return cacheEntry.shader; } /** Releases a previously-requested {@link Shader}, destroying it if no users remain. */ release(shader) { const key = this._hashShader(shader); const cacheEntry = this._cache[key]; if (cacheEntry) { cacheEntry.useCount--; if (cacheEntry.useCount === 0) { if (this.destroyPolicy === "unused") { delete this._cache[key]; cacheEntry.shader.destroy(); } } } } // PRIVATE _hashShader(value) { return `${value.stage}:${value.source}`; } }; var ShaderFactory = _ShaderFactory; __publicField(ShaderFactory, "defaultProps", { ...import_core5.Shader.defaultProps }); // 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 canvas = null; var ctx = null; function debugFramebuffer(fbo, { id, minimap, opaque, top = "0", left = "0", rgbaScale = 1 }) { if (!canvas) { canvas = document.createElement("canvas"); canvas.id = id; canvas.title = id; canvas.style.zIndex = "100"; canvas.style.position = "absolute"; canvas.style.top = top; canvas.style.left = left; canvas.style.border = "blue 5px solid"; canvas.style.transform = "scaleY(-1)"; document.body.appendChild(canvas); ctx = canvas.getContext("2d"); } if (canvas.width !== fbo.width || canvas.height !== fbo.height) { canvas.width = fbo.width / 2; canvas.height = fbo.height / 2; canvas.style.width = "400px"; canvas.style.height = "400px"; } const color = fbo.device.readPixelsToArrayWebGL(fbo); const imageData = ctx?.createImageData(fbo.width, fbo.height); if (imageData) { const offset = 0; for (let i = 0; i < color.length; i += 4) { imageData.data[offset + i + 0] = color[i + 0] * rgbaScale; imageData.data[offset + i + 1] = color[i + 1] * rgbaScale; imageData.data[offset + i + 2] = color[i + 2] * rgbaScale; imageData.data[offset + i + 3] = opaque ? 255 : color[i + 3] * rgbaScale; } ctx?.putImageData(imageData, 0, 0); } } // 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/shader-inputs.ts var import_core6 = __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) { const result = { bindings: {}, uniforms: {} }; Object.keys(uniforms).forEach((name) => { const uniform = uniforms[name]; if (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((module) => module.dependencies) ); for (const resolvedModule of resolvedModules) { modules[resolvedModule.name] = resolvedModule; } import_core6.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)) { this._addModule(module); if (module.name && name !== module.name && !this.options.disableWarnings) { import_core6.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_core6.log.warn(`Module ${name} not found`)(); } continue; } const oldUniforms = this.moduleUniforms[moduleName]; const oldBindings = this.moduleBindings[moduleName]; const uniformsAndBindings = module.getUniforms?.(moduleProps, oldUniforms) || moduleProps; const { uniforms, bindings } = splitUniformsAndBindings(uniformsAndBindings); this.moduleUniforms[moduleName] = { ...oldUniforms, ...uniforms }; 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] = module.defaultUniforms || {}; this.moduleBindings[moduleName] = {}; } }; // src/application-utils/load-file.ts var pathPrefix = ""; function setPathPrefix(prefix) { pathPrefix = prefix; } async function loadImageBitmap(url, opts) { const image = new Image(); image.crossOrigin = opts?.crossOrigin || "anonymous"; image.src = url.startsWith("http") ? url : pathPrefix + url; await image.decode(); return opts ? await createImageBitmap(image, opts) : await createImageBitmap(image); } async function loadImage(url, opts) { return await new Promise((resolve, reject) => { try { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error(`Could not load image ${url}.`)); image.crossOrigin = opts?.crossOrigin || "anonymous"; image.src = url.startsWith("http") ? url : pathPrefix + url; } catch (error) { reject(error); } }); } // src/async-texture/async-texture.ts var AsyncTexture = class { device; id; // TODO - should we type these as possibly `null`? It will make usage harder? // @ts-expect-error texture; // @ts-expect-error sampler; // @ts-expect-error view; ready; isReady = false; destroyed = false; resolveReady = () => { }; rejectReady = () => { }; get [Symbol.toStringTag]() { return "AsyncTexture"; } toString() { return `AsyncTexture:"${this.id}"(${this.isReady ? "ready" : "loading"})`; } constructor(device, props) { this.device = device; this.id = props.id || uid("async-texture"); if (typeof props?.data === "string" && props.dimension === "2d") { props = { ...props, data: loadImageBitmap(props.data) }; } this.ready = new Promise((resolve, reject) => { this.resolveReady = () => { this.isReady = true; resolve(); }; this.rejectReady = reject; }); this.initAsync(props); } async initAsync(props) { const asyncData = props.data; let data; try { data = await awaitAllPromises(asyncData); } catch (error) { this.rejectReady(error); } if (this.destroyed) { return; } const syncProps = { ...props, data }; this.texture = this.device.createTexture(syncProps); this.sampler = this.texture.sampler; this.view = this.texture.view; this.isReady = true; this.resolveReady(); } destroy() { if (this.texture) { this.texture.destroy(); this.texture = null; } this.destroyed = true; } /** * Textures are immutable and cannot be resized after creation, * but we can create a similar texture with the same parameters but a new size. * @note Does not copy contents of the texture * @todo Abort pending promise and create a texture with the new size? */ resize(size) { if (!this.isReady) { throw new Error("Cannot resize texture before it is ready"); } if (size.width === this.texture.width && size.height === this.texture.height) { return false; } if (this.texture) { const texture = this.texture; this.texture = texture.clone(size); texture.destroy(); } return true; } }; async function awaitAllPromises(x) { x = await x; if (Array.isArray(x)) { return await Promise.all(x.map(awaitAllPromises)); } if (x && typeof x === "object" && x.constructor === Object) { const object = x; const values = await Promise.all(Object.values(object)); const keys = Object.keys(object); const resolvedObject = {}; for (let i = 0; i < keys.length; i++) { resolvedObject[keys[i]] = values[i]; } return resolvedObject; } return x; } // src/model/model.ts var LOG_DRAW_PRIORITY = 2; var LOG_DRAW_TIMEOUT = 1e4; var _Model = class { device; id; // @ts-expect-error assigned in function called from constructor source; // @ts-expect-error assigned in function called from constructor vs; // @ts-expect-error assigned in function called from constructor fs; pipelineFactory; shaderFactory; userData = {}; // Fixed properties (change can trigger pipeline rebuild) /** The render pipeline GPU parameters, depth testing etc */ parameters; /** The primitive topology */ topology; /** Buffer layout */ bufferLayout; // Dynamic properties /** Use instanced rendering */ isInstanced = void 0; /** instance count. `undefined` means not instanced */ instanceCount = 0; /** Vertex count */ vertexCount; /** Index buffer */ indexBuffer = null; /** Buffer-valued attributes */ bufferAttributes = {}; /** Constant-valued attributes */ constantAttributes = {}; /** Bindings (textures, samplers, uniform buffers) */