import { Debug } from '../../core/debug.js'; import { TRACEID_TEXTURE_ALLOC, TRACEID_VRAM_TEXTURE } from '../../core/constants.js'; import { math } from '../../core/math/math.js'; import { isCompressedPixelFormat, getPixelFormatArrayType, ADDRESS_REPEAT, ADDRESS_CLAMP_TO_EDGE, FILTER_LINEAR, FILTER_LINEAR_MIPMAP_LINEAR, FUNC_LESS, PIXELFORMAT_RGBA8, TEXHINT_SHADOWMAP, TEXHINT_ASSET, TEXHINT_LIGHTMAP, TEXTURELOCK_WRITE, TEXTUREPROJECTION_NONE, TEXTUREPROJECTION_CUBE, TEXTURETYPE_DEFAULT, TEXTURETYPE_RGBM, TEXTURETYPE_RGBE, TEXTURETYPE_RGBP, TEXTURETYPE_SWIZZLEGGGR, isIntegerPixelFormat, FILTER_NEAREST, TEXTURELOCK_NONE, TEXTURELOCK_READ, TEXPROPERTY_MIN_FILTER, TEXPROPERTY_MAG_FILTER, TEXPROPERTY_ADDRESS_U, TEXPROPERTY_ADDRESS_V, TEXPROPERTY_ADDRESS_W, TEXPROPERTY_COMPARE_ON_READ, TEXPROPERTY_COMPARE_FUNC, TEXPROPERTY_ANISOTROPY, TEXPROPERTY_ALL, requiresManualGamma, pixelFormatInfo, isSrgbPixelFormat, pixelFormatLinearToGamma, pixelFormatGammaToLinear, isMultisampleCapablePixelFormat } from './constants.js'; import { TextureUtils } from './texture-utils.js'; import { TextureView } from './texture-view.js'; /** * @import { GraphicsDevice } from './graphics-device.js' * @import { RenderTarget } from './render-target.js' */ let id = 0; /** * Represents a texture, which is typically an image composed of pixels (texels). Textures are * fundamental resources for rendering graphical objects. They are commonly used by * {@link Material}s and sampled in {@link Shader}s (usually fragment shaders) to define the visual * appearance of a 3D model's surface. Beyond storing color images, textures can hold various data * types like normal maps, environment maps (cubemaps), or custom data for shader computations. Key * properties control how the texture data is sampled, including filtering modes and coordinate * wrapping. * * Note on **HDR texture format** support: * 1. **As textures**: * - float (i.e. {@link PIXELFORMAT_RGBA32F}), half-float (i.e. {@link PIXELFORMAT_RGBA16F}) and * small-float ({@link PIXELFORMAT_111110F}) formats are always supported on both WebGL2 and WebGPU * with point sampling. * - half-float and small-float formats are always supported on WebGL2 and WebGPU with linear * sampling. * - float formats are supported on WebGL2 and WebGPU with linear sampling only if * {@link GraphicsDevice#textureFloatFilterable} is true. * - {@link PIXELFORMAT_RGB9E5} is a compact HDR format with shared exponent, supported for * sampling on both WebGL2 and WebGPU, but cannot be used as a render target. * * 2. **As renderable textures** that can be used as color buffers in a {@link RenderTarget}: * - on WebGPU, rendering to float and half-float formats is always supported. * - on WebGPU, rendering to small-float format is supported only if * {@link GraphicsDevice#textureRG11B10Renderable} is true. * - on WebGL2, rendering to these 3 formats is supported only if * {@link GraphicsDevice#textureFloatRenderable} is true. * - on WebGL2, if {@link GraphicsDevice#textureFloatRenderable} is false, but * {@link GraphicsDevice#textureHalfFloatRenderable} is true, rendering to half-float formats only * is supported. This is the case of many mobile iOS devices. * - you can determine available renderable HDR format using * {@link GraphicsDevice#getRenderableHdrFormat}. * - {@link PIXELFORMAT_RGB10A2} provides 10 bits per RGB channel with 2-bit alpha, offering * higher precision than {@link PIXELFORMAT_RGBA8} at the same memory cost. It is renderable on * both WebGL2 and WebGPU. {@link PIXELFORMAT_RGB10A2U} is the unsigned integer variant. * * @category Graphics */ class Texture { /** * Creates a 2D data texture with nearest filtering, clamp-to-edge addressing and no mipmaps. * * @param {GraphicsDevice} graphicsDevice - The graphics device used to manage this texture. * @param {string} name - The name of the texture. * @param {number} width - The width of the texture in pixels. * @param {number} height - The height of the texture in pixels. * @param {number} format - The pixel format of the texture. * @param {Uint8Array[]|Uint8ClampedArray[]|Uint16Array[]|Uint32Array[]|Float32Array[]|HTMLCanvasElement[]|HTMLImageElement[]|HTMLVideoElement[]|Uint8Array[][]} [levels] * - Optional initial mip level data. * @returns {Texture} The created texture. * @ignore */ static createDataTexture2D(graphicsDevice, name, width, height, format, levels) { return new Texture(graphicsDevice, { name, width, height, format, mipmaps: false, minFilter: FILTER_NEAREST, magFilter: FILTER_NEAREST, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, levels }); } /** * The name of the texture. * * @type {string} */ name; /** @ignore */ _gpuSize = 0; /** @ignore */ releaseSourceAfterUpload = false; /** @protected */ id = id++; /** @protected */ _invalid = false; /** @protected */ _lockedLevel = -1; /** @protected */ _lockedMode = TEXTURELOCK_NONE; /** * A render version used to track the last time the texture properties requiring bind group * to be updated were changed. * * @ignore */ renderVersionDirty = 0; /** * A render version stamped each time the texture content is marked for upload to the GPU. * Unlike {@link renderVersionDirty} (which tracks property changes), this tracks pixel content * changes - including same-size video frame uploads - allowing consumers to detect when the * texture content has changed since they last used it. * * @ignore */ uploadVersion = 0; /** @protected */ _storage = false; /** * The number of MSAA samples of the texture, 1 if not multisampled. * * @type {number} * @protected */ _samples = 1; /** @protected */ _numLevels = 0; /** @protected */ _numLevelsRequested; /** * Create a new Texture instance. * * @param {GraphicsDevice} graphicsDevice - The graphics device used to manage this texture. * @param {object} [options] - Object for passing optional arguments. * @param {string} [options.name] - The name of the texture. Defaults to null. * @param {number} [options.width] - The width of the texture in pixels. Defaults to 4. * @param {number} [options.height] - The height of the texture in pixels. Defaults to 4. * @param {number} [options.depth] - The number of depth slices in a 3D texture. * @param {number} [options.format] - The pixel format of the texture. Can be: * * - {@link PIXELFORMAT_R8} * - {@link PIXELFORMAT_RG8} * - {@link PIXELFORMAT_RGB565} * - {@link PIXELFORMAT_RGBA5551} * - {@link PIXELFORMAT_RGBA4} * - {@link PIXELFORMAT_RGB8} * - {@link PIXELFORMAT_RGBA8} * - {@link PIXELFORMAT_DXT1} * - {@link PIXELFORMAT_DXT3} * - {@link PIXELFORMAT_DXT5} * - {@link PIXELFORMAT_RGB16F} * - {@link PIXELFORMAT_RGBA16F} * - {@link PIXELFORMAT_RGB32F} * - {@link PIXELFORMAT_RGBA32F} * - {@link PIXELFORMAT_ETC1} * - {@link PIXELFORMAT_PVRTC_2BPP_RGB_1} * - {@link PIXELFORMAT_PVRTC_2BPP_RGBA_1} * - {@link PIXELFORMAT_PVRTC_4BPP_RGB_1} * - {@link PIXELFORMAT_PVRTC_4BPP_RGBA_1} * - {@link PIXELFORMAT_111110F} * - {@link PIXELFORMAT_ASTC_4x4} * - {@link PIXELFORMAT_ATC_RGB} * - {@link PIXELFORMAT_ATC_RGBA} * * Defaults to {@link PIXELFORMAT_RGBA8}. * @param {boolean} [options.srgb] - When true, the texture is created in the sRGB variant of * the requested format, if one exists, and is automatically converted to linear space when * sampled. When the format has no sRGB variant, this option is ignored. Defaults to false. * @param {string} [options.projection] - The projection type of the texture, used when the * texture represents an environment. Can be: * * - {@link TEXTUREPROJECTION_NONE} * - {@link TEXTUREPROJECTION_CUBE} * - {@link TEXTUREPROJECTION_EQUIRECT} * - {@link TEXTUREPROJECTION_OCTAHEDRAL} * * Defaults to {@link TEXTUREPROJECTION_CUBE} if options.cubemap is true, otherwise * {@link TEXTUREPROJECTION_NONE}. * @param {number} [options.minFilter] - The minification filter type to use. Defaults to * {@link FILTER_LINEAR_MIPMAP_LINEAR}. * @param {number} [options.magFilter] - The magnification filter type to use. Defaults to * {@link FILTER_LINEAR}. * @param {number} [options.anisotropy] - The level of anisotropic filtering to use. Defaults * to 1. * @param {number} [options.addressU] - The repeat mode to use in the U direction. Defaults to * {@link ADDRESS_REPEAT}. * @param {number} [options.addressV] - The repeat mode to use in the V direction. Defaults to * {@link ADDRESS_REPEAT}. * @param {number} [options.addressW] - The repeat mode to use in the W direction. Defaults to * {@link ADDRESS_REPEAT}. * @param {boolean} [options.mipmaps] - When enabled try to generate or use mipmaps for this * texture. Default is true. * @param {number} [options.numLevels] - Specifies the number of mip levels to generate. If not * specified, the number is calculated based on the texture size. When this property is set, * the mipmaps property is ignored. * @param {boolean} [options.cubemap] - Specifies whether the texture is to be a cubemap. * Defaults to false. * @param {number} [options.arrayLength] - Specifies whether the texture is to be a 2D texture array. * When passed in as undefined or < 1, this is not an array texture. If >= 1, this is an array texture. * Defaults to undefined. * @param {boolean} [options.volume] - Specifies whether the texture is to be a 3D volume. * Defaults to false. * @param {string} [options.type] - Specifies the texture type. Can be: * * - {@link TEXTURETYPE_DEFAULT} * - {@link TEXTURETYPE_RGBM} * - {@link TEXTURETYPE_RGBE} * - {@link TEXTURETYPE_RGBP} * - {@link TEXTURETYPE_SWIZZLEGGGR} * * Defaults to {@link TEXTURETYPE_DEFAULT}. * @param {boolean} [options.flipY] - Specifies whether the texture should be flipped in the * Y-direction. Only affects textures with a source that is an image, canvas or video element. * Does not affect cubemaps, compressed textures or textures set from raw pixel data. Defaults * to false. * @param {boolean} [options.premultiplyAlpha] - If true, the alpha channel of the texture (if * present) is multiplied into the color channels. Defaults to false. * @param {boolean} [options.compareOnRead] - When enabled, and if texture format is * {@link PIXELFORMAT_DEPTH} or {@link PIXELFORMAT_DEPTHSTENCIL}, hardware PCF is enabled for * this texture, and you can get filtered results of comparison using texture() in your shader. * Defaults to false. * @param {number} [options.compareFunc] - Comparison function when compareOnRead is enabled. * Can be: * * - {@link FUNC_LESS} * - {@link FUNC_LESSEQUAL} * - {@link FUNC_GREATER} * - {@link FUNC_GREATEREQUAL} * - {@link FUNC_EQUAL} * - {@link FUNC_NOTEQUAL} * * Defaults to {@link FUNC_LESS}. * @param {Uint8Array[]|Uint8ClampedArray[]|Uint16Array[]|Uint32Array[]|Float32Array[]|HTMLCanvasElement[]|HTMLImageElement[]|ImageBitmap[]|HTMLVideoElement[]|Uint8Array[][]|Uint8ClampedArray[][]|Uint16Array[][]|Uint32Array[][]|Float32Array[][]} [options.levels] * - Array of Uint8Array or other supported browser interface; or a two-dimensional array * of Uint8Array or other typed array if options.arrayLength is defined and greater than zero. * @param {boolean} [options.storage] - Defines if texture can be used as a storage texture by * a compute shader. Defaults to false. * @param {number} [options.samples] - The number of MSAA samples. A value greater than 1 * creates a multisampled texture (WebGPU only, ignored with a warning on other devices, and * rounded up to the device's supported sample count). A multisampled texture can only be * rendered into, and its individual samples read in a shader using `textureLoad` - it cannot * be sampled with a sampler, uploaded to or read back. It must be a 2D non-array * texture with a format that supports multisampling, cannot be a storage texture, and has no * mipmaps (the mipmaps option is ignored). Defaults to 1. * @example * // Create a 8x8x24-bit texture * const texture = new Texture(graphicsDevice, { * width: 8, * height: 8, * format: PIXELFORMAT_RGB8 * }); * * // Fill the texture with a gradient * const pixels = texture.lock(); * const count = 0; * for (let i = 0; i < 8; i++) { * for (let j = 0; j < 8; j++) { * pixels[count++] = i * 32; * pixels[count++] = j * 32; * pixels[count++] = 255; * } * } * texture.unlock(); */ constructor(graphicsDevice, options = {}) { this.device = graphicsDevice; Debug.assert(this.device, 'Texture constructor requires a graphicsDevice to be valid'); Debug.assert(!options.width || Number.isInteger(options.width), 'Texture width must be an integer number, got', options); Debug.assert(!options.height || Number.isInteger(options.height), 'Texture height must be an integer number, got', options); Debug.assert(!options.depth || Number.isInteger(options.depth), 'Texture depth must be an integer number, got', options); this.name = options.name ?? ''; this._width = Math.floor(options.width ?? 4); this._height = Math.floor(options.height ?? 4); // when srgb is requested, the texture is created in the sRGB variant of the format, if // one exists - this avoids an expensive runtime format switch when it is first used as such const format = options.format ?? PIXELFORMAT_RGBA8; this._format = options.srgb ? pixelFormatLinearToGamma(format) : format; this._compressed = isCompressedPixelFormat(this._format); this._integerFormat = isIntegerPixelFormat(this._format); if (this._integerFormat) { options.minFilter = FILTER_NEAREST; options.magFilter = FILTER_NEAREST; } this._volume = options.volume ?? false; this._depth = Math.floor(options.depth ?? 1); this._arrayLength = Math.floor(options.arrayLength ?? 0); this._storage = options.storage ?? false; this._cubemap = options.cubemap ?? false; this._flipY = options.flipY ?? false; this._premultiplyAlpha = options.premultiplyAlpha ?? false; // multisampled (MSAA) texture, WebGPU only - can only be rendered into, and read in // shaders using textureLoad const requestedSamples = options.samples ?? 1; if (requestedSamples > 1) { if (graphicsDevice.isWebGPU) { // WebGPU only supports sample counts of 1 or 4 this._samples = graphicsDevice.maxSamples; Debug.assert(!this._volume && !this._cubemap && this._arrayLength === 0, `Multisampled texture '${this.name}' must be a 2D non-array texture.`, this); Debug.assert(!this._storage, `Multisampled texture '${this.name}' cannot be a storage texture.`, this); Debug.assert(!options.levels, `Multisampled texture '${this.name}' cannot be created with initial data, it can only be rendered into.`, this); Debug.assert(options.numLevels === undefined, `Multisampled texture '${this.name}' cannot use the numLevels option, it always has a single mip level.`, this); Debug.assert(isMultisampleCapablePixelFormat(this._format), `Multisampled texture '${this.name}' uses format ${pixelFormatInfo.get(this._format)?.name}, which does not support multisampling.`, this); } else { Debug.warnOnce(`Texture '${this.name}' was created with samples > 1, which is only supported on WebGPU; the samples option is ignored.`); } } this._mipmaps = (options.mipmaps ?? true) && this._samples === 1; this._numLevelsRequested = options.numLevels; if (options.numLevels !== undefined) { this._numLevels = options.numLevels; } this._updateNumLevels(); this._minFilter = options.minFilter ?? FILTER_LINEAR_MIPMAP_LINEAR; this._magFilter = options.magFilter ?? FILTER_LINEAR; this._anisotropy = options.anisotropy ?? 1; this._addressU = options.addressU ?? ADDRESS_REPEAT; this._addressV = options.addressV ?? ADDRESS_REPEAT; this._addressW = options.addressW ?? ADDRESS_REPEAT; this._compareOnRead = options.compareOnRead ?? false; this._compareFunc = options.compareFunc ?? FUNC_LESS; this._type = options.type ?? TEXTURETYPE_DEFAULT; Debug.assert(!options.hasOwnProperty('rgbm'), 'Use options.type.'); Debug.assert(!options.hasOwnProperty('swizzleGGGR'), 'Use options.type.'); this.projection = TEXTUREPROJECTION_NONE; if (this._cubemap) { this.projection = TEXTUREPROJECTION_CUBE; } else if (options.projection && options.projection !== TEXTUREPROJECTION_CUBE) { this.projection = options.projection; } // #if _PROFILER this.profilerHint = options.profilerHint ?? 0; // #endif this._levels = options.levels; const upload = !!options.levels; if (!this._levels) { this._clearLevels(); } this.recreateImpl(upload); // a multisampled texture is never uploaded (the usual point where VRAM tracking is // updated), so account for its VRAM at creation; destroy() subtracts it if (this._samples > 1) { this._gpuSize = this.gpuSize; this.adjustVramSizeTracking(graphicsDevice._vram, this._gpuSize); } Debug.trace(TRACEID_TEXTURE_ALLOC, `Alloc: Id ${this.id} ${this.name}: ${this.width}x${this.height} [${pixelFormatInfo.get(this.format)?.name}]` + `${this.cubemap ? '[Cubemap]' : ''}` + `${this.volume ? '[Volume]' : ''}` + `${this.array ? '[Array]' : ''}` + `[MipLevels:${this.numLevels}]`, this); } /** * Frees resources associated with this texture. */ destroy() { Debug.trace(TRACEID_TEXTURE_ALLOC, `DeAlloc: Id ${this.id} ${this.name}`); const device = this.device; if (device) { device.onTextureDestroyed(this); // destroy implementation this.impl.destroy(device); // Update texture stats this.adjustVramSizeTracking(device._vram, -this._gpuSize); // Free CPU-side decoded pixel data if the owner has opted in via // setReleaseSourceAfterUpload; only safe when the source is engine-owned. if (this.releaseSourceAfterUpload) { this.releaseImageSources(); } this._levels = null; this.device = null; } } /** * Closes any ImageBitmaps held on `_levels` and nulls those entries. The GPU has its own * copy after upload, so the decoded pixels in CPU memory can be released. Safe to call only * when no subsequent re-upload from CPU source will be needed and the source is owned by * the engine (not shared with caller code or other textures). Clears the * `releaseSourceAfterUpload` flag so future uploads keep their sources by default. * * @ignore */ releaseImageSources() { this.releaseSourceAfterUpload = false; if (typeof ImageBitmap === 'undefined' || !this._levels) { return; } for (let i = 0; i < this._levels.length; i++) { const level = this._levels[i]; if (level instanceof ImageBitmap) { level.close(); this._levels[i] = null; } else if (Array.isArray(level)) { for (let j = 0; j < level.length; j++) { if (level[j] instanceof ImageBitmap) { level[j].close(); level[j] = null; } } } } } /** * One-shot opt-in: marks this texture so its CPU-side ImageBitmap source is released after * the next upload completes. The flag is cleared once the release runs, so callers must * re-arm after assigning a new source. The caller must own the ImageBitmap and guarantee * that no re-upload from CPU source will be needed (e.g. the owner re-creates the texture * on device loss). Used by the gsplat octree for streamed SOG textures. * * @ignore */ setReleaseSourceAfterUpload() { this.releaseSourceAfterUpload = true; // If upload has already happened, release eagerly. if (!this._needsUpload && !this._needsMipmapsUpload) { this.releaseImageSources(); } } recreateImpl(upload = true) { const { device } = this; // destroy existing this.impl?.destroy(device); this.impl = null; // create new this.impl = device.createTextureImpl(this); this.dirtyAll(); if (upload) { this.upload(); } } _clearLevels() { this._levels = this._cubemap ? [[null, null, null, null, null, null]] : [null]; } /** * Resizes the texture. This operation is supported for render target textures, and it resizes * the allocated buffer used for rendering, not the existing content of the texture. * * It is also supported for textures with data provided via the {@link lock} method. After * resizing, the appropriately sized data must be assigned by calling {@link lock} again. * * @param {number} width - The new width of the texture. * @param {number} height - The new height of the texture. * @param {number} [depth] - The new depth of the texture. Defaults to 1. * @ignore */ resize(width, height, depth = 1) { if (this.width !== width || this.height !== height || this.depth !== depth) { // destroy texture impl const device = this.device; this.adjustVramSizeTracking(device._vram, -this._gpuSize); this._gpuSize = 0; this.impl.destroy(device); this._clearLevels(); this._width = Math.floor(width); this._height = Math.floor(height); this._depth = Math.floor(depth); this._updateNumLevels(); // re-create the implementation this.impl = device.createTextureImpl(this); this.dirtyAll(); // a multisampled texture is never uploaded, so re-account for its VRAM here if (this._samples > 1) { this._gpuSize = this.gpuSize; this.adjustVramSizeTracking(device._vram, this._gpuSize); } } } /** * Called when the rendering context was lost. It releases all context related resources. * * @ignore */ loseContext() { this.impl.loseContext(); this.dirtyAll(); } /** * Updates vram size tracking for the texture, size can be positive to add or negative to subtract * * @ignore */ adjustVramSizeTracking(vram, size) { Debug.trace(TRACEID_VRAM_TEXTURE, `${this.id} ${this.name} size: ${size} vram.texture: ${vram.tex} => ${vram.tex + size}`); vram.tex += size; // #if _PROFILER if (this.profilerHint === TEXHINT_SHADOWMAP) { vram.texShadow += size; } else if (this.profilerHint === TEXHINT_ASSET) { vram.texAsset += size; } else if (this.profilerHint === TEXHINT_LIGHTMAP) { vram.texLightmap += size; } // #endif } propertyChanged(flag) { this.impl.propertyChanged(flag); this.renderVersionDirty = this.device.renderVersion; } _updateNumLevels() { const maxLevels = this.mipmaps ? TextureUtils.calcMipLevelsCount(this.width, this.height, this._volume ? this._depth : 1) : 1; const requestedLevels = this._numLevelsRequested; if (requestedLevels !== undefined && requestedLevels > maxLevels) { Debug.warn('Texture#numLevels: requested mip level count is greater than the maximum possible, will be clamped to', maxLevels, this); } this._numLevels = Math.min(requestedLevels ?? maxLevels, maxLevels); this._mipmaps = this._numLevels > 1; } /** * Returns the current lock mode. One of: * * - {@link TEXTURELOCK_NONE} * - {@link TEXTURELOCK_READ} * - {@link TEXTURELOCK_WRITE} * * @ignore * @type {number} */ get lockedMode() { return this._lockedMode; } /** * Sets the minification filter to be applied to the texture. Can be: * * - {@link FILTER_NEAREST} * - {@link FILTER_LINEAR} * - {@link FILTER_NEAREST_MIPMAP_NEAREST} * - {@link FILTER_NEAREST_MIPMAP_LINEAR} * - {@link FILTER_LINEAR_MIPMAP_NEAREST} * - {@link FILTER_LINEAR_MIPMAP_LINEAR} * * @type {number} */ set minFilter(v) { if (this._minFilter !== v) { if (isIntegerPixelFormat(this._format)) { Debug.warn('Texture#minFilter: minFilter property cannot be changed on an integer texture, will remain FILTER_NEAREST', this); } else { this._minFilter = v; this.propertyChanged(TEXPROPERTY_MIN_FILTER); } } } /** * Gets the minification filter to be applied to the texture. * * @type {number} */ get minFilter() { return this._minFilter; } /** * Sets the magnification filter to be applied to the texture. Can be: * * - {@link FILTER_NEAREST} * - {@link FILTER_LINEAR} * * @type {number} */ set magFilter(v) { if (this._magFilter !== v) { if (isIntegerPixelFormat(this._format)) { Debug.warn('Texture#magFilter: magFilter property cannot be changed on an integer texture, will remain FILTER_NEAREST', this); } else { this._magFilter = v; this.propertyChanged(TEXPROPERTY_MAG_FILTER); } } } /** * Gets the magnification filter to be applied to the texture. * * @type {number} */ get magFilter() { return this._magFilter; } /** * Sets the addressing mode to be applied to the texture horizontally. Can be: * * - {@link ADDRESS_REPEAT} * - {@link ADDRESS_CLAMP_TO_EDGE} * - {@link ADDRESS_MIRRORED_REPEAT} * * @type {number} */ set addressU(v) { if (this._addressU !== v) { this._addressU = v; this.propertyChanged(TEXPROPERTY_ADDRESS_U); } } /** * Gets the addressing mode to be applied to the texture horizontally. * * @type {number} */ get addressU() { return this._addressU; } /** * Sets the addressing mode to be applied to the texture vertically. Can be: * * - {@link ADDRESS_REPEAT} * - {@link ADDRESS_CLAMP_TO_EDGE} * - {@link ADDRESS_MIRRORED_REPEAT} * * @type {number} */ set addressV(v) { if (this._addressV !== v) { this._addressV = v; this.propertyChanged(TEXPROPERTY_ADDRESS_V); } } /** * Gets the addressing mode to be applied to the texture vertically. * * @type {number} */ get addressV() { return this._addressV; } /** * Sets the addressing mode to be applied to the 3D texture depth. Can be: * * - {@link ADDRESS_REPEAT} * - {@link ADDRESS_CLAMP_TO_EDGE} * - {@link ADDRESS_MIRRORED_REPEAT} * * @type {number} */ set addressW(addressW) { if (!this._volume) { Debug.warn('Texture#addressW: Can\'t set W addressing mode for a non-3D texture.'); return; } if (addressW !== this._addressW) { this._addressW = addressW; this.propertyChanged(TEXPROPERTY_ADDRESS_W); } } /** * Gets the addressing mode to be applied to the 3D texture depth. * * @type {number} */ get addressW() { return this._addressW; } /** * When enabled, and if texture format is {@link PIXELFORMAT_DEPTH} or * {@link PIXELFORMAT_DEPTHSTENCIL}, hardware PCF is enabled for this texture, and you can get * filtered results of comparison using texture() in your shader. * * @type {boolean} */ set compareOnRead(v) { if (this._compareOnRead !== v) { this._compareOnRead = v; this.propertyChanged(TEXPROPERTY_COMPARE_ON_READ); } } /** * Gets whether you can get filtered results of comparison using texture() in your shader. * * @type {boolean} */ get compareOnRead() { return this._compareOnRead; } /** * Sets the comparison function when {@link compareOnRead} is enabled. Possible values: * * - {@link FUNC_LESS} * - {@link FUNC_LESSEQUAL} * - {@link FUNC_GREATER} * - {@link FUNC_GREATEREQUAL} * - {@link FUNC_EQUAL} * - {@link FUNC_NOTEQUAL} * * @type {number} */ set compareFunc(v) { if (this._compareFunc !== v) { this._compareFunc = v; this.propertyChanged(TEXPROPERTY_COMPARE_FUNC); } } /** * Gets the comparison function when {@link compareOnRead} is enabled. * * @type {number} */ get compareFunc() { return this._compareFunc; } /** * Sets the integer value specifying the level of anisotropy to apply to the texture. The value * ranges from 1 (no anisotropic filtering) to the maximum anisotropy supported by the graphics * device (see {@link GraphicsDevice#maxAnisotropy}). * * @type {number} */ set anisotropy(v) { if (this._anisotropy !== v) { this._anisotropy = v; this.propertyChanged(TEXPROPERTY_ANISOTROPY); } } /** * Gets the integer value specifying the level of anisotropy to apply to the texture. * * @type {number} */ get anisotropy() { return this._anisotropy; } /** * Sets whether the texture should generate/upload mipmaps. Note that changing this property * on an array texture, or on any texture on WebGPU, re-creates the texture on the GPU, which * is an expensive operation, so it is preferable to create the texture with the correct * mipmaps setting from the start. * * @type {boolean} */ set mipmaps(v) { if (this._mipmaps !== v) { if (isIntegerPixelFormat(this._format)) { Debug.warn('Texture#mipmaps: mipmap property cannot be changed on an integer texture, will remain false', this); } else { const oldMipmaps = this._mipmaps; const oldNumLevels = this._numLevels; this._mipmaps = v; this._updateNumLevels(); // Array textures (and all textures on WebGPU) use immutable storage, so changing // the mip count requires re-creating the texture. if ((this.array || this.device.isWebGPU) && this._numLevels !== oldNumLevels) { Debug.warn(`Changing mipmaps of texture '${this.name}' requires it to be re-created. This is an expensive operation, and the texture should be created with the desired mipmaps setting to avoid this.`, this); this.recreateImpl(); } else if (this._mipmaps !== oldMipmaps) { this.propertyChanged(TEXPROPERTY_MIN_FILTER); if (this._mipmaps) { this._needsMipmapsUpload = true; this.device?.texturesToUpload?.add(this); } else { this._needsMipmapsUpload = false; } } } } } /** * Gets whether the texture should generate/upload mipmaps. * * @type {boolean} */ get mipmaps() { return this._mipmaps; } /** * Gets the number of mip levels. * * @type {number} */ get numLevels() { return this._numLevels; } /** * Defines if texture can be used as a storage texture by a compute shader. * * @type {boolean} */ get storage() { return this._storage; } /** * The number of MSAA samples of the texture, 1 if the texture is not multisampled. Specified * via the `samples` constructor option (WebGPU only). A multisampled texture can only be * rendered into, and its individual samples read in a shader using `textureLoad`. * * @type {number} */ get samples() { return this._samples; } /** * The width of the texture in pixels. * * @type {number} */ get width() { return this._width; } /** * The height of the texture in pixels. * * @type {number} */ get height() { return this._height; } /** * The number of depth slices in a 3D texture. * * @type {number} */ get depth() { return this._depth; } /** * The pixel format of the texture. Can be: * * - {@link PIXELFORMAT_R8} * - {@link PIXELFORMAT_RG8} * - {@link PIXELFORMAT_RGB565} * - {@link PIXELFORMAT_RGBA5551} * - {@link PIXELFORMAT_RGBA4} * - {@link PIXELFORMAT_RGB8} * - {@link PIXELFORMAT_RGBA8} * - {@link PIXELFORMAT_DXT1} * - {@link PIXELFORMAT_DXT3} * - {@link PIXELFORMAT_DXT5} * - {@link PIXELFORMAT_RGB16F} * - {@link PIXELFORMAT_RGBA16F} * - {@link PIXELFORMAT_RGB32F} * - {@link PIXELFORMAT_RGBA32F} * - {@link PIXELFORMAT_ETC1} * - {@link PIXELFORMAT_PVRTC_2BPP_RGB_1} * - {@link PIXELFORMAT_PVRTC_2BPP_RGBA_1} * - {@link PIXELFORMAT_PVRTC_4BPP_RGB_1} * - {@link PIXELFORMAT_PVRTC_4BPP_RGBA_1} * - {@link PIXELFORMAT_111110F} * - {@link PIXELFORMAT_ASTC_4x4} * - {@link PIXELFORMAT_ATC_RGB} * - {@link PIXELFORMAT_ATC_RGBA} * * @type {number} */ get format() { return this._format; } /** * Returns true if this texture is a cube map and false otherwise. * * @type {boolean} */ get cubemap() { return this._cubemap; } get gpuSize() { const mips = this._mipmaps && !(this._compressed && this._levels.length === 1); // unlike the depth of a volume texture, the layers of a texture array are not reduced by // the mip levels, and so each layer has a full mip chain const layers = Math.max(this._arrayLength, 1); return TextureUtils.calcGpuSize(this._width, this._height, this._depth, this._format, mips, this._cubemap) * layers * this._samples; } /** * Returns true if this texture is a 2D texture array and false otherwise. * * @type {boolean} */ get array() { return this._arrayLength > 0; } /** * Returns the number of textures inside this texture if this is a 2D array texture or 0 otherwise. * * @type {number} */ get arrayLength() { return this._arrayLength; } /** * Returns true if this texture is a 3D volume and false otherwise. * * @type {boolean} */ get volume() { return this._volume; } /** * Sets the texture type. * * @type {string} * @ignore */ set type(value) { if (this._type !== value) { this._type = value; // update all shaders to respect the encoding of the texture (needed by the standard material) this.device._shadersDirty = true; } } /** * Gets the texture type. * * @type {string} * @ignore */ get type() { return this._type; } /** * @deprecated Use Texture#type instead. * @ignore */ set rgbm(value) { Debug.deprecated('Texture#rgbm is deprecated. Use Texture#type instead.'); this.type = value ? TEXTURETYPE_RGBM : TEXTURETYPE_DEFAULT; } /** * @deprecated Use Texture#type instead. * @ignore */ get rgbm() { Debug.deprecated('Texture#rgbm is deprecated. Use Texture#type instead.'); return this.type === TEXTURETYPE_RGBM; } /** * @deprecated Use Texture#type instead. * @ignore */ set swizzleGGGR(value) { Debug.deprecated('Texture#swizzleGGGR is deprecated. Use Texture#type instead.'); this.type = value ? TEXTURETYPE_SWIZZLEGGGR : TEXTURETYPE_DEFAULT; } /** * @deprecated Use Texture#type instead. * @ignore */ get swizzleGGGR() { Debug.deprecated('Texture#swizzleGGGR is deprecated. Use Texture#type instead.'); return this.type === TEXTURETYPE_SWIZZLEGGGR; } get _glTexture() { Debug.deprecated('Texture#_glTexture is no longer available. Use Texture.impl._glTexture instead.'); return this.impl._glTexture; } /** * Sets the texture's internal format to an sRGB or linear equivalent of its current format. * When set to true, the texture is stored in sRGB format and automatically converted to linear * space when sampled. When set to false, the texture remains in a linear format. Changing this * property recreates the texture on the GPU, which is an expensive operation, so it is * preferable to create the texture with the correct format from the start. If the texture * format has no sRGB variant, this operation is ignored. * This is not a public API and is used by Editor only to update rendering when the sRGB * property is changed in the inspector. The higher cost is acceptable in this case. * * @type {boolean} * @ignore */ set srgb(value) { const currentSrgb = isSrgbPixelFormat(this.format); if (value !== currentSrgb) { if (value) { // switch to sRGB const srgbFormat = pixelFormatLinearToGamma(this.format); if (this._format !== srgbFormat) { Debug.warn(`Switching format of texture '${this.name}' to sRGB equivalent: ${pixelFormatInfo.get(this.format)?.name} -> ${pixelFormatInfo.get(srgbFormat)?.name}. This is an expensive operation, and the texture should be created using the right format to avoid this.`, this); this._format = srgbFormat; this.recreateImpl(); // update all shaders to respect the encoding of the texture (needed by the standard material) this.device._shadersDirty = true; } } else { // switch to linear const linearFormat = pixelFormatGammaToLinear(this.format); if (this._format !== linearFormat) { Debug.warn(`Switching format of texture '${this.name}' to linear equivalent: ${pixelFormatInfo.get(this.format)?.name} -> ${pixelFormatInfo.get(linearFormat)?.name}. This is an expensive operation, and the texture should be created using the right format to avoid this.`, this); this._format = linearFormat; this.recreateImpl(); // update all shaders to respect the encoding of the texture (needed by the standard material) this.device._shadersDirty = true; } } } } /** * Returns true if the texture is stored in an sRGB format, meaning it will be converted to * linear space when sampled. Returns false if the texture is stored in a linear format. * * @type {boolean} */ get srgb() { return isSrgbPixelFormat(this.format); } /** * Sets whether the texture should be flipped in the Y-direction. Only affects textures * with a source that is an image, canvas or video element. Does not affect cubemaps, * compressed textures or textures set from raw pixel data. Defaults to true. * * @type {boolean} */ set flipY(flipY) { if (this._flipY !== flipY) { this._flipY = flipY; this.markForUpload(); } } /** * Gets whether the texture should be flipped in the Y-direction. * * @type {boolean} */ get flipY() { return this._flipY; } set premultiplyAlpha(premultiplyAlpha) { if (this._premultiplyAlpha !== premultiplyAlpha) { this._premultiplyAlpha = premultiplyAlpha; this.markForUpload(); } } get premultiplyAlpha() { return this._premultiplyAlpha; } /** * Returns true if all dimensions of the texture are power of two, and false otherwise. * * @type {boolean} */ get pot() { return math.powerOfTwo(this._width) && math.powerOfTwo(this._height); } // get the texture's encoding type get encoding() { switch (this.type) { case TEXTURETYPE_RGBM: return 'rgbm'; case TEXTURETYPE_RGBE: return 'rgbe'; case TEXTURETYPE_RGBP: return 'rgbp'; } // note that the srgb part only makes sense for texture storing color data return requiresManualGamma(this.format) ? 'srgb' : 'linear'; } // Force a full resubmission of the texture to the GPU (used on a context restore event) dirtyAll() { this._levelsUpdated = this._cubemap ? [[true, true, true, true, true, true]] : [true]; this.markForUpload(); this._needsMipmapsUpload = this._mipmaps; this._mipmapsUploaded = false; this.propertyChanged(TEXPROPERTY_ALL); } /** * Locks a miplevel of the texture, returning a typed array to be filled with pixel data. * * @param {object} [options] - Optional options object. Valid properties are as follows: * @param {number} [options.level] - The mip level to lock with 0 being the top level. Defaults * to 0. * @param {number} [options.face] - If the texture is a cubemap, this is the index of the face * to lock. * @param {number} [options.mode] - The lock mode. Can be: * - {@link TEXTURELOCK_READ} * - {@link TEXTURELOCK_WRITE} * Defaults to {@link TEXTURELOCK_WRITE}. * @returns {Uint8Array|Uint16Array|Uint32Array|Float32Array} A typed array containing the pixel data of * the locked mip level. */ lock(options = {}) { // Initialize options to some sensible defaults options.level ??= 0; options.face ??= 0; options.mode ??= TEXTURELOCK_WRITE; Debug.assert(this._samples === 1, 'Cannot lock a multisampled texture.', this); Debug.assert( this._lockedMode === TEXTURELOCK_NONE, 'The texture is already locked. Call `texture.unlock()` before attempting to lock again.', this ); Debug.assert( options.mode === TEXTURELOCK_READ || options.mode === TEXTURELOCK_WRITE, 'Cannot lock a texture with TEXTURELOCK_NONE. To unlock a texture, call `texture.unlock()`.', this ); // cubemap levels are stored as [mip][face] const levels = this._cubemap ? (this._levels[options.level] ??= [null, null, null, null, null, null]) : this._levels; const index = this._cubemap ? options.face : options.level; if (!levels[index]) { // allocate storage for this mip level const width = Math.max(1, this._width >> options.level); const height = Math.max(1, this._height >> options.level); const depth = Math.max(1, this._depth >> options.level); const data = new ArrayBuffer(TextureUtils.calcLevelGpuSize(width, height, depth, this._format)); levels[index] = new (getPixelFormatArrayType(this._format))(data); } // WebGL only re-uploads the cubemap faces flagged as updated if (this._cubemap && options.mode === TEXTURELOCK_WRITE) { this._levelsUpdated[0][options.face] = true; } this._lockedMode = options.mode; this._lockedLevel = options.level; return levels[index]; } /** * Set the pixel data of the texture from a canvas, image, image bitmap, video, or HTML DOM * element. If the texture is a cubemap, the supplied source must be an array of 6 canvases, * images, image bitmaps or videos. * * Note: using an HTML element (e.g. `
`) as a source requires * {@link GraphicsDevice#supportsHtmlTextures} to be true. * * @param {HTMLCanvasElement|HTMLImageElement|ImageBitmap|HTMLVideoElement|HTMLElement|HTMLCanvasElement[]|HTMLImageElement[]|ImageBitmap[]|HTMLVideoElement[]|HTMLElement[]} source - A * canvas, image, image bitmap, video, or HTML element, or an array of 6 canvas, image, image * bitmap, video, or HTML elements. * @param {number} [mipLevel] - A non-negative integer specifying the image level of detail. * Defaults to 0, which represents the base image source. A level value of N, that is greater * than 0, represents the image source for the Nth mipmap reduction level. */ setSource(source, mipLevel = 0) { Debug.assert(this._samples === 1, 'Cannot set the source of a multisampled texture.', this); if (this.device._isHTMLElementInterface(source)) { if (!this.device.supportsHtmlTextures) { Debug.error('Texture#setSource: HTML element textures are not supported on this device. Check device.supportsHtmlTextures before calling setSource with an HTML element.'); return; } if (this._cubemap || this._volume) { Debug.error('Texture#setSource: HTML element textures can only be used with 2D textures, not cubemaps or volume textures.'); return; } } let invalid = false; let width, height; const prevWidth = this._width; const prevHeight = this._height; if (this._cubemap) { if (source[0]) { // rely on first face sizes width = source[0].width || 0; height = source[0].height || 0; for (let i = 0; i < 6; i++) { const face = source[i]; // cubemap becomes invalid if any condition is not satisfied if (!face || // face is missing face.width !== width || // face is different width face.height !== height || // face is different height !this.device._isBrowserInterface(face)) { // new image bitmap invalid = true; break; } } } else { // first face is missing invalid = true; } if (!invalid) { // mark levels as updated for (let i = 0; i < 6; i++) { if (this._levels[mipLevel][i] !== source[i]) { this._levelsUpdated[mipLevel][i] = true; } } } } else { // check if source is valid type of element if (!this.device._isBrowserInterface(source)) { invalid = true; } if (!invalid) { // mark level as updated if (source !== this._levels[mipLevel]) { this._levelsUpdated[mipLevel] = true; } if (typeof HTMLVideoElement !== 'undefined' && source instanceof HTMLVideoElement) { width = source.videoWidth; height = source.videoHeight; } else if (this.device._isHTMLElementInterface(source)) { const rect = source.getBoundingClientRect(); width = Math.floor(rect.width) || 1; height = Math.floor(rect.height) || 1; } else { width = source.width; height = source.height; } } } if (invalid) { // invalid texture // default sizes this._width = 4; this._height = 4; // remove levels if (this._cubemap) { for (let i = 0; i < 6; i++) { this._levels[mipLevel][i] = null; this._levelsUpdated[mipLevel][i] = true; } } else { this._levels[mipLevel] = null; this._levelsUpdated[mipLevel] = true; } } else { // valid texture if (mipLevel === 0) { this._width = width; this._height = height; } this._levels[mipLevel] = source; } // the mip chain follows the size of the base level, which the source may have changed if (this._width !== prevWidth || this._height !== prevHeight) { this._updateNumLevels(); } // valid or changed state of validity if (this._invalid !== invalid || !invalid) { this._invalid = invalid; // reupload this.upload(); } } /** * Get the pixel data of the texture. If this is a cubemap then an array of 6 images will be * returned otherwise a single image. * * @param {number} [mipLevel] - A non-negative integer specifying the image level of detail. * Defaults to 0, which represents the base image source. A level value of N, that is greater * than 0, represents the image source for the Nth mipmap reduction level. * @returns {HTMLImageElement} The source image of this texture. Can be null if source not * assigned for specific image level. */ getSource(mipLevel = 0) { return this._levels[mipLevel]; } /** * Unlocks the currently locked mip level and uploads it to VRAM. */ unlock() { if (this._lockedMode === TEXTURELOCK_NONE) { Debug.warn('Texture#unlock: Attempting to unlock a texture that is not locked.', this); } // Upload the new pixel data if locked in write mode (default) if (this._lockedMode === TEXTURELOCK_WRITE) { this.upload(); } this._lockedLevel = -1; this._lockedMode = TEXTURELOCK_NONE; } /** * Mark this texture as needing upload to the GPU. * * @ignore */ markForUpload() { this._needsUpload = true; // stamp the content version, allowing consumers (e.g. the clustered cookie atlas) to detect // that the content has changed since they last used it if (this.device) { this.uploadVersion = this.device.renderVersion; } this.device?.texturesToUpload?.add(this); } /** * Forces a reupload of the texture's pixel data to graphics memory. Ordinarily, this function * is called internally by {@link setSource} and {@link unlock}. However, it still needs to * be called explicitly in the case where an HTMLVideoElement is set as the source of the * texture. Normally, this is done once every frame before video textured geometry is * rendered. */ upload() { this.markForUpload(); this._needsMipmapsUpload = this._mipmaps; this.impl.uploadImmediate?.(this.device, this); } /** * Download the textures data from the graphics memory to the local memory. * * @param {number} x - The left edge of the rectangle. * @param {number} y - The top edge of the rectangle. * @param {number} width - The width of the rectangle. * @param {number} height - The height of the rectangle. * @param {object} [options] - Object for passing optional arguments. * @param {RenderTarget} [options.renderTarget] - The render target using the texture as a color * buffer. Provide as an optimization to avoid creating a new render target. Important especially * when this function is called with high frequency (per frame). Note that this is only utilized * on the WebGL platform, and ignored on WebGPU. * @param {number} [options.mipLevel] - The mip level to download. Defaults to 0. * @param {number} [options.face] - If the texture is a cubemap, the face to download. * Defaults to 0. * @param {number} [options.layer] - If the texture is a 2D array texture, the array layer to * download. Defaults to 0. * @param {number} [options.slice] - If the texture is a volume texture, the depth slice to * download. When not specified, all depth slices of the mip level are downloaded, one after * another, and the returned data holds `width * height * depth` pixels. * @param {Uint8Array|Uint16Array|Uint32Array|Float32Array} [options.data] - The data buffer to * write the pixel data to. If not provided, a new buffer will be created. The type of the buffer * must match the texture's format. * @param {boolean} [options.immediate] - If true, the read operation will be executed as soon as * possible. This has a performance impact, so it should be used only when necessary. Defaults * to false. * @param {boolean} [options.frequent] - Set this when the read is one of many, issued every * frame or every few frames. Such a read is given the treatment which costs it a frame of * latency and keeps it from stalling the frame it is issued in, which is the trade a one-off * read would not want. Only utilized on the WebGL platform, where a readback has a blocking * step; ignored on WebGPU, whose readback does not block. Defaults to false. * @returns {Promise} A promise that resolves * with the pixel data of the texture. */ read(x, y, width, height, options = {}) { Debug.assert(this._samples === 1, 'Cannot read back a multisampled texture.', this); Debug.call(() => { if (options.slice !== undefined) { const depth = TextureUtils.calcLevelDimension(this._depth, options.mipLevel ?? 0); Debug.assert(this._volume, `Texture#read: the slice option only applies to volume textures, '${this.name}' is not one.`, this); Debug.assert(options.slice >= 0 && options.slice < depth, `Texture#read: slice ${options.slice} is out of range of the ${depth} depth slices of '${this.name}'.`, this); } }); return this.impl.read?.(x, y, width, height, options); } /** * Upload texture data asynchronously to the GPU. * * @param {number} x - The left edge of the rectangle. * @param {number} y - The top edge of the rectangle. * @param {number} width - The width of the rectangle. * @param {number} height - The height of the rectangle. * @param {Uint8Array|Uint16Array|Uint32Array|Float32Array} data - The pixel data to upload. This should be a typed array. * * @returns {Promise} A promise that resolves when the upload is complete. * @ignore */ write(x, y, width, height, data) { return this.impl.write?.(x, y, width, height, data); } /** * Validates the parameters of a {@link Texture#copy} operation. * * @param {Texture} source - The source texture. * @param {object} options - The copy options (see {@link Texture#copy}). * @returns {boolean} True if the copy parameters are valid. * @private */ _validateCopy(source, options) { // #if _DEBUG if (!source) { Debug.error('Texture#copy: a source texture must be provided.'); return false; } if (source._format !== this._format) { Debug.error(`Texture#copy: source and destination formats must match (source '${source.name}', destination '${this.name}').`); return false; } if (source._compressed || this._compressed) { Debug.error('Texture#copy: copying compressed textures is not supported.'); return false; } if (source._volume !== this._volume) { Debug.error(`Texture#copy: a volume texture can only be copied to or from another volume texture (source '${source.name}', destination '${this.name}').`); return false; } if (options.slice !== undefined && !source._volume) { Debug.error('Texture#copy: the slice option only applies to volume textures.'); return false; } if (source._samples !== this._samples) { Debug.error(`Texture#copy: source and destination sample counts must match (source '${source.name}' has ${source._samples}, destination '${this.name}' has ${this._samples}). A multisampled texture cannot be copied to or from a single-sampled texture - use a resolve instead.`); return false; } const sourceMipLevel = options.sourceMipLevel ?? 0; const destMipLevel = options.destMipLevel ?? 0; if (sourceMipLevel < 0 || sourceMipLevel >= source.numLevels) { Debug.error(`Texture#copy: sourceMipLevel ${sourceMipLevel} is out of range (source has ${source.numLevels} levels).`); return false; } if (destMipLevel < 0 || destMipLevel >= this.numLevels) { Debug.error(`Texture#copy: destMipLevel ${destMipLevel} is out of range (destination has ${this.numLevels} levels).`); return false; } // number of array layers / cubemap faces Debug.assert(options.face === undefined || options.layer === undefined, 'Texture#copy: options.face and options.layer must not be used together.'); if (options.face !== undefined && (source.array || this.array)) { Debug.deprecated('Texture#copy: the face option selecting a layer of a texture array is deprecated, use the layer option instead.'); } const sourceLayers = source.cubemap ? 6 : Math.max(1, source.arrayLength); const destLayers = this.cubemap ? 6 : Math.max(1, this.arrayLength); const layer = options.layer ?? options.face ?? 0; if (layer < 0 || layer >= sourceLayers || layer >= destLayers) { Debug.error(`Texture#copy: face / layer ${layer} is out of range.`); return false; } // depth slices of volume textures - a single slice, or all slices of the source mip level if (source._volume) { const sourceDepth = Math.max(1, source.depth >> sourceMipLevel); const destDepth = Math.max(1, this.depth >> destMipLevel); if (options.slice !== undefined) { if (options.slice < 0 || options.slice >= sourceDepth || options.slice >= destDepth) { Debug.error(`Texture#copy: slice ${options.slice} is out of range (source has ${sourceDepth} depth slices, destination has ${destDepth}).`); return false; } } else if (sourceDepth > destDepth) { Debug.error(`Texture#copy: the ${sourceDepth} depth slices of the source do not fit into the ${destDepth} depth slices of the destination. Use the slice option to copy a single slice.`); return false; } } // region bounds, evaluated at the chosen mip levels const sw = Math.max(1, source.width >> sourceMipLevel); const sh = Math.max(1, source.height >> sourceMipLevel); const dw = Math.max(1, this.width >> destMipLevel); const dh = Math.max(1, this.height >> destMipLevel); const sx = options.sourceX ?? 0; const sy = options.sourceY ?? 0; const dx = options.destX ?? 0; const dy = options.destY ?? 0; const w = options.width ?? (sw - sx); const h = options.height ?? (sh - sy); if (w <= 0 || h <= 0 || sx < 0 || sy < 0 || dx < 0 || dy < 0 || sx + w > sw || sy + h > sh || dx + w > dw || dy + h > dh) { Debug.error(`Texture#copy: copy region is out of bounds (source ${sw}x${sh}, destination ${dw}x${dh}).`); return false; } // WebGPU requires copies involving multisampled textures to cover the entire texture if (this._samples > 1) { if (sx !== 0 || sy !== 0 || dx !== 0 || dy !== 0 || w !== sw || h !== sh || sw !== dw || sh !== dh) { Debug.error(`Texture#copy: copies of multisampled textures must cover the entire texture (source '${source.name}' ${sw}x${sh}, destination '${this.name}' ${dw}x${dh}, no offsets or partial regions).`); return false; } } // #endif return true; } /** * Copies a region of a source texture into this texture. Both textures must have the same * pixel format. The copied region sizes must match (no scaling), and must lie within the * chosen mip levels of both textures. Multisampled textures can be copied to other * multisampled textures with the same sample count (WebGPU only), but only as a full-texture * copy - no offsets or partial regions, and no copies between different sample counts (use a * resolve instead). * * @param {Texture} source - The source texture to copy from. * @param {object} [options] - Optional arguments. * @param {number} [options.sourceMipLevel] - The source mip level to copy from. Defaults to 0. * @param {number} [options.destMipLevel] - The destination mip level to copy to. Defaults to 0. * @param {number} [options.face] - If the textures are cubemaps, the face to copy (applies to * both source and destination). Defaults to 0. * @param {number} [options.layer] - If the textures are 2D array textures, the array layer to * copy (applies to both source and destination). Defaults to 0. * @param {number} [options.slice] - If the textures are volume textures, the depth slice to * copy (applies to both source and destination). When not specified, all depth slices of the * source mip level are copied, which requires the destination mip level to have at least as * many depth slices. * @param {number} [options.sourceX] - The left edge of the source region. Defaults to 0. * @param {number} [options.sourceY] - The top edge of the source region. Defaults to 0. * @param {number} [options.width] - The width of the copied region. Defaults to the full width * of the source mip level (minus sourceX). * @param {number} [options.height] - The height of the copied region. Defaults to the full * height of the source mip level (minus sourceY). * @param {number} [options.destX] - The left edge of the destination region. Defaults to 0. * @param {number} [options.destY] - The top edge of the destination region. Defaults to 0. * @param {RenderTarget} [options.sourceRenderTarget] - A render target wrapping the source * texture as its color buffer, at the matching face / layer / mip level. Provide as an optimization to * avoid allocating a temporary one when copying with high frequency (per frame). Note that this * is only utilized on the WebGL platform, and ignored on WebGPU. * @returns {boolean} True if the copy was successful, false otherwise. */ copy(source, options = {}) { if (!this._validateCopy(source, options)) { return false; } return this.impl.copy?.(source, options) ?? true; } /** * Creates a TextureView for this texture, specifying a subset of mip levels and array layers. * TextureViews can be bound to compute shaders and materials to access specific portions of a * texture. See {@link TextureView} for more details. * * Note: TextureView is only supported on WebGPU. On WebGL, the full texture is always bound, * including all its mip levels and array layers. * * @param {number} [baseMipLevel] - The first mip level accessible to the view. Defaults to 0. * @param {number} [mipLevelCount] - The number of mip levels accessible to the view. Defaults * to 1. * @param {number} [baseArrayLayer] - The first array layer accessible to the view. Defaults to * 0. * @param {number} [arrayLayerCount] - The number of array layers accessible to the view. * Defaults to 1. * @returns {TextureView} A new TextureView for this texture. * @example * // Create a view for mip level 1 * const mip1View = texture.getView(1); * * // Use with compute shader * compute.setParameter('outputTexture', mip1View); * * // Sample array layer 1 of a texture array in a material, while a render target renders to * // its layer 0 in the same render pass. The shader samples the view's layer 0. * material.setParameter('uLayers', textureArray.getView(0, 1, 1, 1)); */ getView(baseMipLevel = 0, mipLevelCount = 1, baseArrayLayer = 0, arrayLayerCount = 1) { return new TextureView(this, baseMipLevel, mipLevelCount, baseArrayLayer, arrayLayerCount); } } export { Texture };