mirror of
https://github.com/visgl/luma.gl.git
synced 2026-02-01 14:33:49 +00:00
test(engine): add ShaderPassRenderer test (#2437)
This commit is contained in:
parent
8314ecefac
commit
f256de326f
@ -13,7 +13,7 @@
|
||||
import {webgpuAdapter} from '@luma.gl/webgpu';
|
||||
import {makeAnimationLoop} from '@luma.gl/engine';
|
||||
import AnimationLoopTemplate from './app.ts';
|
||||
const animationLoop = makeAnimationLoop(AnimationLoopTemplate, {adapters: [webgpuAdapter, webgl2Adapter]});
|
||||
const animationLoop = makeAnimationLoop(AnimationLoopTemplate, {adapters: [webgl2Adapter]});
|
||||
animationLoop.start();
|
||||
</script>
|
||||
<body>
|
||||
|
||||
@ -201,6 +201,8 @@ export {getTextureImageView, setTextureImageData} from './shadertypes/textures/t
|
||||
// export {TexturePacker} from './shadertypes/textures/texture-packer'
|
||||
export {type PixelData, readPixel, writePixel} from './shadertypes/textures/pixel-utils';
|
||||
|
||||
export {isExternalImage, getExternalImageSize} from './image-utils/image-types';
|
||||
|
||||
// GENERAL EXPORTS - FOR APPLICATIONS
|
||||
|
||||
export type {StatsManager} from './utils/stats-manager'; // TODO - should this be moved to probe.gl?
|
||||
|
||||
@ -12,12 +12,15 @@ import {Device, Resource, Buffer, Framebuffer, Texture} from '@luma.gl/core';
|
||||
* @note the two resources can be destroyed by calling `destroy()`
|
||||
*/
|
||||
export class Swap<T extends Resource<any>> {
|
||||
id: string;
|
||||
|
||||
/** The current resource - usually the source for renders or computations */
|
||||
current: T;
|
||||
/** The next resource - usually the target/destination for transforms / computations */
|
||||
next: T;
|
||||
|
||||
constructor(props: {current: T; next: T}) {
|
||||
constructor(props: {current: T; next: T; id?: string}) {
|
||||
this.id = props.id || 'swap';
|
||||
this.current = props.current;
|
||||
this.next = props.next;
|
||||
}
|
||||
@ -47,6 +50,7 @@ export class SwapFramebuffers extends Swap<Framebuffer> {
|
||||
typeof colorAttachment !== 'string'
|
||||
? colorAttachment
|
||||
: device.createTexture({
|
||||
id: `${props.id}-texture-0`,
|
||||
format: colorAttachment,
|
||||
usage: Texture.SAMPLE | Texture.RENDER | Texture.COPY_SRC | Texture.COPY_DST,
|
||||
width,
|
||||
@ -60,9 +64,9 @@ export class SwapFramebuffers extends Swap<Framebuffer> {
|
||||
typeof colorAttachment !== 'string'
|
||||
? colorAttachment
|
||||
: device.createTexture({
|
||||
id: `${props.id}-texture-1`,
|
||||
format: colorAttachment,
|
||||
usage:
|
||||
Texture.TEXTURE | Texture.COPY_SRC | Texture.COPY_DST | Texture.RENDER_ATTACHMENT,
|
||||
usage: Texture.SAMPLE | Texture.RENDER | Texture.COPY_SRC | Texture.COPY_DST,
|
||||
width,
|
||||
height
|
||||
})
|
||||
|
||||
@ -1,105 +1,47 @@
|
||||
// luma.gl, MIT license
|
||||
// Copyright (c) vis.gl contributors
|
||||
|
||||
import type {
|
||||
TextureProps,
|
||||
SamplerProps,
|
||||
TextureView,
|
||||
Device,
|
||||
TypedArray,
|
||||
TextureFormat,
|
||||
ExternalImage
|
||||
} from '@luma.gl/core';
|
||||
import type {TextureProps, SamplerProps, TextureView, Device} from '@luma.gl/core';
|
||||
|
||||
import {Texture, Sampler, log} from '@luma.gl/core';
|
||||
|
||||
import {loadImageBitmap} from '../application-utils/load-file';
|
||||
// import {loadImageBitmap} from '../application-utils/load-file';
|
||||
import {uid} from '../utils/uid';
|
||||
import {
|
||||
// cube constants
|
||||
type TextureCubeFace,
|
||||
TEXTURE_CUBE_FACE_MAP,
|
||||
|
||||
type DynamicTextureDataProps =
|
||||
| DynamicTexture1DProps
|
||||
| DynamicTexture2DProps
|
||||
| DynamicTexture3DProps
|
||||
| DynamicTextureArrayProps
|
||||
| DynamicTextureCubeProps
|
||||
| DynamicTextureCubeArrayProps;
|
||||
// texture slice/mip data types
|
||||
type TextureSubresource,
|
||||
|
||||
type DynamicTexture1DProps = {dimension: '1d'; data: Promise<Texture1DData> | Texture1DData | null};
|
||||
type DynamicTexture2DProps = {
|
||||
dimension?: '2d';
|
||||
data: Promise<Texture2DData> | Texture2DData | null;
|
||||
};
|
||||
type DynamicTexture3DProps = {dimension: '3d'; data: Promise<Texture3DData> | Texture3DData | null};
|
||||
type DynamicTextureArrayProps = {
|
||||
dimension: '2d-array';
|
||||
data: Promise<TextureArrayData> | TextureArrayData | null;
|
||||
};
|
||||
type DynamicTextureCubeProps = {
|
||||
dimension: 'cube';
|
||||
data: Promise<TextureCubeData> | TextureCubeData | null;
|
||||
};
|
||||
type DynamicTextureCubeArrayProps = {
|
||||
dimension: 'cube-array';
|
||||
data: Promise<TextureCubeArrayData> | TextureCubeArrayData | null;
|
||||
};
|
||||
// props (dimension + data)
|
||||
type TextureDataProps,
|
||||
type TextureDataAsyncProps,
|
||||
|
||||
type DynamicTextureData = DynamicTextureProps['data'];
|
||||
// combined data for different texture types
|
||||
type Texture1DData,
|
||||
type Texture2DData,
|
||||
type Texture3DData,
|
||||
type TextureArrayData,
|
||||
type TextureCubeArrayData,
|
||||
type TextureCubeData,
|
||||
|
||||
/** Names of cube texture faces */
|
||||
export type TextureCubeFace = '+X' | '-X' | '+Y' | '-Y' | '+Z' | '-Z';
|
||||
export const TextureCubeFaces: TextureCubeFace[] = ['+X', '-X', '+Y', '-Y', '+Z', '-Z'];
|
||||
// prettier-ignore
|
||||
export const TextureCubeFaceMap = {'+X': 0, '-X': 1, '+Y': 2, '-Y': 3, '+Z': 4, '-Z': 5};
|
||||
// Helpers
|
||||
getTextureSizeFromData,
|
||||
getTexture1DSubresources,
|
||||
getTexture2DSubresources,
|
||||
getTexture3DSubresources,
|
||||
getTextureCubeSubresources,
|
||||
getTextureArraySubresources,
|
||||
getTextureCubeArraySubresources
|
||||
} from './texture-data';
|
||||
|
||||
/**
|
||||
* One mip level
|
||||
* Basic data structure is similar to `ImageData`
|
||||
* additional optional fields can describe compressed texture data.
|
||||
* Properties for a dynamic texture
|
||||
*/
|
||||
export type TextureImageData = {
|
||||
/** WebGPU style format string. Defaults to 'rgba8unorm' */
|
||||
format?: TextureFormat;
|
||||
data: TypedArray;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
compressed?: boolean;
|
||||
byteLength?: number;
|
||||
hasAlpha?: boolean;
|
||||
};
|
||||
|
||||
export type TextureLevelSource = TextureImageData | ExternalImage;
|
||||
|
||||
/** Texture data can be one or more mip levels */
|
||||
export type TextureData = TextureImageData | ExternalImage | (TextureImageData | ExternalImage)[];
|
||||
|
||||
/** @todo - define what data type is supported for 1D textures */
|
||||
export type Texture1DData = TypedArray | TextureImageData;
|
||||
|
||||
/** Texture data can be one or more mip levels */
|
||||
export type Texture2DData =
|
||||
| TypedArray
|
||||
| TextureImageData
|
||||
| ExternalImage
|
||||
| (TextureImageData | ExternalImage)[];
|
||||
|
||||
/** 6 face textures */
|
||||
export type TextureCubeData = Record<TextureCubeFace, TextureData>;
|
||||
|
||||
/** Array of textures */
|
||||
export type Texture3DData = TextureData[];
|
||||
|
||||
/** Array of textures */
|
||||
export type TextureArrayData = TextureData[];
|
||||
|
||||
/** Array of 6 face textures */
|
||||
export type TextureCubeArrayData = Record<TextureCubeFace, TextureData>[];
|
||||
|
||||
export const CubeFaces: TextureCubeFace[] = ['+X', '-X', '+Y', '-Y', '+Z', '-Z'];
|
||||
|
||||
/** Properties for an async texture */
|
||||
export type DynamicTextureProps = Omit<TextureProps, 'data' | 'mipLevels' | 'width' | 'height'> &
|
||||
DynamicTextureDataProps & {
|
||||
TextureDataAsyncProps & {
|
||||
/** Generate mipmaps after creating textures and setting data */
|
||||
mipmaps?: boolean;
|
||||
/** nipLevels can be set to 'auto' to generate max number of mipLevels */
|
||||
@ -111,429 +53,349 @@ export type DynamicTextureProps = Omit<TextureProps, 'data' | 'mipLevels' | 'wid
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Dynamic Textures
|
||||
*
|
||||
* - Mipmaps - DynamicTexture can generate mipmaps for textures (WebGPU does not provide built-in mipmap generation).
|
||||
*
|
||||
* - Texture initialization and updates - complex textures (2d array textures, cube textures, 3d textures) need multiple images
|
||||
* `DynamicTexture` provides an API that makes it easy to provide the required data.
|
||||
*
|
||||
* - Texture resizing - Textures are immutable in WebGPU, meaning that they cannot be resized after creation.
|
||||
* DynamicTexture provides a `resize()` method that internally creates a new texture with the same parameters
|
||||
* but a different size.
|
||||
*
|
||||
* - Async image data initialization - It is often very convenient to be able to initialize textures with promises
|
||||
* returned by image or data loading functions, as it allows a callback-free linear style of programming.
|
||||
*
|
||||
* @note GPU Textures are quite complex objects, with many subresources and modes of usage.
|
||||
* The `DynamicTexture` class allows luma.gl to provide some support for working with textures
|
||||
* without accumulating excessive complexity in the core Texture class which is designed as an immutable nature of GPU resource.
|
||||
*/
|
||||
export class DynamicTexture {
|
||||
readonly device: Device;
|
||||
readonly id: string;
|
||||
props: Required<Omit<DynamicTextureProps, 'data'>>;
|
||||
|
||||
// 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;
|
||||
/** Props with defaults resolved (except `data` which is processed separately) */
|
||||
props: Readonly<Required<DynamicTextureProps>>;
|
||||
|
||||
/** Created resources */
|
||||
private _texture: Texture | null = null;
|
||||
private _sampler: Sampler | null = null;
|
||||
private _view: TextureView | null = null;
|
||||
|
||||
/** Ready when GPU texture has been created and data (if any) uploaded */
|
||||
readonly ready: Promise<Texture>;
|
||||
isReady: boolean = false;
|
||||
destroyed: boolean = false;
|
||||
isReady = false;
|
||||
destroyed = false;
|
||||
|
||||
protected resolveReady: () => void = () => {};
|
||||
protected rejectReady: (error: Error) => void = () => {};
|
||||
private resolveReady: (t: Texture) => void = () => {};
|
||||
private rejectReady: (error: Error) => void = () => {};
|
||||
|
||||
get texture(): Texture {
|
||||
if (!this._texture) throw new Error('Texture not initialized yet');
|
||||
return this._texture;
|
||||
}
|
||||
get sampler(): Sampler {
|
||||
if (!this._sampler) throw new Error('Sampler not initialized yet');
|
||||
return this._sampler;
|
||||
}
|
||||
get view(): TextureView {
|
||||
if (!this._view) throw new Error('View not initialized yet');
|
||||
return this._view;
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() {
|
||||
return 'DynamicTexture';
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `DynamicTexture:"${this.id}"(${this.isReady ? 'ready' : 'loading'})`;
|
||||
return `DynamicTexture:"${this.id}":${this.texture.width}x${this.texture.height}px:(${this.isReady ? 'ready' : 'loading...'})`;
|
||||
}
|
||||
|
||||
constructor(device: Device, props: DynamicTextureProps) {
|
||||
this.device = device;
|
||||
|
||||
// TODO - if we support URL strings as data...
|
||||
const id = uid('dynamic-texture'); // typeof props?.data === 'string' ? props.data.slice(-20) : uid('dynamic-texture');
|
||||
this.props = {...DynamicTexture.defaultProps, id, ...props};
|
||||
const id = uid('dynamic-texture');
|
||||
// NOTE: We avoid holding on to data to make sure it can be garbage collected.
|
||||
const originalPropsWithAsyncData = props;
|
||||
this.props = {...DynamicTexture.defaultProps, id, ...props, data: null};
|
||||
this.id = this.props.id;
|
||||
|
||||
props = {...props};
|
||||
// Signature: new DynamicTexture(device, {data: url})
|
||||
if (typeof props?.data === 'string' && props.dimension === '2d') {
|
||||
props.data = loadImageBitmap(props.data);
|
||||
}
|
||||
|
||||
// If mipmaps are requested, we need to allocate space for them
|
||||
if (props.mipmaps) {
|
||||
props.mipLevels = 'auto';
|
||||
}
|
||||
|
||||
this.ready = new Promise<Texture>((resolve, reject) => {
|
||||
this.resolveReady = () => {
|
||||
this.isReady = true;
|
||||
resolve(this.texture);
|
||||
};
|
||||
this.resolveReady = resolve;
|
||||
this.rejectReady = reject;
|
||||
});
|
||||
|
||||
this.initAsync(props);
|
||||
this.initAsync(originalPropsWithAsyncData);
|
||||
}
|
||||
|
||||
async initAsync(props: DynamicTextureProps): Promise<void> {
|
||||
const asyncData: DynamicTextureData = props.data;
|
||||
// @ts-expect-error not clear how to convince TS that null will be returned
|
||||
const data: TextureData | null = await awaitAllPromises(asyncData).then(
|
||||
undefined,
|
||||
this.rejectReady
|
||||
);
|
||||
/** @note Fire and forget; caller can await `ready` */
|
||||
async initAsync(originalPropsWithAsyncData: TextureDataAsyncProps): Promise<void> {
|
||||
try {
|
||||
// TODO - Accept URL string for 2D: turn into ExternalImage promise
|
||||
// const dataProps =
|
||||
// typeof props.data === 'string' && (props.dimension ?? '2d') === '2d'
|
||||
// ? ({dimension: '2d', data: loadImageBitmap(props.data)} as const)
|
||||
// : {};
|
||||
|
||||
// Check that we haven't been destroyed while waiting for texture data to load
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
const propsWithSyncData = await this._loadAllData(originalPropsWithAsyncData);
|
||||
this._checkNotDestroyed();
|
||||
|
||||
// Now we can actually create the texture
|
||||
// Deduce size when not explicitly provided
|
||||
// TODO - what about depth?
|
||||
const deduceSize = (): {width: number; height: number} => {
|
||||
if (this.props.width && this.props.height) {
|
||||
return {width: this.props.width, height: this.props.height};
|
||||
}
|
||||
|
||||
// Auto-deduce width and height if not supplied
|
||||
const size =
|
||||
this.props.width && this.props.height
|
||||
? {width: this.props.width, height: this.props.height}
|
||||
: this.getTextureDataSize(data);
|
||||
if (!size) {
|
||||
throw new Error('Texture size could not be determined');
|
||||
}
|
||||
const syncProps: TextureProps = {...size, ...props, data: undefined, mipLevels: 1};
|
||||
const size = getTextureSizeFromData(propsWithSyncData);
|
||||
if (size) {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Auto-calculate the number of mip levels as a convenience
|
||||
// TODO - Should we clamp to 1-getMipLevelCount?
|
||||
const maxMips = this.device.getMipLevelCount(syncProps.width, syncProps.height);
|
||||
syncProps.mipLevels = Math.max(
|
||||
1,
|
||||
this.props.mipLevels === 'auto' ? maxMips : Math.min(maxMips, this.props.mipLevels)
|
||||
);
|
||||
return {width: this.props.width || 1, height: this.props.height || 1};
|
||||
};
|
||||
|
||||
this.texture = this.device.createTexture(syncProps);
|
||||
this.sampler = this.texture.sampler;
|
||||
this.view = this.texture.view;
|
||||
|
||||
if (props.data) {
|
||||
switch (this.props.dimension) {
|
||||
case '1d':
|
||||
this._setTexture1DData(this.texture, data as Texture1DData);
|
||||
break;
|
||||
case '2d':
|
||||
this._setTexture2DData(data as Texture2DData);
|
||||
break;
|
||||
case '3d':
|
||||
this._setTexture3DData(this.texture, data as Texture3DData);
|
||||
break;
|
||||
case '2d-array':
|
||||
this._setTextureArrayData(this.texture, data as TextureArrayData);
|
||||
break;
|
||||
case 'cube':
|
||||
this._setTextureCubeData(this.texture, data as unknown as TextureCubeData);
|
||||
break;
|
||||
case 'cube-array':
|
||||
this._setTextureCubeArrayData(this.texture, data as unknown as TextureCubeArrayData);
|
||||
break;
|
||||
const size = deduceSize();
|
||||
if (!size || size.width <= 0 || size.height <= 0) {
|
||||
throw new Error(`${this} size could not be determined or was zero`);
|
||||
}
|
||||
}
|
||||
|
||||
// Do we need to generate mipmaps?
|
||||
if (this.props.mipmaps) {
|
||||
this.generateMipmaps();
|
||||
}
|
||||
// Create a minimal TextureProps and validate via `satisfies`
|
||||
const baseTextureProps = {
|
||||
...this.props,
|
||||
...size,
|
||||
mipLevels: 1, // temporary; updated below
|
||||
data: undefined
|
||||
} satisfies TextureProps;
|
||||
|
||||
log.info(1, `${this} loaded`);
|
||||
this.resolveReady();
|
||||
// Compute mip levels (auto clamps to max)
|
||||
const maxMips = this.device.getMipLevelCount(baseTextureProps.width, baseTextureProps.height);
|
||||
const desired =
|
||||
this.props.mipLevels === 'auto'
|
||||
? maxMips
|
||||
: Math.max(1, Math.min(maxMips, this.props.mipLevels ?? 1));
|
||||
|
||||
const finalTextureProps: TextureProps = {...baseTextureProps, mipLevels: desired};
|
||||
|
||||
this._texture = this.device.createTexture(finalTextureProps);
|
||||
this._sampler = this.texture.sampler;
|
||||
this._view = this.texture.view;
|
||||
|
||||
// Upload data if provided
|
||||
if (propsWithSyncData.data) {
|
||||
switch (propsWithSyncData.dimension) {
|
||||
case '1d':
|
||||
this.setTexture1DData(propsWithSyncData.data);
|
||||
break;
|
||||
case '2d':
|
||||
this.setTexture2DData(propsWithSyncData.data);
|
||||
break;
|
||||
case '3d':
|
||||
this.setTexture3DData(propsWithSyncData.data);
|
||||
break;
|
||||
case '2d-array':
|
||||
this.setTextureArrayData(propsWithSyncData.data);
|
||||
break;
|
||||
case 'cube':
|
||||
this.setTextureCubeData(propsWithSyncData.data);
|
||||
break;
|
||||
case 'cube-array':
|
||||
this.setTextureCubeArrayData(propsWithSyncData.data);
|
||||
break;
|
||||
default: {
|
||||
throw new Error(`Unhandled dimension ${propsWithSyncData.dimension}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.mipmaps) {
|
||||
this.generateMipmaps();
|
||||
}
|
||||
|
||||
this.isReady = true;
|
||||
this.resolveReady(this.texture);
|
||||
|
||||
log.info(0, `${this} created`)();
|
||||
} catch (e) {
|
||||
const err = e instanceof Error ? e : new Error(String(e));
|
||||
this.rejectReady(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.texture) {
|
||||
this.texture.destroy();
|
||||
// @ts-expect-error
|
||||
this.texture = null;
|
||||
if (this._texture) {
|
||||
this._texture.destroy();
|
||||
this._texture = null;
|
||||
this._sampler = null;
|
||||
this._view = null;
|
||||
}
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
generateMipmaps(): void {
|
||||
// if (this.device.type === 'webgl') {
|
||||
this.texture.generateMipmapsWebGL();
|
||||
// }
|
||||
// Only supported via WebGL helper (luma.gl path)
|
||||
if (this.device.type === 'webgl') {
|
||||
this.texture.generateMipmapsWebGL();
|
||||
} else {
|
||||
throw new Error('Automatic mipmap generation not supported on this device');
|
||||
}
|
||||
}
|
||||
|
||||
/** Set sampler or create and set new Sampler from SamplerProps */
|
||||
/** Set sampler or create one from props */
|
||||
setSampler(sampler: Sampler | SamplerProps = {}): void {
|
||||
this.texture.setSampler(
|
||||
sampler instanceof Sampler ? sampler : this.device.createSampler(sampler)
|
||||
);
|
||||
this._checkReady();
|
||||
const s = sampler instanceof Sampler ? sampler : this.device.createSampler(sampler);
|
||||
this.texture.setSampler(s);
|
||||
this._sampler = s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @note Mipmaps may need to be regenerated after resizing / setting new data
|
||||
* @todo Abort pending promise and create a texture with the new size?
|
||||
* Resize by cloning the underlying immutable texture.
|
||||
* Does not copy contents; caller may need to re-upload and/or regenerate mips.
|
||||
*/
|
||||
resize(size: {width: number; height: number}): boolean {
|
||||
if (!this.isReady) {
|
||||
throw new Error('Cannot resize texture before it is ready');
|
||||
}
|
||||
this._checkReady();
|
||||
|
||||
if (size.width === this.texture.width && size.height === this.texture.height) {
|
||||
return false;
|
||||
}
|
||||
const prev = this.texture;
|
||||
this._texture = prev.clone(size);
|
||||
this._sampler = this.texture.sampler;
|
||||
this._view = this.texture.view;
|
||||
|
||||
if (this.texture) {
|
||||
const texture = this.texture;
|
||||
this.texture = texture.clone(size);
|
||||
texture.destroy();
|
||||
}
|
||||
|
||||
prev.destroy();
|
||||
log.info(`${this} resized`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Check if texture data is a typed array */
|
||||
isTextureLevelData(data: TextureData): data is TextureImageData {
|
||||
const typedArray = (data as TextureImageData)?.data;
|
||||
return ArrayBuffer.isView(typedArray);
|
||||
/** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
|
||||
getCubeFaceIndex(face: TextureCubeFace): number {
|
||||
const index = TEXTURE_CUBE_FACE_MAP[face];
|
||||
if (index === undefined) throw new Error(`Invalid cube face: ${face}`);
|
||||
return index;
|
||||
}
|
||||
|
||||
/** Get the size of the texture described by the provided TextureData */
|
||||
getTextureDataSize(
|
||||
data:
|
||||
| TextureData
|
||||
| TextureCubeData
|
||||
| TextureArrayData
|
||||
| TextureCubeArrayData
|
||||
| TypedArray
|
||||
| null
|
||||
): {width: number; height: number} | null {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
if (ArrayBuffer.isView(data)) {
|
||||
return null;
|
||||
}
|
||||
// Recurse into arrays (array of miplevels)
|
||||
if (Array.isArray(data)) {
|
||||
return this.getTextureDataSize(data[0]);
|
||||
}
|
||||
if (this.device.isExternalImage(data)) {
|
||||
return this.device.getExternalImageSize(data);
|
||||
}
|
||||
if (data && typeof data === 'object' && data.constructor === Object) {
|
||||
const textureDataArray = Object.values(data);
|
||||
const untypedData = textureDataArray[0];
|
||||
return {width: untypedData.width, height: untypedData.height};
|
||||
}
|
||||
throw new Error('texture size deduction failed');
|
||||
/** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
|
||||
getCubeArrayFaceIndex(cubeIndex: number, face: TextureCubeFace): number {
|
||||
return 6 * cubeIndex + this.getCubeFaceIndex(face);
|
||||
}
|
||||
|
||||
/** Convert luma.gl cubemap face constants to depth index */
|
||||
getCubeFaceDepth(face: TextureCubeFace): number {
|
||||
// prettier-ignore
|
||||
switch (face) {
|
||||
case '+X': return 0;
|
||||
case '-X': return 1;
|
||||
case '+Y': return 2;
|
||||
case '-Y': return 3;
|
||||
case '+Z': return 4;
|
||||
case '-Z': return 5;
|
||||
default: throw new Error(face);
|
||||
}
|
||||
/** @note experimental: Set multiple mip levels (1D) */
|
||||
setTexture1DData(data: Texture1DData): void {
|
||||
this._checkReady();
|
||||
if (this.texture.props.dimension !== '1d') {
|
||||
throw new Error(`${this} is not 1d`);
|
||||
}
|
||||
const subresources = getTexture1DSubresources(data);
|
||||
this._setTextureSubresources(subresources);
|
||||
}
|
||||
|
||||
// EXPERIMENTAL
|
||||
/** @note experimental: Set multiple mip levels (2D), optionally at `z`, slice (depth/array level) index */
|
||||
setTexture2DData(lodData: Texture2DData, z: number = 0): void {
|
||||
this._checkReady();
|
||||
if (this.texture.props.dimension !== '2d') {
|
||||
throw new Error(`${this} is not 2d`);
|
||||
}
|
||||
|
||||
setTextureData(data: TextureData) {}
|
||||
|
||||
/** Experimental: Set multiple mip levels */
|
||||
_setTexture1DData(texture: Texture, data: Texture1DData): void {
|
||||
throw new Error('setTexture1DData not supported in WebGL.');
|
||||
const subresources = getTexture2DSubresources(z, lodData);
|
||||
this._setTextureSubresources(subresources);
|
||||
}
|
||||
|
||||
/** Experimental: Set multiple mip levels */
|
||||
_setTexture2DData(lodData: Texture2DData, depth = 0): void {
|
||||
if (!this.texture) {
|
||||
throw new Error('Texture not initialized');
|
||||
/** 3D: multiple depth slices, each may carry multiple mip levels */
|
||||
setTexture3DData(data: Texture3DData): void {
|
||||
if (this.texture.props.dimension !== '3d') {
|
||||
throw new Error(`${this} is not 3d`);
|
||||
}
|
||||
const subresources = getTexture3DSubresources(data);
|
||||
this._setTextureSubresources(subresources);
|
||||
}
|
||||
|
||||
const lodArray = this._normalizeTextureData(lodData);
|
||||
|
||||
// If the user provides multiple LODs, then automatic mipmap
|
||||
// generation generateMipmap() should be disabled to avoid overwriting them.
|
||||
if (lodArray.length > 1 && this.props.mipmaps !== false) {
|
||||
log.warn(`Texture ${this.id} mipmap and multiple LODs.`)();
|
||||
/** 2D array: multiple layers, each may carry multiple mip levels */
|
||||
setTextureArrayData(data: TextureArrayData): void {
|
||||
if (this.texture.props.dimension !== '2d-array') {
|
||||
throw new Error(`${this} is not 2d-array`);
|
||||
}
|
||||
const subresources = getTextureArraySubresources(data);
|
||||
this._setTextureSubresources(subresources);
|
||||
}
|
||||
|
||||
for (let mipLevel = 0; mipLevel < lodArray.length; mipLevel++) {
|
||||
const imageData = lodArray[mipLevel];
|
||||
if (this.device.isExternalImage(imageData)) {
|
||||
this.texture.copyExternalImage({image: imageData, z: depth, mipLevel, flipY: true});
|
||||
} else {
|
||||
this.texture.copyImageData({data: imageData.data, z: depth, mipLevel});
|
||||
/** Cube: 6 faces, each may carry multiple mip levels */
|
||||
setTextureCubeData(data: TextureCubeData): void {
|
||||
if (this.texture.props.dimension !== 'cube') {
|
||||
throw new Error(`${this} is not cube`);
|
||||
}
|
||||
const subresources = getTextureCubeSubresources(data);
|
||||
this._setTextureSubresources(subresources);
|
||||
}
|
||||
|
||||
/** Cube array: multiple cubes (faces×layers), each face may carry multiple mips */
|
||||
private setTextureCubeArrayData(data: TextureCubeArrayData): void {
|
||||
if (this.texture.props.dimension !== 'cube-array') {
|
||||
throw new Error(`${this} is not cube-array`);
|
||||
}
|
||||
const subresources = getTextureCubeArraySubresources(data);
|
||||
this._setTextureSubresources(subresources);
|
||||
}
|
||||
|
||||
/** Sets multiple mip levels on different `z` slices (depth/array index) */
|
||||
private _setTextureSubresources(subresources: TextureSubresource[]): void {
|
||||
// If user supplied multiple mip levels, warn if auto-mips also requested
|
||||
// if (lodArray.length > 1 && this.props.mipmaps !== false) {
|
||||
// log.warn(
|
||||
// `Texture ${this.id}: provided multiple LODs and also requested mipmap generation.`
|
||||
// )();
|
||||
// }
|
||||
|
||||
for (const subresource of subresources) {
|
||||
const {z, mipLevel} = subresource;
|
||||
switch (subresource.type) {
|
||||
case 'external-image':
|
||||
const {image, flipY} = subresource;
|
||||
this.texture.copyExternalImage({image, z, mipLevel, flipY});
|
||||
break;
|
||||
case 'texture-data':
|
||||
const {data} = subresource;
|
||||
// TODO - we are throwing away some of the info in data.
|
||||
// Did we not need it in the first place? Can we use it to validate?
|
||||
this.texture.copyImageData({data: data.data, z, mipLevel});
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported 2D mip-level payload');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental: Sets 3D texture data: multiple depth slices, multiple mip levels
|
||||
* @param data
|
||||
*/
|
||||
_setTexture3DData(texture: Texture, data: Texture3DData): void {
|
||||
if (this.texture?.props.dimension !== '3d') {
|
||||
throw new Error(this.id);
|
||||
}
|
||||
for (let depth = 0; depth < data.length; depth++) {
|
||||
this._setTexture2DData(data[depth], depth);
|
||||
// ------------------ helpers ------------------
|
||||
|
||||
/** Recursively resolve all promises in data structures */
|
||||
private async _loadAllData(props: TextureDataAsyncProps): Promise<TextureDataProps> {
|
||||
const syncData = await awaitAllPromises(props.data);
|
||||
const dimension = (props.dimension ?? '2d') as TextureDataProps['dimension'];
|
||||
return {dimension, data: syncData ?? null} as TextureDataProps;
|
||||
}
|
||||
|
||||
private _checkNotDestroyed() {
|
||||
if (this.destroyed) {
|
||||
log.warn(`${this} already destroyed`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental: Set Cube texture data, multiple faces, multiple mip levels
|
||||
* @todo - could support TextureCubeArray with depth
|
||||
* @param data
|
||||
* @param index
|
||||
*/
|
||||
_setTextureCubeData(texture: Texture, data: TextureCubeData): void {
|
||||
if (this.texture?.props.dimension !== 'cube') {
|
||||
throw new Error(this.id);
|
||||
private _checkReady() {
|
||||
if (!this.isReady) {
|
||||
log.warn(`${this} Cannot perform this operation before ready`);
|
||||
}
|
||||
for (const [face, faceData] of Object.entries(data)) {
|
||||
const faceDepth = CubeFaces.indexOf(face as TextureCubeFace);
|
||||
this._setTexture2DData(faceData, faceDepth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental: Sets texture array data, multiple levels, multiple depth slices
|
||||
* @param data
|
||||
*/
|
||||
_setTextureArrayData(texture: Texture, data: TextureArrayData): void {
|
||||
if (this.texture?.props.dimension !== '2d-array') {
|
||||
throw new Error(this.id);
|
||||
}
|
||||
for (let depth = 0; depth < data.length; depth++) {
|
||||
this._setTexture2DData(data[depth], depth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental: Sets texture cube array, multiple faces, multiple levels, multiple mip levels
|
||||
* @param data
|
||||
*/
|
||||
_setTextureCubeArrayData(texture: Texture, data: TextureCubeArrayData): void {
|
||||
throw new Error('setTextureCubeArrayData not supported in WebGL2.');
|
||||
}
|
||||
|
||||
/** Experimental */
|
||||
_setTextureCubeFaceData(
|
||||
texture: Texture,
|
||||
lodData: Texture2DData,
|
||||
face: TextureCubeFace,
|
||||
depth: number = 0
|
||||
): void {
|
||||
// assert(this.props.dimension === 'cube');
|
||||
|
||||
// If the user provides multiple LODs, then automatic mipmap
|
||||
// generation generateMipmap() should be disabled to avoid overwriting them.
|
||||
if (Array.isArray(lodData) && lodData.length > 1 && this.props.mipmaps !== false) {
|
||||
log.warn(`${this.id} has mipmap and multiple LODs.`)();
|
||||
}
|
||||
|
||||
const faceDepth = TextureCubeFaces.indexOf(face);
|
||||
this._setTexture2DData(lodData, faceDepth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize TextureData to an array of TextureImageData / ExternalImages
|
||||
* @param data
|
||||
* @param options
|
||||
* @returns array of TextureImageData / ExternalImages
|
||||
*/
|
||||
_normalizeTextureData(data: Texture2DData): (TextureImageData | ExternalImage)[] {
|
||||
const options: {width: number; height: number; depth: number} = this.texture;
|
||||
let mipLevelArray: (TextureImageData | ExternalImage)[];
|
||||
if (ArrayBuffer.isView(data)) {
|
||||
mipLevelArray = [
|
||||
{
|
||||
// ts-expect-error does data really need to be Uint8ClampedArray?
|
||||
data,
|
||||
width: options.width,
|
||||
height: options.height
|
||||
// depth: options.depth
|
||||
}
|
||||
];
|
||||
} else if (!Array.isArray(data)) {
|
||||
mipLevelArray = [data];
|
||||
} else {
|
||||
mipLevelArray = data;
|
||||
}
|
||||
return mipLevelArray;
|
||||
}
|
||||
|
||||
static defaultProps: Required<DynamicTextureProps> = {
|
||||
...Texture.defaultProps,
|
||||
dimension: '2d',
|
||||
data: null,
|
||||
mipmaps: false
|
||||
};
|
||||
}
|
||||
|
||||
// TODO - Remove when texture refactor is complete
|
||||
|
||||
/*
|
||||
setCubeMapData(options: {
|
||||
width: number;
|
||||
height: number;
|
||||
data: Record<GL, Texture2DData> | Record<TextureCubeFace, Texture2DData>;
|
||||
format?: any;
|
||||
type?: any;
|
||||
/** @deprecated Use .data *
|
||||
pixels: any;
|
||||
}): void {
|
||||
const {gl} = this;
|
||||
|
||||
const {width, height, pixels, data, format = GL.RGBA, type = GL.UNSIGNED_BYTE} = options;
|
||||
|
||||
// pixel data (imageDataMap) is an Object from Face to Image or Promise.
|
||||
// For example:
|
||||
// {
|
||||
// GL.TEXTURE_CUBE_MAP_POSITIVE_X : Image-or-Promise,
|
||||
// GL.TEXTURE_CUBE_MAP_NEGATIVE_X : Image-or-Promise,
|
||||
// ... }
|
||||
// To provide multiple level-of-details (LODs) this can be Face to Array
|
||||
// of Image or Promise, like this
|
||||
// {
|
||||
// GL.TEXTURE_CUBE_MAP_POSITIVE_X : [Image-or-Promise-LOD-0, Image-or-Promise-LOD-1],
|
||||
// GL.TEXTURE_CUBE_MAP_NEGATIVE_X : [Image-or-Promise-LOD-0, Image-or-Promise-LOD-1],
|
||||
// ... }
|
||||
|
||||
const imageDataMap = this._getImageDataMap(pixels || data);
|
||||
|
||||
const resolvedFaces = WEBGLTexture.FACES.map(face => {
|
||||
const facePixels = imageDataMap[face];
|
||||
return Array.isArray(facePixels) ? facePixels : [facePixels];
|
||||
});
|
||||
this.bind();
|
||||
|
||||
WEBGLTexture.FACES.forEach((face, index) => {
|
||||
if (resolvedFaces[index].length > 1 && this.props.mipmaps !== false) {
|
||||
// If the user provides multiple LODs, then automatic mipmap
|
||||
// generation generateMipmaps() should be disabled to avoid overwritting them.
|
||||
log.warn(`${this.id} has mipmap and multiple LODs.`)();
|
||||
}
|
||||
resolvedFaces[index].forEach((image, lodLevel) => {
|
||||
// TODO: adjust width & height for LOD!
|
||||
if (width && height) {
|
||||
gl.texImage2D(face, lodLevel, format, width, height, 0 /* border*, format, type, image);
|
||||
} else {
|
||||
gl.texImage2D(face, lodLevel, format, format, type, image);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.unbind();
|
||||
}
|
||||
*/
|
||||
|
||||
// HELPERS
|
||||
|
||||
/** Resolve all promises in a nested data structure */
|
||||
@ -554,3 +416,33 @@ async function awaitAllPromises(x: any): Promise<any> {
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
// /** @note experimental: Set multiple mip levels (2D), optionally at `z`, slice (depth/array level) index */
|
||||
// setTexture2DData(lodData: Texture2DData, z: number = 0): void {
|
||||
// this._checkReady();
|
||||
|
||||
// const lodArray = this._normalizeTexture2DData(lodData);
|
||||
|
||||
// // If user supplied multiple mip levels, warn if auto-mips also requested
|
||||
// if (lodArray.length > 1 && this.props.mipmaps !== false) {
|
||||
// log.warn(
|
||||
// `Texture ${this.id}: provided multiple LODs and also requested mipmap generation.`
|
||||
// )();
|
||||
// }
|
||||
|
||||
// for (let mipLevel = 0; mipLevel < lodArray.length; mipLevel++) {
|
||||
// const imageData = lodArray[mipLevel];
|
||||
// if (this.device.isExternalImage(imageData)) {
|
||||
// this.texture.copyExternalImage({image: imageData, z, mipLevel, flipY: true});
|
||||
// } else if (this._isTextureImageData(imageData)) {
|
||||
// this.texture.copyImageData({data: imageData.data, z, mipLevel});
|
||||
// } else {
|
||||
// throw new Error('Unsupported 2D mip-level payload');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /** Normalize 2D layer payload into an array of mip-level items */
|
||||
// private _normalizeTexture2DData(data: Texture2DData): (TextureImageData | ExternalImage)[] {
|
||||
// return Array.isArray(data) ? data : [data];
|
||||
// }
|
||||
|
||||
301
modules/engine/src/dynamic-texture/texture-data.ts
Normal file
301
modules/engine/src/dynamic-texture/texture-data.ts
Normal file
@ -0,0 +1,301 @@
|
||||
import type {TypedArray, TextureFormat, ExternalImage} from '@luma.gl/core';
|
||||
import {isExternalImage, getExternalImageSize} from '@luma.gl/core';
|
||||
|
||||
export type TextureImageSource = ExternalImage;
|
||||
|
||||
/**
|
||||
* One mip level
|
||||
* Basic data structure is similar to `ImageData`
|
||||
* additional optional fields can describe compressed texture data.
|
||||
*/
|
||||
export type TextureImageData = {
|
||||
/** WebGPU style format string. Defaults to 'rgba8unorm' */
|
||||
format?: TextureFormat;
|
||||
/** Typed Array with the bytes of the image. @note beware row byte alignment requirements */
|
||||
data: TypedArray;
|
||||
/** Width of the image, in pixels, @note beware row byte alignment requirements */
|
||||
width: number;
|
||||
/** Height of the image, in rows */
|
||||
height: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A single mip-level can be initialized by data or an ImageBitmap etc
|
||||
* @note in the WebGPU spec a mip-level is called a subresource
|
||||
*/
|
||||
export type TextureMipLevelData = TextureImageData | TextureImageSource;
|
||||
|
||||
/**
|
||||
* Texture data for one image "slice" (which can consist of multiple miplevels)
|
||||
* Thus data for one slice be a single mip level or an array of miplevels
|
||||
* @note in the WebGPU spec each cross-section image in a 3D texture is called a "slice",
|
||||
* in a array texture each image in the array is called an array "layer"
|
||||
* luma.gl calls one image in a GPU texture a "slice" regardless of context.
|
||||
*/
|
||||
export type TextureSliceData = TextureMipLevelData | TextureMipLevelData[];
|
||||
|
||||
/** Names of cube texture faces */
|
||||
export type TextureCubeFace = '+X' | '-X' | '+Y' | '-Y' | '+Z' | '-Z';
|
||||
|
||||
/** Array of cube texture faces. @note: index in array is the face index */
|
||||
// prettier-ignore
|
||||
export const TEXTURE_CUBE_FACES = ['+X', '-X', '+Y', '-Y', '+Z', '-Z'] as const satisfies readonly TextureCubeFace[];
|
||||
|
||||
/** Map of cube texture face names to face indexes */
|
||||
// prettier-ignore
|
||||
export const TEXTURE_CUBE_FACE_MAP = {'+X': 0, '-X': 1, '+Y': 2, '-Y': 3, '+Z': 4, '-Z': 5} as const satisfies Record<TextureCubeFace, number>;
|
||||
|
||||
/** @todo - Define what data type is supported for 1D textures. TextureImageData with height = 1 */
|
||||
export type Texture1DData = TextureSliceData;
|
||||
|
||||
/** Texture data can be one or more mip levels */
|
||||
export type Texture2DData = TextureSliceData;
|
||||
|
||||
/** 6 face textures */
|
||||
export type TextureCubeData = Record<TextureCubeFace, TextureSliceData>;
|
||||
|
||||
/** Array of textures */
|
||||
export type Texture3DData = TextureSliceData[];
|
||||
|
||||
/** Array of textures */
|
||||
export type TextureArrayData = TextureSliceData[];
|
||||
|
||||
/** Array of 6 face textures */
|
||||
export type TextureCubeArrayData = Record<TextureCubeFace, TextureSliceData>[];
|
||||
|
||||
type TextureData =
|
||||
| Texture1DData
|
||||
| Texture3DData
|
||||
| TextureArrayData
|
||||
| TextureCubeArrayData
|
||||
| TextureCubeData;
|
||||
|
||||
/** Sync data props */
|
||||
export type TextureDataProps =
|
||||
| {dimension: '1d'; data: Texture1DData | null}
|
||||
| {dimension?: '2d'; data: Texture2DData | null}
|
||||
| {dimension: '3d'; data: Texture3DData | null}
|
||||
| {dimension: '2d-array'; data: TextureArrayData | null}
|
||||
| {dimension: 'cube'; data: TextureCubeData | null}
|
||||
| {dimension: 'cube-array'; data: TextureCubeArrayData | null};
|
||||
|
||||
/** Async data props */
|
||||
export type TextureDataAsyncProps =
|
||||
| {dimension: '1d'; data?: Promise<Texture1DData> | Texture1DData | null}
|
||||
| {dimension?: '2d'; data?: Promise<Texture2DData> | Texture2DData | null}
|
||||
| {dimension: '3d'; data?: Promise<Texture3DData> | Texture3DData | null}
|
||||
| {dimension: '2d-array'; data?: Promise<TextureArrayData> | TextureArrayData | null}
|
||||
| {dimension: 'cube'; data?: Promise<TextureCubeData> | TextureCubeData | null}
|
||||
| {dimension: 'cube-array'; data?: Promise<TextureCubeArrayData> | TextureCubeArrayData | null};
|
||||
|
||||
/** Describes data for one sub resource (one mip level of one slice (depth or array layer)) */
|
||||
export type TextureSubresource = {
|
||||
/** slice (depth or array layer)) */
|
||||
z: number;
|
||||
/** mip level (0 - max mip levels) */
|
||||
mipLevel: number;
|
||||
} & (
|
||||
| {
|
||||
type: 'external-image';
|
||||
image: ExternalImage;
|
||||
/** @deprecated is this an appropriate place for this flag? */
|
||||
flipY?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'texture-data';
|
||||
data: TextureImageData;
|
||||
}
|
||||
);
|
||||
|
||||
/** Check if texture data is a typed array */
|
||||
export function isTextureSliceData(data: TextureData): data is TextureImageData {
|
||||
const typedArray = (data as TextureImageData)?.data;
|
||||
return ArrayBuffer.isView(typedArray);
|
||||
}
|
||||
|
||||
export function getFirstMipLevel(layer: TextureSliceData | null): TextureMipLevelData | null {
|
||||
if (!layer) return null;
|
||||
return Array.isArray(layer) ? (layer[0] ?? null) : layer;
|
||||
}
|
||||
|
||||
export function getTextureSizeFromData(
|
||||
props: TextureDataProps
|
||||
): {width: number; height: number} | null {
|
||||
const {dimension, data} = props;
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (dimension) {
|
||||
case '1d': {
|
||||
const mipLevel = getFirstMipLevel(data);
|
||||
if (!mipLevel) return null;
|
||||
const {width} = getTextureMipLevelSize(mipLevel);
|
||||
return {width, height: 1};
|
||||
}
|
||||
case '2d': {
|
||||
const mipLevel = getFirstMipLevel(data);
|
||||
return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
|
||||
}
|
||||
case '3d':
|
||||
case '2d-array': {
|
||||
if (!Array.isArray(data) || data.length === 0) return null;
|
||||
const mipLevel = getFirstMipLevel(data[0]);
|
||||
return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
|
||||
}
|
||||
case 'cube': {
|
||||
const face = (Object.keys(data)[0] as TextureCubeFace) ?? null;
|
||||
if (!face) return null;
|
||||
const faceData = (data as Record<TextureCubeFace, TextureSliceData>)[face];
|
||||
const mipLevel = getFirstMipLevel(faceData);
|
||||
return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
|
||||
}
|
||||
case 'cube-array': {
|
||||
if (!Array.isArray(data) || data.length === 0) return null;
|
||||
const firstCube = data[0];
|
||||
const face = (Object.keys(firstCube)[0] as TextureCubeFace) ?? null;
|
||||
if (!face) return null;
|
||||
const mipLevel = getFirstMipLevel(firstCube[face]);
|
||||
return mipLevel ? getTextureMipLevelSize(mipLevel) : null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getTextureMipLevelSize(data: TextureMipLevelData): {width: number; height: number} {
|
||||
if (isExternalImage(data)) {
|
||||
return getExternalImageSize(data);
|
||||
}
|
||||
if (typeof data === 'object' && 'width' in data && 'height' in data) {
|
||||
return {width: data.width, height: data.height};
|
||||
}
|
||||
throw new Error('Unsupported mip-level data');
|
||||
}
|
||||
|
||||
/** Type guard: is a mip-level `TextureImageData` (vs ExternalImage) */
|
||||
function isTextureImageData(data: TextureMipLevelData): data is TextureImageData {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'data' in data &&
|
||||
'width' in data &&
|
||||
'height' in data
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve size for a single mip-level datum */
|
||||
// function getTextureMipLevelSizeFromData(data: TextureMipLevelData): {
|
||||
// width: number;
|
||||
// height: number;
|
||||
// } {
|
||||
// if (this.device.isExternalImage(data)) {
|
||||
// return this.device.getExternalImageSize(data);
|
||||
// }
|
||||
// if (this.isTextureImageData(data)) {
|
||||
// return {width: data.width, height: data.height};
|
||||
// }
|
||||
// // Fallback (should not happen with current types)
|
||||
// throw new Error('Unsupported mip-level data');
|
||||
// }
|
||||
|
||||
/** Convert cube face label to depth index */
|
||||
export function getCubeFaceIndex(face: TextureCubeFace): number {
|
||||
const idx = TEXTURE_CUBE_FACE_MAP[face];
|
||||
if (idx === undefined) throw new Error(`Invalid cube face: ${face}`);
|
||||
return idx;
|
||||
}
|
||||
|
||||
/** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
|
||||
export function getCubeArrayFaceIndex(cubeIndex: number, face: TextureCubeFace): number {
|
||||
return 6 * cubeIndex + getCubeFaceIndex(face);
|
||||
}
|
||||
|
||||
// ------------------ Upload helpers ------------------
|
||||
|
||||
/** Experimental: Set multiple mip levels (1D) */
|
||||
export function getTexture1DSubresources(data: Texture1DData): TextureSubresource[] {
|
||||
// Not supported in WebGL; left explicit
|
||||
throw new Error('setTexture1DData not supported in WebGL.');
|
||||
// const subresources: TextureSubresource[] = [];
|
||||
// return subresources;
|
||||
}
|
||||
|
||||
/** Normalize 2D layer payload into an array of mip-level items */
|
||||
function _normalizeTexture2DData(data: Texture2DData): (TextureImageData | ExternalImage)[] {
|
||||
return Array.isArray(data) ? data : [data];
|
||||
}
|
||||
|
||||
/** Experimental: Set multiple mip levels (2D), optionally at `z` (depth/array index) */
|
||||
export function getTexture2DSubresources(
|
||||
slice: number,
|
||||
lodData: Texture2DData
|
||||
): TextureSubresource[] {
|
||||
const lodArray = _normalizeTexture2DData(lodData);
|
||||
const z = slice;
|
||||
|
||||
const subresources: TextureSubresource[] = [];
|
||||
|
||||
for (let mipLevel = 0; mipLevel < lodArray.length; mipLevel++) {
|
||||
const imageData = lodArray[mipLevel];
|
||||
if (isExternalImage(imageData)) {
|
||||
subresources.push({
|
||||
type: 'external-image',
|
||||
image: imageData,
|
||||
z,
|
||||
mipLevel
|
||||
});
|
||||
} else if (isTextureImageData(imageData)) {
|
||||
subresources.push({
|
||||
type: 'texture-data',
|
||||
data: imageData,
|
||||
z,
|
||||
mipLevel
|
||||
});
|
||||
} else {
|
||||
throw new Error('Unsupported 2D mip-level payload');
|
||||
}
|
||||
}
|
||||
|
||||
return subresources;
|
||||
}
|
||||
|
||||
/** 3D: multiple depth slices, each may carry multiple mip levels */
|
||||
export function getTexture3DSubresources(data: Texture3DData): TextureSubresource[] {
|
||||
const subresources: TextureSubresource[] = [];
|
||||
for (let depth = 0; depth < data.length; depth++) {
|
||||
subresources.push(...getTexture2DSubresources(depth, data[depth]));
|
||||
}
|
||||
return subresources;
|
||||
}
|
||||
|
||||
/** 2D array: multiple layers, each may carry multiple mip levels */
|
||||
export function getTextureArraySubresources(data: TextureArrayData): TextureSubresource[] {
|
||||
const subresources: TextureSubresource[] = [];
|
||||
for (let layer = 0; layer < data.length; layer++) {
|
||||
subresources.push(...getTexture2DSubresources(layer, data[layer]));
|
||||
}
|
||||
return subresources;
|
||||
}
|
||||
|
||||
/** Cube: 6 faces, each may carry multiple mip levels */
|
||||
export function getTextureCubeSubresources(data: TextureCubeData): TextureSubresource[] {
|
||||
const subresources: TextureSubresource[] = [];
|
||||
for (const [face, faceData] of Object.entries(data) as [TextureCubeFace, TextureSliceData][]) {
|
||||
const faceDepth = getCubeFaceIndex(face);
|
||||
subresources.push(...getTexture2DSubresources(faceDepth, faceData));
|
||||
}
|
||||
return subresources;
|
||||
}
|
||||
|
||||
/** Cube array: multiple cubes (faces×layers), each face may carry multiple mips */
|
||||
export function getTextureCubeArraySubresources(data: TextureCubeArrayData): TextureSubresource[] {
|
||||
const subresources: TextureSubresource[] = [];
|
||||
data.forEach((cubeData, cubeIndex) => {
|
||||
for (const [face, faceData] of Object.entries(cubeData)) {
|
||||
const faceDepth = getCubeArrayFaceIndex(cubeIndex, face as TextureCubeFace);
|
||||
getTexture2DSubresources(faceDepth, faceData);
|
||||
}
|
||||
});
|
||||
return subresources;
|
||||
}
|
||||
@ -83,14 +83,13 @@ export {Computation} from './compute/computation';
|
||||
export type {
|
||||
TextureCubeFace,
|
||||
TextureImageData,
|
||||
TextureData,
|
||||
Texture1DData,
|
||||
Texture2DData,
|
||||
Texture3DData,
|
||||
TextureCubeData,
|
||||
TextureArrayData,
|
||||
TextureCubeArrayData
|
||||
} from './dynamic-texture/dynamic-texture';
|
||||
} from './dynamic-texture/texture-data';
|
||||
|
||||
export type {DynamicTextureProps} from './dynamic-texture/dynamic-texture';
|
||||
export {DynamicTexture} from './dynamic-texture/dynamic-texture';
|
||||
|
||||
@ -39,19 +39,23 @@ function getFilterShaderWGSL(func: string) {
|
||||
// Binding 0:1 is reserved for shader passes
|
||||
// @group(0) @binding(0) var<uniform> brightnessContrast : brightnessContrastUniforms;
|
||||
@group(0) @binding(1) var texture: texture_2d<f32>;
|
||||
@group(0) @binding(2) var sampler: sampler;
|
||||
@group(0) @binding(2) var textureSampler: sampler;
|
||||
|
||||
struct FragmentInputs {
|
||||
@location(0) fragUV: vec2f,
|
||||
@location(1) fragPosition: vec4f,
|
||||
@location(2) fragCoordinate: vec4f
|
||||
};
|
||||
// This needs to be aligned with
|
||||
// struct FragmentInputs {
|
||||
// @location(0) fragUV: vec2f,
|
||||
// @location(1) fragPosition: vec4f,
|
||||
// @location(2) fragCoordinate: vec4f
|
||||
// };
|
||||
|
||||
@fragment
|
||||
fn fragmentMain(inputs: FragmentInputs) -> @location(0) vec4f {
|
||||
let texSize = textureDimensions(texture, 0);
|
||||
var fragColor = textureSample(texture, sampler, fragUV);
|
||||
fragColor = ${func}(gl_FragColor, texSize, texCoord);
|
||||
let fragUV = inputs.uv;
|
||||
let fragCoordinate = inputs.coordinate;
|
||||
let texSize = vec2f(textureDimensions(texture, 0));
|
||||
|
||||
var fragColor = textureSample(texture, textureSampler, fragUV);
|
||||
fragColor = ${func}(fragColor, texSize, fragCoordinate);
|
||||
return fragColor;
|
||||
}
|
||||
`;
|
||||
@ -73,9 +77,9 @@ struct FragmentInputs = {
|
||||
|
||||
@fragment
|
||||
fn fragmentMain(inputs: FragmentInputs) -> @location(0) vec4f {
|
||||
let texSize = textureDimensions(texture, 0);
|
||||
let texSize = vec2f(textureDimensions(texture, 0));
|
||||
var fragColor = textureSample(texture, sampler, fragUV);
|
||||
fragColor = ${func}(gl_FragColor, texSize, texCoord);
|
||||
fragColor = ${func}(fragColor, texSize, texCoord);
|
||||
return fragColor;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -157,6 +157,7 @@ void main() {
|
||||
this.textureModel.draw(clearTexturePass);
|
||||
clearTexturePass.end();
|
||||
|
||||
// Copy the texture contents
|
||||
// const commandEncoder = this.device.createCommandEncoder();
|
||||
// commandEncoder.copyTextureToTexture({
|
||||
// sourceTexture: sourceTexture.texture,
|
||||
@ -164,33 +165,33 @@ void main() {
|
||||
// });
|
||||
// commandEncoder.finish();
|
||||
|
||||
// let first = true;
|
||||
// for (const passRenderer of this.passRenderers) {
|
||||
// for (const subPassRenderer of passRenderer.subPassRenderers) {
|
||||
// if (!first) {
|
||||
// this.swapFramebuffers.swap();
|
||||
// }
|
||||
// first = false;
|
||||
let first = true;
|
||||
for (const passRenderer of this.passRenderers) {
|
||||
for (const subPassRenderer of passRenderer.subPassRenderers) {
|
||||
if (!first) {
|
||||
this.swapFramebuffers.swap();
|
||||
}
|
||||
first = false;
|
||||
|
||||
// const swapBufferTexture = this.swapFramebuffers.current.colorAttachments[0].texture;
|
||||
const swapBufferTexture = this.swapFramebuffers.current.colorAttachments[0].texture;
|
||||
|
||||
// const bindings = {
|
||||
// sourceTexture: swapBufferTexture
|
||||
// // texSize: [sourceTextures.width, sourceTextures.height]
|
||||
// };
|
||||
const bindings = {
|
||||
sourceTexture: swapBufferTexture
|
||||
// texSize: [sourceTextures.width, sourceTextures.height]
|
||||
};
|
||||
|
||||
// const renderPass = this.device.beginRenderPass({
|
||||
// id: 'shader-pass-renderer-run-pass',
|
||||
// framebuffer: this.swapFramebuffers.next,
|
||||
// clearColor: [0, 0, 0, 1],
|
||||
// clearDepth: 1
|
||||
// });
|
||||
// subPassRenderer.render({renderPass, bindings});
|
||||
// renderPass.end();
|
||||
// }
|
||||
// }
|
||||
const renderPass = this.device.beginRenderPass({
|
||||
id: 'shader-pass-renderer-run-pass',
|
||||
framebuffer: this.swapFramebuffers.next,
|
||||
clearColor: [0, 0, 0, 1],
|
||||
clearDepth: 1
|
||||
});
|
||||
subPassRenderer.render({renderPass, bindings});
|
||||
renderPass.end();
|
||||
}
|
||||
}
|
||||
|
||||
// this.swapFramebuffers.swap();
|
||||
this.swapFramebuffers.swap();
|
||||
const outputTexture = this.swapFramebuffers.current.colorAttachments[0].texture;
|
||||
return outputTexture;
|
||||
}
|
||||
|
||||
@ -3,16 +3,14 @@
|
||||
|
||||
import test from 'tape-promise/tape';
|
||||
import {getWebGLTestDevice} from '@luma.gl/test-utils';
|
||||
import {AsyncTexture} from '../../src/index';
|
||||
import {DynamicTexture} from '../../src/index';
|
||||
|
||||
// Verify that specifying mipLevels: 0 is clamped to at least 1
|
||||
// See issue or commit reference for details.
|
||||
test('AsyncTexture#mipLevels clamped to minimum 1', async t => {
|
||||
test('DynamicTexture#mipLevels clamped to minimum 1', async t => {
|
||||
const device = await getWebGLTestDevice();
|
||||
const texture = new AsyncTexture(device, {
|
||||
data: new Uint8Array(4),
|
||||
width: 1,
|
||||
height: 1,
|
||||
const texture = new DynamicTexture(device, {
|
||||
data: {data: new Uint8Array(4), width: 1, height: 1},
|
||||
mipLevels: 0
|
||||
});
|
||||
await texture.ready;
|
||||
|
||||
235
modules/engine/test/dynamic-texture/texture-data.spec.ts
Normal file
235
modules/engine/test/dynamic-texture/texture-data.spec.ts
Normal file
@ -0,0 +1,235 @@
|
||||
import test from 'tape';
|
||||
|
||||
import {
|
||||
isTextureSliceData,
|
||||
getFirstMipLevel,
|
||||
getTextureSizeFromData,
|
||||
type TextureImageData,
|
||||
type TextureDataProps,
|
||||
type TextureCubeFace,
|
||||
type TextureSliceData
|
||||
} from '../../src/dynamic-texture/texture-data';
|
||||
|
||||
import type {} from '../../src/dynamic-texture/dynamic-texture';
|
||||
|
||||
import {isExternalImage, getExternalImageSize} from '@luma.gl/core';
|
||||
|
||||
test('isTextureSliceData: typed array image vs not', t => {
|
||||
t.equal(
|
||||
isTextureSliceData(mkImageData(4, 2)),
|
||||
true,
|
||||
'true for TextureImageData with typed array'
|
||||
);
|
||||
|
||||
// Random non-image objects
|
||||
t.equal(isTextureSliceData({} as any), false, 'false for random object');
|
||||
t.equal(isTextureSliceData([] as any), false, 'false for array');
|
||||
t.equal(isTextureSliceData(null as any), false, 'false for null');
|
||||
|
||||
// If environment provides an ExternalImage, ensure it returns false here
|
||||
const image = maybeMakeExternalImage();
|
||||
if (image) {
|
||||
t.equal(isExternalImage(image), true, 'isExternalImage: external image is detected by luma');
|
||||
t.equal(isTextureSliceData(image), true, 'isTextureSliceData: ExternalImage');
|
||||
} else {
|
||||
t.comment(
|
||||
'ExternalImage test skipped (no native external image type available in this environment).'
|
||||
);
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getFirstMipLevel: single, array, empty', t => {
|
||||
const m0 = mkImageData(8, 8);
|
||||
t.equal(getFirstMipLevel(m0), m0, 'returns item when single mip');
|
||||
|
||||
const m1 = mkImageData(4, 4);
|
||||
t.equal(getFirstMipLevel([m0, m1]), m0, 'returns first when array');
|
||||
|
||||
t.equal(getFirstMipLevel(null), null, 'null input → null');
|
||||
t.equal(getFirstMipLevel([]), null, 'empty array → null');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getTextureSizeFromData: 1d', t => {
|
||||
const props: TextureDataProps = {dimension: '1d', data: mkImageData(32, 1)};
|
||||
t.deepEqual(getTextureSizeFromData(props), {width: 32, height: 1});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getTextureSizeFromData: 2d (single & mips)', t => {
|
||||
t.deepEqual(
|
||||
getTextureSizeFromData({dimension: '2d', data: mkImageData(64, 32)}),
|
||||
{width: 64, height: 32},
|
||||
'2d single mip'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
getTextureSizeFromData({dimension: '2d', data: [mkImageData(64, 64), mkImageData(32, 32)]}),
|
||||
{width: 64, height: 64},
|
||||
'2d first mip from array'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getTextureSizeFromData: 2d with ExternalImage (env-dependent)', t => {
|
||||
const ext = maybeMakeExternalImage();
|
||||
if (!ext) {
|
||||
t.comment('Skipping 2d ExternalImage size test (no native external image available).');
|
||||
t.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanity check: luma reports correct size
|
||||
const reported = getExternalImageSize(ext);
|
||||
t.ok(
|
||||
reported && typeof reported.width === 'number' && typeof reported.height === 'number',
|
||||
'getExternalImageSize returns width/height'
|
||||
);
|
||||
|
||||
const props: TextureDataProps = {dimension: '2d', data: ext};
|
||||
t.deepEqual(
|
||||
getTextureSizeFromData(props),
|
||||
reported,
|
||||
'uses getExternalImageSize for ExternalImage'
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getTextureSizeFromData: 3d & 2d-array', t => {
|
||||
const threeD = [
|
||||
[mkImageData(16, 8), mkImageData(8, 4)], // depth slice 0 (with mips)
|
||||
[mkImageData(16, 8)] // depth slice 1
|
||||
];
|
||||
t.deepEqual(
|
||||
getTextureSizeFromData({dimension: '3d', data: threeD as any}),
|
||||
{width: 16, height: 8},
|
||||
'3d first slice, first mip'
|
||||
);
|
||||
|
||||
const arr = [mkImageData(20, 10), mkImageData(20, 10)];
|
||||
t.deepEqual(
|
||||
getTextureSizeFromData({dimension: '2d-array', data: arr as any}),
|
||||
{width: 20, height: 10},
|
||||
'2d-array first layer, first mip'
|
||||
);
|
||||
|
||||
t.equal(getTextureSizeFromData({dimension: '3d', data: [] as any}), null, '3d empty → null');
|
||||
t.equal(
|
||||
getTextureSizeFromData({dimension: '2d-array', data: [] as any}),
|
||||
null,
|
||||
'2d-array empty → null'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getTextureSizeFromData: cube & cube-array', t => {
|
||||
const cube: Record<TextureCubeFace, TextureSliceData> = {
|
||||
'+X': mkImageData(128, 128),
|
||||
'-X': mkImageData(128, 128),
|
||||
'+Y': mkImageData(128, 128),
|
||||
'-Y': mkImageData(128, 128),
|
||||
'+Z': mkImageData(128, 128),
|
||||
'-Z': mkImageData(128, 128)
|
||||
};
|
||||
t.deepEqual(
|
||||
getTextureSizeFromData({dimension: 'cube', data: cube}),
|
||||
{width: 128, height: 128},
|
||||
'cube: picks first face first mip'
|
||||
);
|
||||
|
||||
const cube0: Record<TextureCubeFace, TextureSliceData> = {
|
||||
'+X': [mkImageData(64, 64), mkImageData(32, 32)],
|
||||
'-X': mkImageData(64, 64),
|
||||
'+Y': mkImageData(64, 64),
|
||||
'-Y': mkImageData(64, 64),
|
||||
'+Z': mkImageData(64, 64),
|
||||
'-Z': mkImageData(64, 64)
|
||||
};
|
||||
const cube1: Record<TextureCubeFace, TextureSliceData> = {
|
||||
'+X': mkImageData(32, 32),
|
||||
'-X': mkImageData(32, 32),
|
||||
'+Y': mkImageData(32, 32),
|
||||
'-Y': mkImageData(32, 32),
|
||||
'+Z': mkImageData(32, 32),
|
||||
'-Z': mkImageData(32, 32)
|
||||
};
|
||||
t.deepEqual(
|
||||
getTextureSizeFromData({dimension: 'cube-array', data: [cube0, cube1]}),
|
||||
{width: 64, height: 64},
|
||||
'cube-array: first cube, first face, first mip'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
getTextureSizeFromData({dimension: 'cube', data: {} as any}),
|
||||
null,
|
||||
'cube empty map → null'
|
||||
);
|
||||
t.equal(
|
||||
getTextureSizeFromData({dimension: 'cube-array', data: [] as any}),
|
||||
null,
|
||||
'cube-array empty → null'
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getTextureSizeFromData: invalid 2d payload throws', t => {
|
||||
// When first mip is neither ExternalImage nor has width/height keys.
|
||||
const bad: any = {foo: 'bar'};
|
||||
t.throws(
|
||||
() => getTextureSizeFromData({dimension: '2d', data: bad}),
|
||||
/Unsupported mip-level data/,
|
||||
'throws for unsupported mip-level data'
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
// ---------------- Helpers ----------------
|
||||
|
||||
function mkImageData(width: number, height: number, bytes = width * height * 4): TextureImageData {
|
||||
return {
|
||||
data: new Uint8Array(bytes),
|
||||
width,
|
||||
height,
|
||||
format: 'rgba8unorm'
|
||||
};
|
||||
}
|
||||
|
||||
function maybeMakeExternalImage(): any | null {
|
||||
// Try to build a native "ExternalImage" that luma.gl recognizes in your env.
|
||||
// Prefer ImageData (widely available in browsers, sometimes available under jsdom),
|
||||
// then OffscreenCanvas/ImageBitmap if present.
|
||||
try {
|
||||
// ImageData path
|
||||
if (typeof ImageData !== 'undefined') {
|
||||
const imgData = new ImageData(4, 3);
|
||||
if (isExternalImage(imgData)) {
|
||||
return imgData;
|
||||
}
|
||||
}
|
||||
|
||||
// OffscreenCanvas -> transferToImageBitmap
|
||||
if (
|
||||
typeof OffscreenCanvas !== 'undefined' &&
|
||||
typeof (globalThis as any).createImageBitmap === 'function'
|
||||
) {
|
||||
const oc = new OffscreenCanvas(5, 7);
|
||||
const bmp = (globalThis as any).createImageBitmap(oc);
|
||||
// Note: createImageBitmap may return a Promise in some envs; bail if async
|
||||
if (bmp && typeof bmp.then !== 'function' && isExternalImage(bmp)) {
|
||||
return bmp;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// TODO - Could add HTMLCanvasElement/HTMLImageElement branches for browser runs if desired.
|
||||
|
||||
return null; // environment doesn’t provide a suitable external image type
|
||||
}
|
||||
@ -15,6 +15,7 @@ import './lib/shader-factory.spec';
|
||||
|
||||
import './dynamic-texture/dynamic-texture.spec';
|
||||
import './dynamic-texture/mip-levels.spec';
|
||||
import './dynamic-texture/texture-data.spec';
|
||||
|
||||
import './geometry/geometries.spec';
|
||||
import './geometry/geometry.spec';
|
||||
@ -39,3 +40,4 @@ import './compute/swap.spec';
|
||||
import './compute/buffer-transform.spec';
|
||||
import './compute/texture-transform.spec';
|
||||
import './compute/computation.spec';
|
||||
import './passes/shader-pass-renderer.spec';
|
||||
|
||||
64
modules/engine/test/passes/shader-pass-renderer.spec.ts
Normal file
64
modules/engine/test/passes/shader-pass-renderer.spec.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// luma.gl
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) vis.gl contributors
|
||||
|
||||
import test from 'tape-promise/tape';
|
||||
import {getTestDevices} from '@luma.gl/test-utils';
|
||||
import {ShaderPassRenderer, DynamicTexture, ShaderInputs} from '@luma.gl/engine';
|
||||
import type {ShaderPass} from '@luma.gl/shadertools';
|
||||
import {Texture} from '@luma.gl/core';
|
||||
|
||||
const invertPass: ShaderPass = {
|
||||
name: 'invert',
|
||||
source: /* wgsl */ `
|
||||
fn invert_filterColor_ext(color: vec4f, texSize: vec2f, texCoord: vec2f) -> vec4f {
|
||||
return vec4f(1.0 - color.rgb, color.a);
|
||||
}
|
||||
`,
|
||||
fs: /* glsl */ `
|
||||
vec4 invert_filterColor_ext(vec4 color, vec2 texSize, vec2 texCoord) {
|
||||
return vec4(1.0 - color.rgb, color.a);
|
||||
}
|
||||
`,
|
||||
passes: [{filter: true}]
|
||||
};
|
||||
|
||||
test('ShaderPassRenderer#renderToTexture', async t => {
|
||||
const devices = await getTestDevices();
|
||||
for (const device of devices) {
|
||||
// TODO - fix, we are getting close
|
||||
if (device.type === 'webgpu') {
|
||||
continue; // eslint-disable-line no-continue
|
||||
}
|
||||
t.comment(`Testing ${device.type}`);
|
||||
const sourceTexture = new DynamicTexture(device, {
|
||||
id: 'source-texture',
|
||||
usage: Texture.RENDER | Texture.COPY_SRC | Texture.COPY_DST,
|
||||
dimension: '2d',
|
||||
data: {data: new Uint8Array([255, 0, 0, 255]), width: 1, height: 1, format: 'rgba8unorm'}
|
||||
});
|
||||
await sourceTexture.ready;
|
||||
|
||||
// Sanity check
|
||||
const arrayBuffer = await sourceTexture.texture.readDataAsync();
|
||||
const pixels1 = new Uint8Array(arrayBuffer, 0, 4); // slice away WebGPU padding
|
||||
t.deepEqual(Array.from(pixels1), [255, 0, 0, 255], 'initialization success');
|
||||
|
||||
const shaderInputs = new ShaderInputs({invert: invertPass});
|
||||
const renderer = new ShaderPassRenderer(device, {
|
||||
shaderPasses: [invertPass],
|
||||
shaderInputs
|
||||
});
|
||||
const output = renderer.renderToTexture({sourceTexture});
|
||||
|
||||
t.ok(output, 'produces output texture');
|
||||
|
||||
const arrayBufferOut = await output!.readDataAsync();
|
||||
const pixelsOut = new Uint8Array(arrayBufferOut, 0, 4); // slice away WebGPU padding
|
||||
t.deepEqual(Array.from(pixelsOut), [0, 255, 255, 255], 'applies filter');
|
||||
|
||||
renderer.destroy();
|
||||
sourceTexture.destroy();
|
||||
}
|
||||
t.end();
|
||||
});
|
||||
@ -130,6 +130,7 @@ export class WebGPUTexture extends Texture {
|
||||
const {width, height, depth} = this;
|
||||
const options = this._normalizeCopyImageDataOptions(options_);
|
||||
this.device.pushErrorScope('validation');
|
||||
|
||||
this.device.handle.queue.writeTexture(
|
||||
// destination: GPUImageCopyTexture
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user