@luma.gl/engine
Version:
3D Engine Components for luma.gl
182 lines (155 loc) • 5.21 kB
text/typescript
// luma.gl, MIT license
// Copyright (c) vis.gl contributors
import type {
Texture,
TextureProps,
Sampler,
TextureView,
Device,
Texture1DData,
Texture2DData,
Texture3DData,
TextureArrayData,
TextureCubeData,
TextureCubeArrayData
} from '@luma.gl/core';
import {loadImageBitmap} from '../application-utils/load-file';
import {uid} from '../utils/uid';
export type AsyncTextureProps = Omit<TextureProps, 'data'> & AsyncTextureDataProps;
type AsyncTextureDataProps =
| AsyncTexture1DProps
| AsyncTexture2DProps
| AsyncTexture3DProps
| AsyncTextureArrayProps
| AsyncTextureCubeProps
| AsyncTextureCubeArrayProps;
type AsyncTexture1DProps = {dimension: '1d'; data: Promise<Texture1DData> | Texture1DData | null};
type AsyncTexture2DProps = {dimension?: '2d'; data: Promise<Texture2DData> | Texture2DData | null};
type AsyncTexture3DProps = {dimension: '3d'; data: Promise<Texture3DData> | Texture3DData | null};
type AsyncTextureArrayProps = {
dimension: '2d-array';
data: Promise<TextureArrayData> | TextureArrayData | null;
};
type AsyncTextureCubeProps = {
dimension: 'cube';
data: Promise<TextureCubeData> | TextureCubeData | null;
};
type AsyncTextureCubeArrayProps = {
dimension: 'cube-array';
data: Promise<TextureCubeArrayData> | TextureCubeArrayData | null;
};
type TextureData = TextureProps['data'];
type AsyncTextureData = AsyncTextureProps['data'];
/**
* It is very convenient to be able to initialize textures with promises
* This can add considerable complexity to the Texture class, and doesn't
* fit with the immutable nature of WebGPU resources.
* Instead, luma.gl offers async textures as a separate class.
*/
export class AsyncTexture {
readonly device: Device;
readonly id: string;
// TODO - should we type these as possibly `null`? It will make usage harder?
// @ts-expect-error
texture: Texture;
// @ts-expect-error
sampler: Sampler;
// @ts-expect-error
view: TextureView;
readonly ready: Promise<void>;
isReady: boolean = false;
destroyed: boolean = false;
protected resolveReady: () => void = () => {};
protected rejectReady: (error: Error) => void = () => {};
get [Symbol.toStringTag]() {
return 'AsyncTexture';
}
toString(): string {
return `AsyncTexture:"${this.id}"(${this.isReady ? 'ready' : 'loading'})`;
}
constructor(device: Device, props: AsyncTextureProps) {
this.device = device;
this.id = props.id || uid('async-texture');
// this.id = typeof props?.data === 'string' ? props.data.slice(-20) : uid('async-texture');
// Signature: new AsyncTexture(device, {data: url})
if (typeof props?.data === 'string' && props.dimension === '2d') {
props = {...props, data: loadImageBitmap(props.data)};
}
this.ready = new Promise<void>((resolve, reject) => {
this.resolveReady = () => {
this.isReady = true;
resolve();
};
this.rejectReady = reject;
});
this.initAsync(props);
}
async initAsync(props: AsyncTextureProps): Promise<void> {
const asyncData: AsyncTextureData = props.data;
let data: TextureData;
try {
data = await awaitAllPromises(asyncData);
} catch (error) {
this.rejectReady(error as Error);
}
// Check that we haven't been destroyed while waiting for texture data to load
if (this.destroyed) {
return;
}
// Now we can actually create the texture
// @ts-expect-error Discriminated union
const syncProps: TextureProps = {...props, data};
this.texture = this.device.createTexture(syncProps);
this.sampler = this.texture.sampler;
this.view = this.texture.view;
this.isReady = true;
this.resolveReady();
}
destroy(): void {
if (this.texture) {
this.texture.destroy();
// @ts-expect-error
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: {width: number; height: number}): boolean {
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;
}
}
// HELPERS
/** Resolve all promises in a nested data structure */
async function awaitAllPromises(x: any): Promise<any> {
x = await x;
if (Array.isArray(x)) {
return await Promise.all(x.map(awaitAllPromises));
}
if (x && typeof x === 'object' && x.constructor === Object) {
const object: Record<string, any> = x;
const values = await Promise.all(Object.values(object));
const keys = Object.keys(object);
const resolvedObject: Record<string, any> = {};
for (let i = 0; i < keys.length; i++) {
resolvedObject[keys[i]] = values[i];
}
return resolvedObject;
}
return x;
}