@luma.gl/engine
Version:
3D Engine Components for luma.gl
1,563 lines (1,539 loc) • 138 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
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 __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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
// dist/index.js
var dist_exports = {};
__export(dist_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
});
module.exports = __toCommonJS(dist_exports);
// dist/animation/timeline.js
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;
}
}
};
// dist/animation/key-frames.js
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);
}
};
// dist/animation-loop/animation-loop-template.js
var AnimationLoopTemplate = class {
constructor(animationProps) {
}
async onInitialize(animationProps) {
return null;
}
};
// dist/animation-loop/animation-loop.js
var import_core = require("@luma.gl/core");
// dist/animation-loop/request-animation-frame.js
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);
}
// dist/animation-loop/animation-loop.js
var import_stats = require("@probe.gl/stats");
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 import_stats.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) {
var _a, _b;
this.props.onError(error);
this._error = Error();
const canvas2 = (_b = (_a = this.device) == null ? void 0 : _a.canvasContext) == null ? void 0 : _b.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() {
var _a;
if (((_a = this.device) == null ? void 0 : _a.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) {
var _a;
if (this.display) {
this.display._renderFrame(animationProps);
return;
}
this.props.onRender(this._getAnimationProps());
(_a = this.device) == null ? void 0 : _a.submit();
}
_clearNeedsRedraw() {
this.needsRedraw = false;
}
_setupFrame() {
this._resizeCanvasDrawingBuffer();
this._resizeViewport();
}
// Initialize the object that will be passed to app callbacks
_initializeAnimationProps() {
var _a, _b;
const canvas2 = (_b = (_a = this.device) == null ? void 0 : _a.canvasContext) == null ? void 0 : _b.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() {
var _a;
this.device = await this.props.device;
if (!this.device) {
throw new Error("No device provided");
}
this.canvas = ((_a = this.device.canvasContext) == null ? void 0 : _a.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() {
var _a, _b, _c, _d;
if (!this.device) {
return { width: 1, height: 1, aspect: 1 };
}
const [width, height] = ((_b = (_a = this.device) == null ? void 0 : _a.canvasContext) == null ? void 0 : _b.getPixelSize()) || [1, 1];
let aspect = 1;
const canvas2 = (_d = (_c = this.device) == null ? void 0 : _c.canvasContext) == null ? void 0 : _d.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() {
var _a, _b;
if (this.props.autoResizeDrawingBuffer) {
(_b = (_a = this.device) == null ? void 0 : _a.canvasContext) == null ? void 0 : _b.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;
}
};
// dist/animation-loop/make-animation-loop.js
var import_core2 = require("@luma.gl/core");
function makeAnimationLoop(AnimationLoopTemplateCtor, props) {
let renderLoop = null;
const device = (props == null ? void 0 : props.device) || import_core2.luma.createDevice({ id: "animation-loop", adapters: props == null ? void 0 : props.adapters, createCanvasContext: true });
const animationLoop = new AnimationLoop({
...props,
device,
async onInitialize(animationProps) {
renderLoop = new AnimationLoopTemplateCtor(animationProps);
return await (renderLoop == null ? void 0 : renderLoop.onInitialize(animationProps));
},
onRender: (animationProps) => renderLoop == null ? void 0 : renderLoop.onRender(animationProps),
onFinalize: (animationProps) => renderLoop == null ? void 0 : renderLoop.onFinalize(animationProps)
});
animationLoop.getInfo = () => {
return this.AnimationLoopTemplateCtor.info;
};
return animationLoop;
}
// dist/model/model.js
var import_core7 = require("@luma.gl/core");
var import_shadertools2 = require("@luma.gl/shadertools");
// dist/geometry/gpu-geometry.js
var import_core3 = require("@luma.gl/core");
// dist/utils/uid.js
var uidCounters = {};
function uid(id = "id") {
uidCounters[id] = uidCounters[id] || 1;
const count = uidCounters[id]++;
return `${id}-${count}`;
}
// dist/geometry/gpu-geometry.js
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() {
var _a;
(_a = this.indices) == null ? void 0 : _a.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 };
}
// dist/factories/pipeline-factory.js
var import_core4 = require("@luma.gl/core");
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 });
// dist/factories/shader-factory.js
var import_core5 = require("@luma.gl/core");
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 });
// dist/debug/debug-shader-layout.js
function getDebugTableForShaderLayout(layout, name) {
var _a;
const table = {};
const header = "Values";
if (layout.attributes.length === 0 && !((_a = layout.varyings) == null ? void 0 : _a.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;
}
// dist/debug/debug-framebuffer.js
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 == null ? void 0 : 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 == null ? void 0 : ctx.putImageData(imageData, 0, 0);
}
}
// dist/utils/deep-equal.js
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;
}
// dist/shader-inputs.js
var import_core6 = require("@luma.gl/core");
var import_shadertools = require("@luma.gl/shadertools");
// dist/model/split-uniforms-and-bindings.js
var import_types = require("@math.gl/types");
function isUniformValue(value) {
return (0, import_types.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;
}
// dist/shader-inputs.js
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((module2) => module2.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, module2] of Object.entries(modules)) {
this._addModule(module2);
if (module2.name && name !== module2.name && !this.options.disableWarnings) {
import_core6.log.warn(`Module name: ${name} vs ${module2.name}`)();
}
}
}
/** Destroy */
destroy() {
}
/**
* Set module props
*/
setProps(props) {
var _a;
for (const name of Object.keys(props)) {
const moduleName = name;
const moduleProps = props[moduleName] || {};
const module2 = this.modules[moduleName];
if (!module2) {
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 = ((_a = module2.getUniforms) == null ? void 0 : _a.call(module2, 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() {
var _a;
const table = {};
for (const [moduleName, module2] of Object.entries(this.moduleUniforms)) {
for (const [key, value] of Object.entries(module2)) {
table[`${moduleName}.${key}`] = {
type: (_a = this.modules[moduleName].uniformTypes) == null ? void 0 : _a[key],
value: String(value)
};
}
}
return table;
}
_addModule(module2) {
const moduleName = module2.name;
this.moduleUniforms[moduleName] = module2.defaultUniforms || {};
this.moduleBindings[moduleName] = {};
}
};
// dist/application-utils/load-file.js
var pathPrefix = "";
function setPathPrefix(prefix) {
pathPrefix = prefix;
}
async function loadImageBitmap(url, opts) {
const image = new Image();
image.crossOrigin = (opts == null ? void 0 : 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 == null ? void 0 : opts.crossOrigin) || "anonymous";
image.src = url.startsWith("http") ? url : pathPrefix + url;
} catch (error) {
reject(error);
}
});
}
// dist/async-texture/async-texture.js
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 == null ? void 0 : 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;
}
// dist/model/model.js
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) */
bindings = {};
/** Sets uniforms @deprecated Use uniform buffers and setBindings() for portability*/
uniforms = {};
/**
* VertexArray
* @note not implemented: if bufferLayout is updated, vertex array has to be rebuilt!
* @todo - allow application to define multiple vertex arrays?
* */
vertexArray;
/** TransformFeedback, WebGL 2 only. */
transformFeedback = null;
/** The underlying GPU "program". @note May be recreated if parameters change */
pipeline;
/** ShaderInputs instance */
// @ts-expect-error Assigned in function called by constructor
shaderInputs;
// @ts-expect-error Assigned in function called by constructor
_uniformStore;
_attributeInfos = {};
_gpuGeometry = null;
_getModuleUniforms;
props;
_pipelineNeedsUpdate = "newly created";
_needsRedraw = "initializing";
_destroyed = false;
/** "Time" of last draw. Monotonically increasing timestamp */
_lastDrawTimestamp = -1;
get [Symbol.toStringTag]() {
return "Model";
}
toString() {
return `Model(${this.id})`;
}
constructor(device, props) {
var _a, _b, _c;
this.props = { ..._Model.defaultProps, ...props };
props = this.props;
this.id = props.id || uid("model");
this.device = device;
Object.assign(this.userData, props.userData);
const moduleMap = Object.fromEntries(((_a = this.props.modules) == null ? void 0 : _a.map((module2) => [module2.name, module2])) || []);
const shaderInputs = props.shaderInputs || new ShaderInputs(moduleMap, { disableWarnings: this.props.disableWarnings });
this.setShaderInputs(shaderInputs);
const platformInfo = getPlatformInfo(device);
const modules = (
// @ts-ignore shaderInputs is assigned in setShaderInputs above.
(((_b = this.props.modules) == null ? void 0 : _b.length) > 0 ? this.props.modules : (_c = this.shaderInputs) == null ? void 0 : _c.getModules()) || []
);
const isWebGPU = this.device.type === "webgpu";
if (isWebGPU && this.props.source) {
const { source: source3, getUniforms: getUniforms2 } = this.props.shaderAssembler.assembleWGSLShader({
platformInfo,
...this.props,
modules
});
this.source = source3;
this._getModuleUniforms = getUniforms2;
this.props.shaderLayout ||= (0, import_shadertools2.getShaderLayoutFromWGSL)(this.source);
} else {
const { vs: vs3, fs: fs3, getUniforms: getUniforms2 } = this.props.shaderAssembler.assembleGLSLShaderPair({
platformInfo,
...this.props,
modules
});
this.vs = vs3;
this.fs = fs3;
this._getModuleUniforms = getUniforms2;
}
this.vertexCount = this.props.vertexCount;
this.instanceCount = this.props.instanceCount;
this.topology = this.props.topology;
this.bufferLayout = this.props.bufferLayout;
this.parameters = this.props.parameters;
if (props.geometry) {
this.setGeometry(props.geometry);
}
this.pipelineFactory = props.pipelineFactory || PipelineFactory.getDefaultPipelineFactory(this.device);
this.shaderFactory = props.shaderFactory || ShaderFactory.getDefaultShaderFactory(this.device);
this.pipeline = this._updatePipeline();
this.vertexArray = device.createVertexArray({
renderPipeline: this.pipeline
});
if (this._gpuGeometry) {
this._setGeometryAttributes(this._gpuGeometry);
}
if ("isInstanced" in props) {
this.isInstanced = props.isInstanced;
}
if (props.instanceCount) {
this.setInstanceCount(props.instanceCount);
}
if (props.vertexCount) {
this.setVertexCount(props.vertexCount);
}
if (props.indexBuffer) {
this.setIndexBuffer(props.indexBuffer);
}
if (props.attributes) {
this.setAttributes(props.attributes);
}
if (props.constantAttributes) {
this.setConstantAttributes(props.constantAttributes);
}
if (props.bindings) {
this.setBindings(props.bindings);
}
if (props.uniforms) {
this.setUniformsWebGL(props.uniforms);
}
if (props.moduleSettings) {
this.updateModuleSettingsWebGL(props.moduleSettings);
}
if (props.transformFeedback) {
this.transformFeedback = props.transformFeedback;
}
Object.seal(this);
}
destroy() {
var _a;
if (this._destroyed)
return;
this.pipelineFactory.release(this.pipeline);
this.shaderFactory.release(this.pipeline.vs);
if (this.pipeline.fs) {
this.shaderFactory.release(this.pipeline.fs);
}
this._uniformStore.destroy();
(_a = this._gpuGeometry) == null ? void 0 : _a.destroy();
this._destroyed = true;
}
// Draw call
/** Query redraw status. Clears the status. */
needsRedraw() {
if (this._getBindingsUpdateTimestamp() > this._lastDrawTimestamp) {
this.setNeedsRedraw("contents of bound textures or buffers updated");
}
const needsRedraw = this._needsRedraw;
this._needsRedraw = false;
return needsRedraw;
}
/** Mark the model as needing a redraw */
setNeedsRedraw(reason) {
this._needsRedraw ||= reason;
}
predraw() {
this.updateShaderInputs();
this.pipeline = this._updatePipeline();
}
draw(renderPass) {
const loadingBinding = this._areBindingsLoading();
if (loadingBinding) {
import_core7.log.info(LOG_DRAW_PRIORITY, `>>> DRAWING ABORTED ${this.id}: ${loadingBinding} not loaded`)();
return false;
}
try {
renderPass.pushDebugGroup(`${this}.predraw(${renderPass})`);
this.predraw();
} finally {
renderPass.popDebugGroup();
}
let drawSuccess;
try {
renderPass.pushDebugGroup(`${this}.draw(${renderPass})`);
this._logDrawCallStart();
this.pipeline = this._updatePipeline();
const syncBindings = this._getBindings();
this.pipeline.setBindings(syncBindings, {
disableWarnings: this.props.disableWarnings
});
if (!isObjectEmpty(this.uniforms)) {
this.pipeline.setUniformsWebGL(this.uniforms);
}
const { indexBuffer } = this.vertexArray;
const indexCount = indexBuffer ? indexBuffer.byteLength / (indexBuffer.indexType === "uint32" ? 4 : 2) : void 0;
drawSuccess = this.pipeline.draw({
renderPass,
vertexArray: this.vertexArray,
isInstanced: this.isInstanced,
vertexCount: this.vertexCount,
instanceCount: this.instanceCount,
indexCount,
transformFeedback: this.transformFeedback || void 0,
// WebGL shares underlying cached pipelines even for models that have different parameters and topology,
// so we must provide our unique parameters to each draw
// (In WebGPU most parameters are encoded in the pipeline and cannot be changed per draw call)
parameters: this.parameters,
topology: this.topology
});
} finally {
renderPass.popDebugGroup();
this._logDrawCallEnd();
}
this._logFramebuffer(renderPass);
if (drawSuccess) {
this._lastDrawTimestamp = this.device.timestamp;
this._needsRedraw = false;
} else {
this._needsRedraw = "waiting for resource initialization";
}
return drawSuccess;
}
// Update fixed fields (can trigger pipeline rebuild)
/**
* Updates the optional geometry
* Geometry, set topology and bufferLayout
* @note Can trigger a pipeline rebuild / pipeline cache fetch on WebGPU
*/
setGeometry(geometry) {
var _a;
(_a = this._gpuGeometry) == null ? void 0 : _a.destroy();
const gpuGeometry = geometry && makeGPUGeometry(this.device, geometry);
if (gpuGeometry) {
this.setTopology(gpuGeometry.topology || "triangle-list");
const bufferLayoutHelper = new import_core7._BufferLayoutHelper(this.bufferLayout);
this.bufferLayout = bufferLayoutHelper.mergeBufferLayouts(gpuGeometry.bufferLayout, this.bufferLayout);
if (this.vertexArray) {
this._setGeometryAttributes(gpuGeometry);
}
}
this._gpuGeometry = gpuGeometry;
}
/**
* Updates the primitive topology ('triangle-list', 'triangle-strip' etc).
* @note Triggers a pipeline rebuild / pipeline cache fetch on WebGPU
*/
setTopology(topology) {
if (topology !== this.topology) {
this.topology = topology;
this._setPipelineNeedsUpdate("topology");
}
}
/**
* Updates the buffer layout.
* @note Triggers a pipeline rebuild / pipeline cache fetch
*/
setBufferLayout(bufferLayout) {
const bufferLayoutHelper = new import_core7._BufferLayoutHelper(this.bufferLayout);
this.bufferLayout = this._gpuGeometry ? bufferLayoutHelper.mergeBufferLayouts(bufferLayout, this._gpuGeometry.bufferLayout) : bufferLayout;
this._setPipelineNeedsUpdate("bufferLayout");
this.pipeline = this._updatePipeline();
this.vertexArray = this.device.createVertexArray({
renderPipeline: this.pipeline