gpu-curtains
Version:
gpu-curtains is a 3D WebGPU rendering engine. It can be used as a standalone 3D engine, but also includes extra classes focused on mapping 3d objects to DOM elements; It allows users to synchronize values such as position, sizing, or scale between them.
266 lines (265 loc) • 9.93 kB
JavaScript
import { generateUUID } from "../../utils/utils.mjs";
import { isRenderer } from "../renderers/utils.mjs";
import { TextureBinding } from "../bindings/TextureBinding.mjs";
import { getDefaultTextureUsage, getNumMipLevels } from "./utils.mjs";
//#region src/core/textures/Texture.ts
/** @const - default {@link Texture} parameters. */
const defaultTextureParams = {
label: "Texture",
name: "renderTexture",
type: "texture",
access: "write",
fromTexture: null,
viewDimension: "2d",
sampleCount: 1,
qualityRatio: 1,
generateMips: false,
useMips: false,
flipY: false,
premultipliedAlpha: false,
aspect: "all",
colorSpace: "srgb",
autoDestroy: true
};
/**
* This is the main class used to create and handle {@link GPUTexture | textures} that can be used with {@link core/computePasses/ComputePass.ComputePass | ComputePass} and/or {@link core/meshes/Mesh.Mesh | Mesh}. Also used as copy source/destination for {@link core/renderPasses/RenderPass.RenderPass | RenderPass} and {@link core/renderPasses/RenderTarget.RenderTarget | RenderTarget}.
*
* Mostly useful to handle depth and storages textures or to copy anything outputted to the screen at one point or another. It can handle basic data or image bitmap upload, but for external sources textures (like images, videos or canvases) you should use the {@link MediaTexture} class instead.
*
* Will create a {@link GPUTexture} and its associated {@link TextureBinding}.
*
* @example
* ```javascript
* // create a texture
* // assuming 'renderer' is a valid GPURenderer
* const texture = new Texture(renderer, {
* label: 'My texture',
* name: 'myTexture',
* })
* ```
*/
var Texture = class {
/** Whether this texture should be automatically resized when the {@link Renderer renderer} size changes. Default to true. */
#autoResize = true;
/**
* Texture constructor
* @param renderer - {@link Renderer | renderer} object or {@link GPUCurtains} class object used to create this {@link Texture}.
* @param parameters - {@link TextureParams | parameters} used to create this {@link Texture}.
*/
constructor(renderer, parameters = defaultTextureParams) {
renderer = isRenderer(renderer, parameters.label ? parameters.label + " Texture" : "Texture");
this.type = "Texture";
this.renderer = renderer;
this.uuid = generateUUID();
this.options = {
...defaultTextureParams,
...parameters
};
if (this.options.generateMips) this.options.useMips = true;
if (this.options.format === "rgba32float" && this.renderer.device && !this.renderer.device.features.has("float32-filterable")) this.options.format = "rgba16float";
if (parameters.fromTexture) {
this.options.format = parameters.fromTexture.texture.format;
this.options.sampleCount = parameters.fromTexture.texture.sampleCount;
this.options.viewDimension = parameters.fromTexture.options.viewDimension;
}
if (!this.options.format) this.options.format = this.renderer.options.context.format;
const { width, height } = this.renderer.canvas;
this.size = this.options.fixedSize ? {
width: this.options.fixedSize.width * this.options.qualityRatio,
height: this.options.fixedSize.height * this.options.qualityRatio,
depth: this.options.fixedSize.depth ?? this.options.viewDimension.indexOf("cube") !== -1 ? 6 : 1
} : {
width: Math.floor(width * this.options.qualityRatio),
height: Math.floor(height * this.options.qualityRatio),
depth: this.options.viewDimension.indexOf("cube") !== -1 ? 6 : 1
};
if (this.options.fixedSize) this.#autoResize = false;
this.setBindings();
this.renderer.addTexture(this);
this.createTexture();
}
/**
* Reset this {@link Texture} {@link Texture.renderer | renderer}, and resize it if needed.
* @param renderer - New {@link Renderer} or {@link GPUCurtains} instance to use.
*/
setRenderer(renderer) {
if (this.renderer) this.renderer.removeTexture(this);
renderer = isRenderer(renderer, this.options.label + " Texture");
this.renderer = renderer;
this.renderer.addTexture(this);
const { width, height } = this.renderer.canvas;
if (this.#autoResize && (this.size.width !== width * this.options.qualityRatio || this.size.height !== height * this.options.qualityRatio)) this.resize();
}
/**
* Set our {@link Texture#bindings | bindings}.
*/
setBindings() {
this.bindings = [new TextureBinding({
label: this.options.label + ": " + this.options.name + " texture",
name: this.options.name,
bindingType: this.options.type,
visibility: this.options.visibility,
texture: this.texture,
format: this.options.format,
viewDimension: this.options.viewDimension,
multisampled: this.options.sampleCount > 1
})];
}
/**
* Get our {@link TextureBinding | texture binding}.
* @readonly
*/
get textureBinding() {
return this.bindings[0];
}
/**
* Copy another {@link Texture} into this {@link Texture}.
* @param texture - {@link Texture} to copy.
*/
copy(texture) {
this.options.fromTexture = texture;
this.createTexture();
}
/**
* Copy a {@link GPUTexture} directly into this {@link Texture}. Mainly used for depth textures.
* @param texture - {@link GPUTexture} to copy.
*/
copyGPUTexture(texture) {
this.size = {
width: texture.width,
height: texture.height,
depth: texture.depthOrArrayLayers
};
this.options.format = texture.format;
this.options.sampleCount = texture.sampleCount;
this.texture = texture;
this.textureBinding.setFormat(this.options.format);
this.textureBinding.setMultisampled(this.options.sampleCount > 1);
this.textureBinding.resource = this.texture;
}
/**
* Create the {@link GPUTexture | texture} (or copy it from source) and update the {@link TextureBinding#resource | binding resource}.
*/
createTexture() {
if (!this.renderer.device || !this.size.width || !this.size.height) return;
if (this.options.fromTexture) {
this.copyGPUTexture(this.options.fromTexture.texture);
return;
}
this.texture?.destroy();
this.texture = this.renderer.createTexture({
label: this.options.label,
format: this.options.format,
size: [
this.size.width,
this.size.height,
this.size.depth ?? 1
],
dimension: this.options.viewDimension.indexOf("1d") !== -1 ? "1d" : this.options.viewDimension.indexOf("3d") !== -1 ? "3d" : "2d",
sampleCount: this.options.sampleCount,
mipLevelCount: this.options.useMips ? getNumMipLevels(this.size.width, this.size.height, this.size.depth ?? 1) : 1,
usage: getDefaultTextureUsage(this.options.usage, this.options.type),
textureBindingViewDimension: this.options.viewDimension
});
this.textureBinding.resource = this.texture;
}
/**
* Upload a source to the GPU and use it for our {@link texture}.
* @param parameters - parameters used to upload the source.
* @param parameters.source - source to use for our {@link texture}.
* @param parameters.width - source width.
* @param parameters.height - source height.
* @param parameters.depth - source depth.
* @param parameters.origin - {@link GPUQueue.copyExternalImageToTexture().destination.origin | GPUOrigin3D} of the source copy.
*/
uploadSource({ source, width = this.size.width, height = this.size.height, depth = this.size.depth, origin = [
0,
0,
0
], colorSpace = "srgb" }) {
this.renderer.deviceManager.copyExternalImageToTexture({
source,
flipY: this.options.flipY
}, {
texture: this.texture,
premultipliedAlpha: this.options.premultipliedAlpha,
origin,
colorSpace
}, [
width,
height,
depth
]);
if (this.texture.mipLevelCount > 1) this.renderer.generateMips(this);
}
/**
* Use data as the {@link texture} source and upload it to the GPU.
* @param parameters - parameters used to upload the source.
* @param parameters.width - data source width.
* @param parameters.height - data source height.
* @param parameters.depth - data source depth.
* @param parameters.origin - {@link GPUQueue.copyExternalImageToTexture().destination.origin | GPUOrigin3D} of the data source copy.
* @param parameters.data - {@link Float32Array} data to use as source.
*/
uploadData({ width = this.size.width, height = this.size.height, depth = this.size.depth, origin = [
0,
0,
0
], data = new Float32Array(width * height * 4) }) {
this.renderer.device.queue.writeTexture({
texture: this.texture,
origin
}, data, {
bytesPerRow: width * data.BYTES_PER_ELEMENT * 4,
rowsPerImage: height
}, [
width,
height,
depth
]);
if (this.texture.mipLevelCount > 1) this.renderer.generateMips(this);
}
/**
* Update our {@link Texture} quality ratio and resize it.
* @param qualityRatio - New quality ratio to use.
*/
setQualityRatio(qualityRatio = 1) {
this.options.qualityRatio = qualityRatio;
this.resize();
}
/**
* Resize our {@link Texture}, which means recreate it/copy it again and tell the {@link core/bindGroups/TextureBindGroup.TextureBindGroup | texture bind group} to update.
* @param size - the optional new {@link TextureSize | size} to set.
*/
resize(size = null) {
if (!this.#autoResize) return;
if (!size) {
const { width, height } = this.renderer.canvas;
size = {
width: Math.floor(width * this.options.qualityRatio),
height: Math.floor(height * this.options.qualityRatio),
depth: this.size.depth
};
}
if (size.width === this.size.width && size.height === this.size.height && size.depth === this.size.depth) return;
this.setSize(size);
}
/**
* Set our {@link Texture} {@link Texture.size | size}, recreate it/copy it again and tell the {@link core/bindGroups/TextureBindGroup.TextureBindGroup | texture bind group} to update.
* @param size - the new {@link TextureSize | size} to set.
*/
setSize(size) {
this.size = size;
this.createTexture();
}
/**
* Destroy our {@link Texture}.
*/
destroy() {
this.renderer.removeTexture(this);
if (!this.options.fromTexture) this.texture?.destroy();
this.texture = null;
}
};
//#endregion
export { Texture };