UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

632 lines (618 loc) 23.2 kB
import { AlphaFormat, ByteType, ClampToEdgeWrapping, DataTexture, DepthFormat, DepthStencilFormat, FloatType, HalfFloatType, IntType, LinearFilter, LuminanceAlphaFormat, LuminanceFormat, MathUtils, NearestFilter, RedFormat, RedIntegerFormat, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, ShortType, Texture, UnsignedByteType, UnsignedInt248Type, UnsignedIntType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedShortType } from 'three'; import Interpretation, { Mode } from '../core/layer/Interpretation'; import Capabilities from '../core/system/Capabilities'; import EmptyTexture from '../renderer/EmptyTexture'; import { createPixelBuffer, createTypedArrayFromBuffer, getTypedArrayType } from './imageDecoderWorker'; import WorkerPool from './WorkerPool'; export const OPAQUE_BYTE = 255; export const OPAQUE_FLOAT = 1.0; export const TRANSPARENT = 0; export const DEFAULT_NODATA = 0; function isTexture(obj) { return obj?.isTexture; } function isRenderTarget(obj) { return obj?.isRenderTarget; } function isDataTexture(texture) { return texture.isDataTexture; } function isCanvasTexture(texture) { return texture.isCanvasTexture; } /** * Returns the number of bytes per channel. * * @param dataType - The pixel format. * @returns The number of bytes per channel. */ function getBytesPerChannel(dataType) { switch (dataType) { case UnsignedByteType: case ByteType: return 1; case ShortType: case UnsignedShortType: case UnsignedShort4444Type: case UnsignedShort5551Type: return 2; case IntType: case UnsignedIntType: case UnsignedInt248Type: case FloatType: return 4; case HalfFloatType: return 2; default: throw new Error(`unknown data type: ${dataType}`); } } function getDataTypeString(dataType) { switch (dataType) { case UnsignedByteType: return 'UnsignedByteType'; case ByteType: return 'ByteType'; case ShortType: return 'ShortType'; case UnsignedShortType: return 'UnsignedShortType'; case UnsignedShort4444Type: return 'UnsignedShort4444Type'; case UnsignedShort5551Type: return 'UnsignedShort5551Type'; case IntType: return 'IntType'; case UnsignedIntType: return 'UnsignedIntType'; case UnsignedInt248Type: return 'UnsignedInt248Type'; case FloatType: return 'FloatType'; case HalfFloatType: return 'HalfFloatType'; default: throw new Error(`unknown data type: ${dataType}`); } } /** * Returns the number of channels per pixel. * * @param pixelFormat - The pixel format. * @returns The number of channels per pixel. */ function getChannelCount(pixelFormat) { switch (pixelFormat) { case AlphaFormat: return 1; case RGBAFormat: return 4; case LuminanceFormat: return 1; case LuminanceAlphaFormat: return 2; case DepthFormat: return 1; case DepthStencilFormat: return 1; case RedFormat: return 1; case RedIntegerFormat: return 1; case RGFormat: return 2; case RGIntegerFormat: return 2; case RGBAIntegerFormat: return 4; default: throw new Error(`invalid pixel format: ${pixelFormat}`); } } /** * Estimate the size of the texture. * * @param texture - The texture. * @returns The size, in bytes. */ function estimateSize(texture) { // Note: this estimation is very broad for several reasons // - It does not know if this texture is GPU-memory only or if there is a copy in CPU-memory // - It does not know any possible optimization done by the GPU const channels = getChannelCount(texture.format); const bpp = getBytesPerChannel(texture.type); return texture.image.width * texture.image.height * channels * bpp; } /** * Reads back the render target buffer into CPU memory, then attach this buffer to the `data` * property of the render target's texture. * * This is useful because normally the pixels of a render target are not readable. * * @param target - The render target to read back. * @param renderer - The WebGL renderer to perform the operation. */ function createDataCopy(target, renderer) { // Render target textures don't have data in CPU memory, // we need to transfer their data into a buffer. const bufSize = target.width * target.height * getChannelCount(target.texture.format); const buf = target.texture.type === UnsignedByteType ? new Uint8Array(bufSize) : new Float32Array(bufSize); renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, buf); target.texture.data = buf; } /** * Gets the underlying pixel buffer of the image. * * @param image - The image. * @returns The pixel buffer. */ function getPixels(image) { const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; const context = canvas.getContext('2d', { willReadFrequently: true }); if (!context) { throw new Error('could not acquire 2D context on canvas'); } context.drawImage(image, 0, 0); return context.getImageData(0, 0, image.width, image.height).data; } let decoderWorkerPool = null; function getDecoderPool() { if (decoderWorkerPool == null) { decoderWorkerPool = new WorkerPool({ createWorker: () => new Worker(URL.createObjectURL(new Blob([atob('InVzZSBzdHJpY3QiOygoKT0+e2Z1bmN0aW9uIF8oZSx0KXtyZXR1cm57cmVxdWVzdElkOmUsZXJyb3I6dCBpbnN0YW5jZW9mIEVycm9yP3QubWVzc2FnZToidW5rbm93biBlcnJvciJ9fXZhciBrPTEwMDksVD0xMDE1LGI9MjU1LEI9MSxBPTAscD0wO2Z1bmN0aW9uIE0oZSl7aWYoZSBpbnN0YW5jZW9mIEZsb2F0MzJBcnJheSlyZXR1cm4iRmxvYXQzMkFycmF5IjtpZihlIGluc3RhbmNlb2YgRmxvYXQ2NEFycmF5KXJldHVybiJGbG9hdDY0QXJyYXkiO2lmKGUgaW5zdGFuY2VvZiBVaW50MzJBcnJheSlyZXR1cm4iVWludDMyQXJyYXkiO2lmKGUgaW5zdGFuY2VvZiBVaW50MTZBcnJheSlyZXR1cm4iVWludDE2QXJyYXkiO2lmKGUgaW5zdGFuY2VvZiBJbnQzMkFycmF5KXJldHVybiJJbnQzMkFycmF5IjtpZihlIGluc3RhbmNlb2YgSW50MTZBcnJheSlyZXR1cm4iSW50MTZBcnJheSI7aWYoZSBpbnN0YW5jZW9mIFVpbnQ4QXJyYXkpcmV0dXJuIlVpbnQ4QXJyYXkiO2lmKGUgaW5zdGFuY2VvZiBJbnQ4QXJyYXkpcmV0dXJuIkludDhBcnJheSI7aWYoZSBpbnN0YW5jZW9mIFVpbnQ4Q2xhbXBlZEFycmF5KXJldHVybiJVaW50OENsYW1wZWRBcnJheSI7dGhyb3cgbmV3IEVycm9yKCJ1bnN1cHBvcnRlZCB0eXBlIil9ZnVuY3Rpb24geChlLHQpe2lmKHR5cGVvZiB0PT0ibnVtYmVyIilzd2l0Y2godCl7Y2FzZSBrOnJldHVybiBuZXcgVWludDhDbGFtcGVkQXJyYXkoZSk7Y2FzZSBUOnJldHVybiBuZXcgRmxvYXQzMkFycmF5KGUpfWVsc2Ugc3dpdGNoKHQpe2Nhc2UiRmxvYXQzMkFycmF5IjpyZXR1cm4gbmV3IEZsb2F0MzJBcnJheShlKTtjYXNlIkZsb2F0NjRBcnJheSI6cmV0dXJuIG5ldyBGbG9hdDY0QXJyYXkoZSk7Y2FzZSJVaW50OENsYW1wZWRBcnJheSI6cmV0dXJuIG5ldyBVaW50OENsYW1wZWRBcnJheShlKTtjYXNlIlVpbnQ4QXJyYXkiOnJldHVybiBuZXcgVWludDhBcnJheShlKTtjYXNlIlVpbnQxNkFycmF5IjpyZXR1cm4gbmV3IFVpbnQxNkFycmF5KGUpO2Nhc2UiVWludDMyQXJyYXkiOnJldHVybiBuZXcgVWludDMyQXJyYXkoZSk7Y2FzZSJJbnQ4QXJyYXkiOnJldHVybiBuZXcgSW50OEFycmF5KGUpO2Nhc2UiSW50MTZBcnJheSI6cmV0dXJuIG5ldyBJbnQxNkFycmF5KGUpO2Nhc2UiSW50MzJBcnJheSI6cmV0dXJuIG5ldyBJbnQzMkFycmF5KGUpfXRocm93IG5ldyBFcnJvcigiaW52YWxpZCBzdGF0ZSIpfWZ1bmN0aW9uIEUoZSl7bGV0IHQ9ZS5pbnB1dC5tYXAobz0+eChvLGUuaW5wdXRUeXBlKSksaD1lLm9wYXF1ZVZhbHVlLG47aWYoZS5idWZmZXJTaXplJiZlLmRhdGFUeXBlKXN3aXRjaChlLmRhdGFUeXBlKXtjYXNlIFQ6bj1uZXcgRmxvYXQzMkFycmF5KGUuYnVmZmVyU2l6ZSk7YnJlYWs7Y2FzZSBrOm49bmV3IFVpbnQ4Q2xhbXBlZEFycmF5KGUuYnVmZmVyU2l6ZSk7YnJlYWs7ZGVmYXVsdDp0aHJvdyBuZXcgRXJyb3IoInVucmVjb2duaXplZCBidWZmZXIgdHlwZTogIitlLmRhdGFUeXBlKX1lbHNlIHRocm93IGNvbnNvbGUuZXJyb3IoIm1pc3NpbmcgdmFsdWVzIiksbmV3IEVycm9yKCJtaXNzaW5nIHZhbHVlcyIpO2xldCB5PTEvMCxtPS0xLzAsdz0hMDtpZih0Lmxlbmd0aD09PTEpe2xldCBvPXRbMF0sZj1vLmxlbmd0aDtmb3IobGV0IGQ9MDtkPGY7ZCsrKXtsZXQgaT1kKjIsYSxyLHM9b1tkXTtzIT09c3x8cz09PWUubm9kYXRhPyhhPXAscj1BKTooYT1zLHI9aCx3PSExKSx5PU1hdGgubWluKHksYSksbT1NYXRoLm1heChtLGEpLG5baSswXT1hLG5baSsxXT1yfX1pZih0Lmxlbmd0aD09PTIpe2xldCBvPXRbMF0sZj10WzFdLGQ9by5sZW5ndGg7Zm9yKGxldCBpPTA7aTxkO2krKyl7bGV0IGE9aSoyLHIscz1vW2ldLGM9ZltpXTtzIT09c3x8cz09PWUubm9kYXRhP3I9cDpyPXMsYz4wJiYodz0hMSkseT1NYXRoLm1pbih5LHIpLG09TWF0aC5tYXgobSxyKSxuW2ErMF09cixuW2ErMV09ZltpXX19aWYodC5sZW5ndGg9PT0zKXtsZXQgbz10WzBdLGY9dFsxXSxkPXRbMl0saT1vLmxlbmd0aCxhO2ZvcihsZXQgcj0wO3I8aTtyKyspe2xldCBzPXIqNCxjPW9bcl0sbD1mW3JdLHU9ZFtyXTsoYyE9PWN8fGM9PT1lLm5vZGF0YSkmJihsIT09bHx8bD09PWUubm9kYXRhKSYmKHUhPT11fHx1PT09ZS5ub2RhdGEpPyhjPXAsbD1wLHU9cCxhPUEpOihhPWgsdz0hMSksbltzKzBdPWMsbltzKzFdPWwsbltzKzJdPXUsbltzKzNdPWF9fWlmKHQubGVuZ3RoPT09NCl7bGV0IG89dFswXSxmPXRbMV0sZD10WzJdLGk9dFszXSxhPW8ubGVuZ3RoO2ZvcihsZXQgcj0wO3I8YTtyKyspe2xldCBzPXIqNCxjPW9bcl0sbD1mW3JdLHU9ZFtyXSxnPWlbcl07KGMhPT1jfHxjPT09ZS5ub2RhdGEpJiYobCE9PWx8fGw9PT1lLm5vZGF0YSkmJih1IT09dXx8dT09PWUubm9kYXRhKT8oYz1wLGw9cCx1PXAsZz1BKTpnPjAmJih3PSExKSxuW3MrMF09YyxuW3MrMV09bCxuW3MrMl09dSxuW3MrM109Z319cmV0dXJue2J1ZmZlcjpuLmJ1ZmZlcixtaW46eSxtYXg6bSxpc1RyYW5zcGFyZW50Ond9fW9ubWVzc2FnZT1hc3luYyBmdW5jdGlvbihlKXtsZXQgdD1lLmRhdGE7dHJ5e3N3aXRjaCh0LnR5cGUpe2Nhc2UiQ3JlYXRlUGl4ZWxCdWZmZXIiOntsZXQgaD1FKHQucGF5bG9hZCksbj17cmVxdWVzdElkOnQuaWQscGF5bG9hZDpofTt0aGlzLnBvc3RNZXNzYWdlKG4se3RyYW5zZmVyOltuLnBheWxvYWQuYnVmZmVyXX0pfWJyZWFrO2Nhc2UiQ3JlYXRlSW1hZ2VCaXRtYXAiOntsZXQgaD1uZXcgQmxvYihbdC5wYXlsb2FkLmJ1ZmZlcl0pLG49YXdhaXQgY3JlYXRlSW1hZ2VCaXRtYXAoaCx0LnBheWxvYWQub3B0aW9ucykseT17cmVxdWVzdElkOnQuaWQscGF5bG9hZDpufTt0aGlzLnBvc3RNZXNzYWdlKHkse3RyYW5zZmVyOltuXX0pfWJyZWFrfX1jYXRjaChoKXt0aGlzLnBvc3RNZXNzYWdlKF8odC5pZCxoKSl9fTt9KSgpOwo=')], {type: "text/javascript"})), { type: 'module' }), concurrency: 2 }); } return decoderWorkerPool; } async function createImageBitmapUsingWorker(blob, options) { if (window.Worker != null) { const pool = getDecoderPool(); const buffer = await blob.arrayBuffer(); const img = await pool.queue('CreateImageBitmap', { buffer, options }, [buffer]); return img; } else { // Fallback to main-thread decoding return createImageBitmap(blob, options); } } /** * Decodes the blob according to its media type, then returns a texture for this blob. * * @param blob - The buffer to decode. * @param options - Options * @returns The generated texture. * @throws When the media type is unsupported or when the image dimensions are greater than the * maximum texture size. */ async function decodeBlob(blob, options = {}) { // media types are in the form 'type;args', for example: 'text/html; charset=UTF-8; const [type] = blob.type.split(';'); switch (type) { case 'image/webp': case 'image/png': case 'image/jpg': // not a valid media type, but we support it for compatibility case 'image/jpeg': { const enableWorker = options?.enableWorkers ?? true; let img; const decodeOptions = { imageOrientation: options.flipY === true ? 'flipY' : 'none' }; if (enableWorker) { img = await createImageBitmapUsingWorker(blob, decodeOptions); } else { img = await createImageBitmap(blob, decodeOptions); } let tex; const max = Capabilities.getMaxTextureSize(); if (img.width > max || img.height > max) { throw new Error(`image dimensions (${img.width} * ${img.height} pixels) exceed max texture size (${max} pixels)`); } if (options.createDataTexture === true) { const buf = getPixels(img); tex = new DataTexture(buf, img.width, img.height, RGBAFormat, UnsignedByteType); } else { tex = new Texture(img); } tex.wrapS = ClampToEdgeWrapping; tex.wrapT = ClampToEdgeWrapping; tex.minFilter = LinearFilter; tex.magFilter = LinearFilter; tex.generateMipmaps = false; tex.needsUpdate = true; return tex; } default: throw new Error(`unsupported media type for textures: ${blob.type}`); } } function createTextureFromPixelBuffer(result, width, height, format, type) { const texture = result.isTransparent ? new EmptyTexture() : new DataTexture(createTypedArrayFromBuffer(result.buffer, type), width, height, format, type); if (!isEmptyTexture(texture)) { texture.needsUpdate = true; texture.generateMipmaps = false; texture.magFilter = LinearFilter; texture.minFilter = LinearFilter; } return { texture, min: result.min, max: result.max }; } function getPixelFormat(channelCount) { switch (channelCount) { case 1: case 2: return RGFormat; default: return RGBAFormat; } } function getCreatePixelBufferOptions(options, ...pixelData) { const { width, height, type, nodata } = options; const targetDataType = type; let channelCount; switch (pixelData.length) { case 1: case 2: channelCount = 2; break; default: channelCount = 4; break; } let opaqueValue; switch (targetDataType) { case FloatType: opaqueValue = OPAQUE_FLOAT; break; default: opaqueValue = OPAQUE_BYTE; break; } return { bufferSize: width * height * channelCount, inputType: getTypedArrayType(pixelData[0]), // Assume all arrays have the same type dataType: targetDataType, input: pixelData.map(p => options.makeCopyOfBuffers ? p.buffer.slice(0) : p.buffer), opaqueValue, nodata }; } /** * Returns a {@link DataTexture} initialized with the specified data. * * @param options - The creation options. * @param sourceDataType - The data type of the input pixel data. * @param pixelData - The pixel data * for each input channels. Must be either one, three, or four channels. */ function createDataTexture(options, sourceDataType, ...pixelData) { const pixelBufferOptions = getCreatePixelBufferOptions({ width: options.width, height: options.height, type: sourceDataType, nodata: options.nodata, makeCopyOfBuffers: false }, ...pixelData); const result = createPixelBuffer(pixelBufferOptions); const format = getPixelFormat(pixelData.length); return createTextureFromPixelBuffer(result, options.width, options.height, format, pixelBufferOptions.dataType); } /** * Returns a {@link DataTexture} initialized with the specified data. * * @param options - The creation options. * @param sourceDataType - The data type of the input pixel data. * @param pixelData - The pixel data * for each input channels. Must be either one, three, or four channels. */ async function createDataTextureAsync(options, sourceDataType, ...pixelData) { const pixelBufferOptions = getCreatePixelBufferOptions({ width: options.width, height: options.height, type: sourceDataType, nodata: options.nodata, makeCopyOfBuffers: true // Since we are going to send them to a worker }, ...pixelData); let result; const enableWorkers = options?.enableWorkers ?? true; if (enableWorkers && window.Worker != null) { const pool = getDecoderPool(); result = await pool.queue('CreatePixelBuffer', pixelBufferOptions, pixelBufferOptions.input); } else { result = createPixelBuffer(pixelBufferOptions); } const format = getPixelFormat(pixelData.length); return createTextureFromPixelBuffer(result, options.width, options.height, format, pixelBufferOptions.dataType); } /** * Returns a 1D texture containing a pixel on the horizontal axis for each color in the array. * * @param colors - The color gradient. * @param alpha - The optional alpha gradient. Must be of the same length as the color gradient. * @returns The resulting texture. */ function create1DTexture(colors, alpha) { const size = colors.length; const buf = new Uint8ClampedArray(size * 4); for (let i = 0; i < size; i++) { const color = colors[i]; const index = i * 4; buf[index + 0] = color.r * 255; buf[index + 1] = color.g * 255; buf[index + 2] = color.b * 255; buf[index + 3] = alpha ? MathUtils.clamp(alpha[i], 0, 1) * 255 : 255; } const texture = new DataTexture(buf, size, 1, RGBAFormat, UnsignedByteType); texture.needsUpdate = true; return texture; } /** * Computes the minimum and maximum value of the buffer, but only taking into account the first * channel (R channel). This is typically used for elevation data. * * @param buffer - The pixel buffer. May be an RGBA or an RG buffer. * @param nodata - The no-data value. Pixels with this value will be ignored. * @param interpretation - The image interpretation. * @param channelCount - The channel count of the buffer * @returns The computed min/max. */ function computeMinMaxFromBuffer(buffer, nodata, interpretation = Interpretation.Raw, channelCount = 4) { let min = Infinity; let max = -Infinity; const RED_CHANNEL = 0; const alphaChannel = channelCount - 1; switch (interpretation.mode) { case Mode.Raw: for (let i = 0; i < buffer.length; i += channelCount) { const value = buffer[i + RED_CHANNEL]; const alpha = buffer[i + alphaChannel]; if (!(value !== value) && value !== nodata && alpha !== 0) { min = Math.min(min, value); max = Math.max(max, value); } } break; case Mode.ScaleToMinMax: { const lower = interpretation.min; const upper = interpretation.max; for (let i = 0; i < buffer.length; i += channelCount) { const value = buffer[i + RED_CHANNEL] / 255; const r = lower + value * (upper - lower); const alpha = buffer[i + alphaChannel]; if (!(r !== r) && r !== nodata && alpha !== 0) { min = Math.min(min, r); max = Math.max(max, r); } } } break; default: throw new Error('not implemented'); } if (interpretation.negateValues === true) { return { min: -max, max: -min }; } return { min, max }; } function getWiderType(left, right) { if (getBytesPerChannel(left) > getBytesPerChannel(right)) { return left; } return right; } function shouldExpandRGB(src, dst) { if (dst !== RGBAFormat) { return false; } if (src === dst) { return false; } return true; } /** * Computes min/max of the given image. * * @param image - The image to process. * @param interpretation - The interpretation of the image. * @returns The min/max. */ function computeMinMaxFromImage(image, interpretation = Interpretation.Raw) { const buf = getPixels(image); return computeMinMaxFromBuffer(buf, 0, interpretation); } function computeMinMax(texture, noDataValue = 0, interpretation = Interpretation.Raw) { if (isDataTexture(texture)) { const channelCount = getChannelCount(texture.format); return computeMinMaxFromBuffer(texture.image.data, noDataValue, interpretation, channelCount); } if (isCanvasTexture(texture)) { return computeMinMaxFromImage(texture.image, interpretation); } return null; } function isEmptyTexture(texture) { if (texture == null) { return true; } if (texture.isEmptyTexture) { return true; } if (isCanvasTexture(texture)) { return texture.source?.data == null; } if (isDataTexture(texture)) { return texture.image?.data == null; } else if (texture.isRenderTargetTexture) { return false; } else { return texture.source?.data == null; } } function getTextureMemoryUsage(context, texture) { if (texture == null) { return; } if (isEmptyTexture(texture)) { context.objects.set(texture.id, { gpuMemory: 0, cpuMemory: 0 }); } else if (texture.userData?.memoryUsage != null) { const existing = texture.userData.memoryUsage; context.objects.set(texture.id, existing); } else if (isCanvasTexture(texture)) { const { width, height } = texture.source.data; context.objects.set(texture.id, { gpuMemory: width * height * 4, cpuMemory: 0 }); } else { const { width, height } = texture.image; const bytes = width * height * getBytesPerChannel(texture.type) * getChannelCount(texture.format); if (texture.isRenderTargetTexture) { // RenderTargets do not exist in CPU memory. context.objects.set(texture.id, { gpuMemory: bytes, cpuMemory: 0 }); } else { context.objects.set(texture.id, { gpuMemory: bytes, cpuMemory: bytes }); } } } function getDepthBufferMemoryUsage(context, renderTarget) { const gl = context.renderer.getContext(); const bpp = gl.getParameter(gl.DEPTH_BITS); const bytes = renderTarget.width * renderTarget.height * (bpp / 8); context.objects.set(renderTarget.texture.id, { gpuMemory: bytes, cpuMemory: 0 }); } function getMemoryUsage(context, texture) { if (texture == null) { return; } if (isTexture(texture)) { getTextureMemoryUsage(context, texture); } else if (isRenderTarget(texture)) { if (texture.depthBuffer) { if (texture.depthTexture != null) { getTextureMemoryUsage(context, texture.depthTexture); } else { getDepthBufferMemoryUsage(context, texture); } } getTextureMemoryUsage(context, texture.texture); } } function getImageData(source) { if (source instanceof HTMLCanvasElement || source instanceof OffscreenCanvas) { const context = source.getContext('2d', { willReadFrequently: true }); const imageData = context.getImageData(0, 0, source.width, source.height); return imageData.data; } else { return getPixels(source); } } function isCanvasEmpty(canvas) { const data = getImageData(canvas); for (let i = 0; i < data.length; i += 4) { // Check if any pixel is not fully transparent or not matching canvas background color if (data[i + 3] !== 0) { return false; // Canvas is not empty } } return true; // Canvas is empty } /** * Returns a texture filter that is compatible with the texture. * @param filter - The requested filter. * @param dataType - The texture data type. * @param renderer - The WebGLRenderer * @returns The requested filter, if compatible, or {@link NearestFilter} if not compatible. */ function getCompatibleTextureFilter(filter, dataType, renderer) { const gl = renderer?.getContext(); // This would happen when running unit test in a case where WebGL is not supported. if (gl == null) { return filter; } const fallback = NearestFilter; if (filter === LinearFilter) { if (dataType === FloatType && !gl.getExtension('OES_texture_float_linear')) { return fallback; } if (dataType === HalfFloatType && !gl.getExtension('OES_texture_half_float_linear')) { return fallback; } } return filter; } /** * Updates the texture to improve compatibility with various platforms. */ function ensureCompatibility(texture, renderer) { texture.minFilter = getCompatibleTextureFilter(texture.minFilter, texture.type, renderer); texture.magFilter = getCompatibleTextureFilter(texture.magFilter, texture.type, renderer); } export default { createDataTexture, createDataTextureAsync, isEmptyTexture, decodeBlob, getChannelCount, getBytesPerChannel, getWiderType, getDataTypeString, create1DTexture, createDataCopy, computeMinMax, isDataTexture, isCanvasTexture, computeMinMaxFromBuffer, computeMinMaxFromImage, estimateSize, shouldExpandRGB, isCanvasEmpty, getMemoryUsage, getCompatibleTextureFilter, ensureCompatibility };