playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
1,158 lines (1,157 loc) • 41.4 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
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,
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
} from "./constants.js";
import { TextureUtils } from "./texture-utils.js";
import { TextureView } from "./texture-view.js";
let id = 0;
class Texture {
/**
* 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 {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[]|Uint16Array[]|Uint32Array[]|Float32Array[]|HTMLCanvasElement[]|HTMLImageElement[]|HTMLVideoElement[]|Uint8Array[][]} [options.levels]
* - Array of Uint8Array or other supported browser interface; or a two-dimensional array
* of Uint8Array 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.
* @example
* // Create a 8x8x24-bit texture
* const texture = new pc.Texture(graphicsDevice, {
* width: 8,
* height: 8,
* format: pc.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 = {}) {
/**
* The name of the texture.
*
* @type {string}
*/
__publicField(this, "name");
/** @ignore */
__publicField(this, "_gpuSize", 0);
/** @ignore */
__publicField(this, "releaseSourceAfterUpload", false);
/** @protected */
__publicField(this, "id", id++);
/** @protected */
__publicField(this, "_invalid", false);
/** @protected */
__publicField(this, "_lockedLevel", -1);
/** @protected */
__publicField(this, "_lockedMode", TEXTURELOCK_NONE);
/**
* A render version used to track the last time the texture properties requiring bind group
* to be updated were changed.
*
* @ignore
*/
__publicField(this, "renderVersionDirty", 0);
/** @protected */
__publicField(this, "_storage", false);
/** @protected */
__publicField(this, "_numLevels", 0);
/** @protected */
__publicField(this, "_numLevelsRequested");
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);
this._format = options.format ?? PIXELFORMAT_RGBA8;
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;
this._mipmaps = options.mipmaps ?? true;
this._numLevelsRequested = options.numLevels;
if (options.numLevels !== void 0) {
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;
}
this.profilerHint = options.profilerHint ?? 0;
this._levels = options.levels;
const upload = !!options.levels;
if (!this._levels) {
this._clearLevels();
}
this.recreateImpl(upload);
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);
}
/**
* 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[]|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
});
}
/**
* 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);
this.impl.destroy(device);
this.adjustVramSizeTracking(device._vram, -this._gpuSize);
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 (!this._needsUpload && !this._needsMipmapsUpload) {
this.releaseImageSources();
}
}
recreateImpl(upload = true) {
const { device } = this;
this.impl?.destroy(device);
this.impl = null;
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) {
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();
this.impl = device.createTextureImpl(this);
this.dirtyAll();
}
}
/**
* 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 (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;
}
}
propertyChanged(flag) {
this.impl.propertyChanged(flag);
this.renderVersionDirty = this.device.renderVersion;
}
_updateNumLevels() {
const maxLevels = this.mipmaps ? TextureUtils.calcMipLevelsCount(this.width, this.height) : 1;
const requestedLevels = this._numLevelsRequested;
if (requestedLevels !== void 0 && 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("pc.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.
*
* @type {boolean}
*/
set mipmaps(v) {
if (this._mipmaps !== v) {
if (this.device.isWebGPU) {
Debug.warn("Texture#mipmaps: mipmap property is currently not allowed to be changed on WebGPU, create the texture appropriately.", this);
} else 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();
if (this.array && this._numLevels !== oldNumLevels) {
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 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.pot && this._mipmaps && !(this._compressed && this._levels.length === 1);
return TextureUtils.calcGpuSize(this._width, this._height, this._depth, this._format, mips, this._cubemap);
}
/**
* 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;
this.device._shadersDirty = true;
}
}
/**
* Gets the texture type.
*
* @type {string}
* @ignore
*/
get type() {
return this._type;
}
/**
* 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) {
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();
this.device._shadersDirty = true;
}
} else {
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();
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";
}
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 = {}) {
options.level ?? (options.level = 0);
options.face ?? (options.face = 0);
options.mode ?? (options.mode = TEXTURELOCK_WRITE);
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
);
this._lockedMode = options.mode;
this._lockedLevel = options.level;
const levels = this.cubemap ? this._levels[options.face] : this._levels;
if (!levels[options.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[options.level] = new (getPixelFormatArrayType(this._format))(data);
}
return levels[options.level];
}
/**
* Set the pixel data of the texture from a canvas, image, video, or HTML DOM element. If the
* texture is a cubemap, the supplied source must be an array of 6 canvases, images or videos.
*
* Note: using an HTML element (e.g. `<div>`) as a source requires
* {@link GraphicsDevice#supportsHtmlTextures} to be true.
*
* @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement|HTMLElement|HTMLCanvasElement[]|HTMLImageElement[]|HTMLVideoElement[]|HTMLElement[]} source - A
* canvas, image, video, or HTML element, or an array of 6 canvas, image, 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) {
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;
if (this._cubemap) {
if (source[0]) {
width = source[0].width || 0;
height = source[0].height || 0;
for (let i = 0; i < 6; i++) {
const face = source[i];
if (!face || // face is missing
face.width !== width || // face is different width
face.height !== height || // face is different height
!this.device._isBrowserInterface(face)) {
invalid = true;
break;
}
}
} else {
invalid = true;
}
if (!invalid) {
for (let i = 0; i < 6; i++) {
if (this._levels[mipLevel][i] !== source[i]) {
this._levelsUpdated[mipLevel][i] = true;
}
}
}
} else {
if (!this.device._isBrowserInterface(source)) {
invalid = true;
}
if (!invalid) {
if (source !== this._levels[mipLevel]) {
this._levelsUpdated[mipLevel] = true;
}
if (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) {
this._width = 4;
this._height = 4;
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 {
if (mipLevel === 0) {
this._width = width;
this._height = height;
}
this._levels[mipLevel] = source;
}
if (this._invalid !== invalid || !invalid) {
this._invalid = invalid;
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("pc.Texture#unlock: Attempting to unlock a texture that is not locked.", this);
}
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;
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] - The face to download. Defaults to 0.
* @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.
* @returns {Promise<Uint8Array|Uint16Array|Uint32Array|Float32Array>} A promise that resolves
* with the pixel data of the texture.
*/
read(x, y, width, height, options = {}) {
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<void>} 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);
}
/**
* Creates a TextureView for this texture, specifying a subset of mip levels and array layers.
* TextureViews can be used with compute shaders to access specific portions of a texture.
*
* Note: TextureView is only supported on WebGPU. On WebGL, the full texture is always bound.
*
* @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);
*/
getView(baseMipLevel = 0, mipLevelCount = 1, baseArrayLayer = 0, arrayLayerCount = 1) {
return new TextureView(this, baseMipLevel, mipLevelCount, baseArrayLayer, arrayLayerCount);
}
}
export {
Texture
};