@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
699 lines (681 loc) • 25.3 kB
JavaScript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import { Camera, Mesh, Vector4, WebGLRenderTarget } from 'three';
import { AlphaFormat, ByteType, ClampToEdgeWrapping, DataTexture, DepthFormat, DepthStencilFormat, FloatType, HalfFloatType, IntType, LinearFilter, MathUtils, NearestFilter, PlaneGeometry, RedFormat, RedIntegerFormat, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, Scene, ShaderMaterial, ShortType, Texture, UnsignedByteType, UnsignedInt248Type, UnsignedIntType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedShortType } from 'three';
import Interpretation, { Mode } from '../core/layer/Interpretation';
import EmptyTexture from '../renderer/EmptyTexture';
import Capabilities from './Capabilities';
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 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
});
}
}
}
const depthToRGBAMaterial = new ShaderMaterial({
uniforms: {
depthTexture: {
value: null
}
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
#include <packing>
uniform sampler2D depthTexture;
varying vec2 vUv;
void main() {
float depth = texture2D(depthTexture, vUv).r;
gl_FragColor = packDepthToRGBA(depth);
}
`
});
// Note: this is copied from packing.glsl.js in three.js
const PackFactors = new Vector4(1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0);
const UnpackDownscale = 255 / 256; // 0..1 -> fraction (excluding 1)
const UnpackFactors4 = new Vector4(UnpackDownscale / PackFactors.x, UnpackDownscale / PackFactors.y, UnpackDownscale / PackFactors.z, 1.0 / PackFactors.w);
const QUAD = new PlaneGeometry(2, 2);
async function readDepthTexture(texture, renderer) {
const width = texture.image.width;
const height = texture.image.height;
const tempScene = new Scene();
const quad = new Mesh(QUAD, depthToRGBAMaterial);
tempScene.add(quad);
depthToRGBAMaterial.uniforms.depthTexture.value = texture;
const target = new WebGLRenderTarget(width, height, {
format: RGBAFormat,
type: UnsignedByteType
});
renderer.setRenderTarget(target);
renderer.render(tempScene, new Camera());
renderer.setRenderTarget(null);
const pixels = new Uint8Array(width * height * 4);
await renderer.readRenderTargetPixelsAsync(target, 0, 0, width, height, pixels);
const result = new Float32Array(width * height);
let k = 0;
const v = new Vector4();
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i + 0] / 255;
const g = pixels[i + 1] / 255;
const b = pixels[i + 2] / 255;
const a = pixels[i + 3] / 255;
v.set(r, g, b, a);
const depth = v.dot(UnpackFactors4);
result[k] = depth;
k++;
}
target.dispose();
return result;
}
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,
readDepthTexture,
shouldExpandRGB,
isCanvasEmpty,
getMemoryUsage,
getCompatibleTextureFilter,
ensureCompatibility
};