@luma.gl/engine
Version:
3D Engine Components for luma.gl
1,559 lines (1,529 loc) • 207 kB
JavaScript
(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) */