@kitware/vtk.js
Version:
Visualization Toolkit for the Web
1,287 lines (1,213 loc) • 65.3 kB
JavaScript
import DeepEqual from 'fast-deep-equal';
import Constants from './Texture/Constants.js';
import HalfFloat from '../../Common/Core/HalfFloat.js';
import { n as newInstance$1, o as obj, s as set, e as setGet, g as get, i as moveToProtected, a as newTypedArray, c as macro } from '../../macros2.js';
import vtkDataArray from '../../Common/Core/DataArray.js';
import { Y as isPowerOfTwo, T as nearestPowerOfTwo } from '../../Common/Core/Math/index.js';
import vtkViewNode from '../SceneGraph/ViewNode.js';
import { registerOverride } from './ViewNodeFactory.js';
import supportsNorm16LinearCached from './Texture/supportsNorm16Linear.js';
const {
Wrap,
Filter
} = Constants;
const {
VtkDataTypes
} = vtkDataArray;
const {
vtkDebugMacro,
vtkErrorMacro,
vtkWarningMacro,
requiredParam
} = macro;
const {
toHalf
} = HalfFloat;
// ----------------------------------------------------------------------------
// vtkOpenGLTexture methods
// ----------------------------------------------------------------------------
function vtkOpenGLTexture(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkOpenGLTexture');
function getTexParams() {
return {
internalFormat: model.internalFormat,
format: model.format,
openGLDataType: model.openGLDataType,
width: model.width,
height: model.height
};
}
// Renders myself
publicAPI.render = (renWin = null) => {
if (renWin) {
model._openGLRenderWindow = renWin;
} else {
model._openGLRenderer = publicAPI.getFirstAncestorOfType('vtkOpenGLRenderer');
// sync renderable properties
model._openGLRenderWindow = model._openGLRenderer.getLastAncestorOfType('vtkOpenGLRenderWindow');
}
model.context = model._openGLRenderWindow.getContext();
if (model.renderable.getInterpolate()) {
if (model.generateMipmap) {
publicAPI.setMinificationFilter(Filter.LINEAR_MIPMAP_LINEAR);
} else {
publicAPI.setMinificationFilter(Filter.LINEAR);
}
publicAPI.setMagnificationFilter(Filter.LINEAR);
} else {
publicAPI.setMinificationFilter(Filter.NEAREST);
publicAPI.setMagnificationFilter(Filter.NEAREST);
}
if (model.renderable.getRepeat()) {
publicAPI.setWrapR(Wrap.REPEAT);
publicAPI.setWrapS(Wrap.REPEAT);
publicAPI.setWrapT(Wrap.REPEAT);
}
// clear image if input data is set
if (model.renderable.getInputData()) {
model.renderable.setImage(null);
}
// create the texture if it is not done already
if (!model.handle || model.renderable.getMTime() > model.textureBuildTime.getMTime()) {
if (model.renderable.getImageBitmap() !== null) {
if (model.renderable.getInterpolate()) {
model.generateMipmap = true;
publicAPI.setMinificationFilter(Filter.LINEAR_MIPMAP_LINEAR);
}
// Have an Image which may not be complete
if (model.renderable.getImageBitmap() && model.renderable.getImageLoaded()) {
publicAPI.create2DFromImageBitmap(model.renderable.getImageBitmap());
publicAPI.activate();
publicAPI.sendParameters();
model.textureBuildTime.modified();
}
}
// if we have an Image
if (model.renderable.getImage() !== null) {
if (model.renderable.getInterpolate()) {
model.generateMipmap = true;
publicAPI.setMinificationFilter(Filter.LINEAR_MIPMAP_LINEAR);
}
// Have an Image which may not be complete
if (model.renderable.getImage() && model.renderable.getImageLoaded()) {
publicAPI.create2DFromImage(model.renderable.getImage());
publicAPI.activate();
publicAPI.sendParameters();
model.textureBuildTime.modified();
}
}
// if we have a canvas
if (model.renderable.getCanvas() !== null) {
if (model.renderable.getInterpolate()) {
model.generateMipmap = true;
publicAPI.setMinificationFilter(Filter.LINEAR_MIPMAP_LINEAR);
}
const canvas = model.renderable.getCanvas();
publicAPI.create2DFromRaw({
width: canvas.width,
height: canvas.height,
numComps: 4,
dataType: VtkDataTypes.UNSIGNED_CHAR,
data: canvas,
flip: true
});
publicAPI.activate();
publicAPI.sendParameters();
model.textureBuildTime.modified();
}
// if we have jsImageData
if (model.renderable.getJsImageData() !== null) {
const jsid = model.renderable.getJsImageData();
if (model.renderable.getInterpolate()) {
model.generateMipmap = true;
publicAPI.setMinificationFilter(Filter.LINEAR_MIPMAP_LINEAR);
}
publicAPI.create2DFromRaw({
width: jsid.width,
height: jsid.height,
numComps: 4,
dataType: VtkDataTypes.UNSIGNED_CHAR,
data: jsid.data,
flip: true
});
publicAPI.activate();
publicAPI.sendParameters();
model.textureBuildTime.modified();
}
// if we have InputData
const input = model.renderable.getInputData(0);
if (input && input.getPointData().getScalars()) {
const ext = input.getExtent();
const inScalars = input.getPointData().getScalars();
// do we have a cube map? Six inputs
const data = [];
for (let i = 0; i < model.renderable.getNumberOfInputPorts(); ++i) {
const indata = model.renderable.getInputData(i);
const scalars = indata ? indata.getPointData().getScalars().getData() : null;
if (scalars) {
data.push(scalars);
}
}
if (model.renderable.getInterpolate() && inScalars.getNumberOfComponents() === 4) {
model.generateMipmap = true;
publicAPI.setMinificationFilter(Filter.LINEAR_MIPMAP_LINEAR);
}
if (data.length % 6 === 0) {
publicAPI.createCubeFromRaw({
width: ext[1] - ext[0] + 1,
height: ext[3] - ext[2] + 1,
numComps: inScalars.getNumberOfComponents(),
dataType: inScalars.getDataType(),
data
});
} else {
publicAPI.create2DFromRaw({
width: ext[1] - ext[0] + 1,
height: ext[3] - ext[2] + 1,
numComps: inScalars.getNumberOfComponents(),
dataType: inScalars.getDataType(),
data: inScalars.getData()
});
}
publicAPI.activate();
publicAPI.sendParameters();
model.textureBuildTime.modified();
}
}
if (model.handle) {
publicAPI.activate();
}
};
const getNorm16Ext = () => {
if ((model.minificationFilter === Filter.LINEAR || model.magnificationFilter === Filter.LINEAR) && !supportsNorm16LinearCached()) {
return undefined;
}
return model.oglNorm16Ext;
};
//----------------------------------------------------------------------------
publicAPI.destroyTexture = () => {
// deactivate it first
publicAPI.deactivate();
if (model.context && model.handle) {
model.context.deleteTexture(model.handle);
}
model._prevTexParams = null;
model.handle = 0;
model.numberOfDimensions = 0;
model.target = 0;
model.components = 0;
model.width = 0;
model.height = 0;
model.depth = 0;
publicAPI.resetFormatAndType();
};
//----------------------------------------------------------------------------
publicAPI.createTexture = () => {
// reuse the existing handle if we have one
if (!model.handle) {
model.handle = model.context.createTexture();
if (model.target) {
model.context.bindTexture(model.target, model.handle);
// See: http://www.openmodel.context..org/wiki/Common_Mistakes#Creating_a_complete_texture
// turn off mip map filter or set the base and max level correctly. here
// both are done.
model.context.texParameteri(model.target, model.context.TEXTURE_MIN_FILTER, publicAPI.getOpenGLFilterMode(model.minificationFilter));
model.context.texParameteri(model.target, model.context.TEXTURE_MAG_FILTER, publicAPI.getOpenGLFilterMode(model.magnificationFilter));
model.context.texParameteri(model.target, model.context.TEXTURE_WRAP_S, publicAPI.getOpenGLWrapMode(model.wrapS));
model.context.texParameteri(model.target, model.context.TEXTURE_WRAP_T, publicAPI.getOpenGLWrapMode(model.wrapT));
if (model._openGLRenderWindow.getWebgl2()) {
model.context.texParameteri(model.target, model.context.TEXTURE_WRAP_R, publicAPI.getOpenGLWrapMode(model.wrapR));
}
model.context.bindTexture(model.target, null);
}
}
};
//---------------------------------------------------------------------------
publicAPI.getTextureUnit = () => {
if (model._openGLRenderWindow) {
return model._openGLRenderWindow.getTextureUnitForTexture(publicAPI);
}
return -1;
};
//---------------------------------------------------------------------------
publicAPI.activate = () => {
// activate a free texture unit for this texture
model._openGLRenderWindow.activateTexture(publicAPI);
publicAPI.bind();
};
//---------------------------------------------------------------------------
publicAPI.deactivate = () => {
if (model._openGLRenderWindow) {
model._openGLRenderWindow.deactivateTexture(publicAPI);
}
};
//---------------------------------------------------------------------------
publicAPI.releaseGraphicsResources = rwin => {
if (rwin && model.handle) {
rwin.activateTexture(publicAPI);
rwin.deactivateTexture(publicAPI);
model.context.deleteTexture(model.handle);
model._prevTexParams = null;
model.handle = 0;
model.numberOfDimensions = 0;
model.target = 0;
model.internalFormat = 0;
model.format = 0;
model.openGLDataType = 0;
model.components = 0;
model.width = 0;
model.height = 0;
model.depth = 0;
model.allocatedGPUMemoryInBytes = 0;
}
if (model.shaderProgram) {
model.shaderProgram.releaseGraphicsResources(rwin);
model.shaderProgram = null;
}
};
//----------------------------------------------------------------------------
publicAPI.bind = () => {
model.context.bindTexture(model.target, model.handle);
if (model.autoParameters && publicAPI.getMTime() > model.sendParametersTime.getMTime()) {
publicAPI.sendParameters();
}
};
//----------------------------------------------------------------------------
publicAPI.isBound = () => {
let result = false;
if (model.context && model.handle) {
let target = 0;
switch (model.target) {
case model.context.TEXTURE_2D:
target = model.context.TEXTURE_BINDING_2D;
break;
default:
vtkWarningMacro('impossible case');
break;
}
const oid = model.context.getIntegerv(target);
result = oid === model.handle;
}
return result;
};
//----------------------------------------------------------------------------
publicAPI.sendParameters = () => {
model.context.texParameteri(model.target, model.context.TEXTURE_WRAP_S, publicAPI.getOpenGLWrapMode(model.wrapS));
model.context.texParameteri(model.target, model.context.TEXTURE_WRAP_T, publicAPI.getOpenGLWrapMode(model.wrapT));
if (model._openGLRenderWindow.getWebgl2()) {
model.context.texParameteri(model.target, model.context.TEXTURE_WRAP_R, publicAPI.getOpenGLWrapMode(model.wrapR));
}
model.context.texParameteri(model.target, model.context.TEXTURE_MIN_FILTER, publicAPI.getOpenGLFilterMode(model.minificationFilter));
model.context.texParameteri(model.target, model.context.TEXTURE_MAG_FILTER, publicAPI.getOpenGLFilterMode(model.magnificationFilter));
if (model._openGLRenderWindow.getWebgl2()) {
model.context.texParameteri(model.target, model.context.TEXTURE_BASE_LEVEL, model.baseLevel);
model.context.texParameteri(model.target, model.context.TEXTURE_MAX_LEVEL, model.maxLevel);
}
// model.context.texParameterf(model.target, model.context.TEXTURE_MIN_LOD, model.minLOD);
// model.context.texParameterf(model.target, model.context.TEXTURE_MAX_LOD, model.maxLOD);
model.sendParametersTime.modified();
};
//----------------------------------------------------------------------------
publicAPI.getInternalFormat = (vtktype, numComps) => {
if (!model._forceInternalFormat) {
model.internalFormat = publicAPI.getDefaultInternalFormat(vtktype, numComps);
}
if (!model.internalFormat) {
vtkDebugMacro(`Unable to find suitable internal format for T=${vtktype} NC= ${numComps}`);
}
if ([model.context.R32F, model.context.RG32F, model.context.RGB32F, model.context.RGBA32F].includes(model.internalFormat) && !model.context.getExtension('OES_texture_float_linear')) {
vtkWarningMacro('Failed to load OES_texture_float_linear. Texture filtering is not available for *32F internal formats.');
}
return model.internalFormat;
};
//----------------------------------------------------------------------------
publicAPI.getDefaultInternalFormat = (vtktype, numComps) => {
let result = 0;
// try default next
result = model._openGLRenderWindow.getDefaultTextureInternalFormat(vtktype, numComps, getNorm16Ext(), publicAPI.useHalfFloat());
if (result) {
return result;
}
if (!result) {
vtkDebugMacro('Unsupported internal texture type!');
vtkDebugMacro(`Unable to find suitable internal format for T=${vtktype} NC= ${numComps}`);
}
return result;
};
publicAPI.useHalfFloat = () => model.enableUseHalfFloat && model.canUseHalfFloat;
//----------------------------------------------------------------------------
publicAPI.setInternalFormat = iFormat => {
model._forceInternalFormat = true;
if (iFormat !== model.internalFormat) {
model.internalFormat = iFormat;
publicAPI.modified();
}
};
//----------------------------------------------------------------------------
publicAPI.getFormat = (vtktype, numComps) => {
model.format = publicAPI.getDefaultFormat(vtktype, numComps);
return model.format;
};
//----------------------------------------------------------------------------
publicAPI.getDefaultFormat = (vtktype, numComps) => {
if (model._openGLRenderWindow.getWebgl2()) {
switch (numComps) {
case 1:
return model.context.RED;
case 2:
return model.context.RG;
case 3:
return model.context.RGB;
case 4:
return model.context.RGBA;
default:
return model.context.RGB;
}
} else {
// webgl1
switch (numComps) {
case 1:
return model.context.LUMINANCE;
case 2:
return model.context.LUMINANCE_ALPHA;
case 3:
return model.context.RGB;
case 4:
return model.context.RGBA;
default:
return model.context.RGB;
}
}
};
//----------------------------------------------------------------------------
publicAPI.resetFormatAndType = () => {
model._prevTexParams = null;
model.format = 0;
model.internalFormat = 0;
model._forceInternalFormat = false;
model.openGLDataType = 0;
};
//----------------------------------------------------------------------------
publicAPI.getDefaultDataType = vtkScalarType => {
const useHalfFloat = publicAPI.useHalfFloat();
// DON'T DEAL with VTK_CHAR as this is platform dependent.
if (model._openGLRenderWindow.getWebgl2()) {
switch (vtkScalarType) {
// case VtkDataTypes.SIGNED_CHAR:
// return model.context.BYTE;
case VtkDataTypes.UNSIGNED_CHAR:
return model.context.UNSIGNED_BYTE;
// prefer norm16 since that is accurate compared to
// half float which is not
case getNorm16Ext() && !useHalfFloat && VtkDataTypes.SHORT:
return model.context.SHORT;
case getNorm16Ext() && !useHalfFloat && VtkDataTypes.UNSIGNED_SHORT:
return model.context.UNSIGNED_SHORT;
// use half float type
case useHalfFloat && VtkDataTypes.SHORT:
return model.context.HALF_FLOAT;
case useHalfFloat && VtkDataTypes.UNSIGNED_SHORT:
return model.context.HALF_FLOAT;
// case VtkDataTypes.INT:
// return model.context.INT;
// case VtkDataTypes.UNSIGNED_INT:
// return model.context.UNSIGNED_INT;
case VtkDataTypes.FLOAT:
case VtkDataTypes.VOID: // used for depth component textures.
default:
return model.context.FLOAT;
}
}
switch (vtkScalarType) {
// case VtkDataTypes.SIGNED_CHAR:
// return model.context.BYTE;
case VtkDataTypes.UNSIGNED_CHAR:
return model.context.UNSIGNED_BYTE;
// case VtkDataTypes.SHORT:
// return model.context.SHORT;
// case VtkDataTypes.UNSIGNED_SHORT:
// return model.context.UNSIGNED_SHORT;
// case VtkDataTypes.INT:
// return model.context.INT;
// case VtkDataTypes.UNSIGNED_INT:
// return model.context.UNSIGNED_INT;
case VtkDataTypes.FLOAT:
case VtkDataTypes.VOID: // used for depth component textures.
default:
if (model.context.getExtension('OES_texture_float') && model.context.getExtension('OES_texture_float_linear')) {
return model.context.FLOAT;
}
{
const halfFloat = model.context.getExtension('OES_texture_half_float');
if (halfFloat && model.context.getExtension('OES_texture_half_float_linear')) {
return halfFloat.HALF_FLOAT_OES;
}
}
return model.context.UNSIGNED_BYTE;
}
};
//----------------------------------------------------------------------------
publicAPI.getOpenGLDataType = (vtkScalarType, forceUpdate = false) => {
if (!model.openGLDataType || forceUpdate) {
model.openGLDataType = publicAPI.getDefaultDataType(vtkScalarType);
}
return model.openGLDataType;
};
publicAPI.getShiftAndScale = () => {
let shift = 0.0;
let scale = 1.0;
// for all float type internal formats
switch (model.openGLDataType) {
case model.context.BYTE:
scale = 127.5;
shift = scale - 128.0;
break;
case model.context.UNSIGNED_BYTE:
scale = 255.0;
shift = 0.0;
break;
case model.context.SHORT:
scale = 32767.5;
shift = scale - 32768.0;
break;
case model.context.UNSIGNED_SHORT:
scale = 65536.0;
shift = 0.0;
break;
case model.context.INT:
scale = 2147483647.5;
shift = scale - 2147483648.0;
break;
case model.context.UNSIGNED_INT:
scale = 4294967295.0;
shift = 0.0;
break;
case model.context.FLOAT:
}
return {
shift,
scale
};
};
//----------------------------------------------------------------------------
publicAPI.getOpenGLFilterMode = emode => {
switch (emode) {
case Filter.NEAREST:
return model.context.NEAREST;
case Filter.LINEAR:
return model.context.LINEAR;
case Filter.NEAREST_MIPMAP_NEAREST:
return model.context.NEAREST_MIPMAP_NEAREST;
case Filter.NEAREST_MIPMAP_LINEAR:
return model.context.NEAREST_MIPMAP_LINEAR;
case Filter.LINEAR_MIPMAP_NEAREST:
return model.context.LINEAR_MIPMAP_NEAREST;
case Filter.LINEAR_MIPMAP_LINEAR:
return model.context.LINEAR_MIPMAP_LINEAR;
default:
return model.context.NEAREST;
}
};
//----------------------------------------------------------------------------
publicAPI.getOpenGLWrapMode = vtktype => {
switch (vtktype) {
case Wrap.CLAMP_TO_EDGE:
return model.context.CLAMP_TO_EDGE;
case Wrap.REPEAT:
return model.context.REPEAT;
case Wrap.MIRRORED_REPEAT:
return model.context.MIRRORED_REPEAT;
default:
return model.context.CLAMP_TO_EDGE;
}
};
//----------------------------------------------------------------------------
/**
* Gets the extent's size.
* @param {Extent} extent
*/
function getExtentSize(extent) {
const [xmin, xmax, ymin, ymax, zmin, zmax] = extent;
return [xmax - xmin + 1, ymax - ymin + 1, zmax - zmin + 1];
}
//----------------------------------------------------------------------------
/**
* Gets the number of pixels in the extent.
* @param {Extent} extent
*/
function getExtentPixelCount(extent) {
const [sx, sy, sz] = getExtentSize(extent);
return sx * sy * sz;
}
//----------------------------------------------------------------------------
/**
* Reads a flattened extent from the image data and writes to the given output array.
*
* Assumes X varies the fastest and Z varies the slowest.
*
* @param {*} data
* @param {*} dataDims
* @param {Extent} extent
* @param {TypedArray} outArray
* @param {number} outOffset
* @returns
*/
function readExtentIntoArray(data, dataDims, extent, outArray, outOffset) {
const [xmin, xmax, ymin, ymax, zmin, zmax] = extent;
const [dx, dy] = dataDims;
const sxy = dx * dy;
let writeOffset = outOffset;
for (let zi = zmin; zi <= zmax; zi++) {
const zOffset = zi * sxy;
for (let yi = ymin; yi <= ymax; yi++) {
const zyOffset = zOffset + yi * dx;
// explicit alternative to data.subarray,
// due to potential perf issues on v8
for (let readOffset = zyOffset + xmin, end = zyOffset + xmax; readOffset <= end; readOffset++, writeOffset++) {
outArray[writeOffset] = data[readOffset];
}
}
}
}
//----------------------------------------------------------------------------
/**
* Reads several image extents into a contiguous pixel array.
*
* @param {*} data
* @param {Extent[]} extent
* @param {TypedArrayConstructor} typedArrayConstructor optional typed array constructor
* @returns
*/
function readExtents(data, extents, typedArrayConstructor = null) {
const constructor = typedArrayConstructor || data.constructor;
const numPixels = extents.reduce((count, extent) => count + getExtentPixelCount(extent), 0);
const extentPixels = new constructor(numPixels);
const dataDims = [model.width, model.height, model.depth];
let writeOffset = 0;
extents.forEach(extent => {
readExtentIntoArray(data, dataDims, extent, extentPixels, writeOffset);
writeOffset += getExtentPixelCount(extent);
});
return extentPixels;
}
//----------------------------------------------------------------------------
/**
* Updates the data array to match the required data type for OpenGL.
*
* This function takes the input data and converts it to the appropriate
* format required by the OpenGL texture, based on the specified data type.
*
* @param {string} dataType - The original data type of the input data.
* @param {Array} data - The input data array that needs to be updated.
* @param {boolean} [depth=false] - Indicates whether the data is a 3D array.
* @param {Array<Extent>} imageExtents only consider these image extents (default: [])
* @returns {Array} The updated data array that matches the OpenGL data type.
*/
publicAPI.updateArrayDataTypeForGL = (dataType, data, depth = false, imageExtents = []) => {
const pixData = [];
let pixCount = model.width * model.height * model.components;
if (depth) {
pixCount *= model.depth;
}
const onlyUpdateExtents = !!imageExtents.length;
// if the opengl data type is float
// then the data array must be float
if (dataType !== VtkDataTypes.FLOAT && model.openGLDataType === model.context.FLOAT) {
for (let idx = 0; idx < data.length; idx++) {
if (data[idx]) {
if (onlyUpdateExtents) {
pixData.push(readExtents(data[idx], imageExtents, Float32Array));
} else {
const dataArrayToCopy = data[idx].length > pixCount ? data[idx].subarray(0, pixCount) : data[idx];
pixData.push(new Float32Array(dataArrayToCopy));
}
} else {
pixData.push(null);
}
}
}
// if the opengl data type is ubyte
// then the data array must be u8, we currently simply truncate the data
if (dataType !== VtkDataTypes.UNSIGNED_CHAR && model.openGLDataType === model.context.UNSIGNED_BYTE) {
for (let idx = 0; idx < data.length; idx++) {
if (data[idx]) {
if (onlyUpdateExtents) {
pixData.push(readExtents(data[idx], imageExtents, Uint8Array));
} else {
const dataArrayToCopy = data[idx].length > pixCount ? data[idx].subarray(0, pixCount) : data[idx];
pixData.push(new Uint8Array(dataArrayToCopy));
}
} else {
pixData.push(null);
}
}
}
// if the opengl data type is half float
// then the data array must be u16
let halfFloat = false;
if (model._openGLRenderWindow.getWebgl2()) {
halfFloat = model.openGLDataType === model.context.HALF_FLOAT;
} else {
const halfFloatExt = model.context.getExtension('OES_texture_half_float');
halfFloat = halfFloatExt && model.openGLDataType === halfFloatExt.HALF_FLOAT_OES;
}
if (halfFloat) {
for (let idx = 0; idx < data.length; idx++) {
if (data[idx]) {
const src = onlyUpdateExtents ? readExtents(data[idx], imageExtents) : data[idx];
const newArray = new Uint16Array(onlyUpdateExtents ? src.length : pixCount);
const newArrayLen = newArray.length;
for (let i = 0; i < newArrayLen; i++) {
newArray[i] = toHalf(src[i]);
}
pixData.push(newArray);
} else {
pixData.push(null);
}
}
}
// The output has to be filled
if (pixData.length === 0) {
for (let i = 0; i < data.length; i++) {
pixData.push(onlyUpdateExtents && data[i] ? readExtents(data[i], imageExtents) : data[i]);
}
}
return pixData;
};
//----------------------------------------------------------------------------
function scaleTextureToHighestPowerOfTwo(data) {
if (model._openGLRenderWindow.getWebgl2()) {
// No need if webGL2
return data;
}
const pixData = [];
const width = model.width;
const height = model.height;
const numComps = model.components;
if (data && (!isPowerOfTwo(width) || !isPowerOfTwo(height))) {
// Scale up the texture to the next highest power of two dimensions.
const halfFloat = model.context.getExtension('OES_texture_half_float');
const newWidth = nearestPowerOfTwo(width);
const newHeight = nearestPowerOfTwo(height);
const pixCount = newWidth * newHeight * model.components;
for (let idx = 0; idx < data.length; idx++) {
if (data[idx] !== null) {
let newArray = null;
const jFactor = height / newHeight;
const iFactor = width / newWidth;
let usingHalf = false;
if (model.openGLDataType === model.context.FLOAT) {
newArray = new Float32Array(pixCount);
} else if (halfFloat && model.openGLDataType === halfFloat.HALF_FLOAT_OES) {
newArray = new Uint16Array(pixCount);
usingHalf = true;
} else {
newArray = new Uint8Array(pixCount);
}
for (let j = 0; j < newHeight; j++) {
const joff = j * newWidth * numComps;
const jidx = j * jFactor;
let jlow = Math.floor(jidx);
let jhi = Math.ceil(jidx);
if (jhi >= height) {
jhi = height - 1;
}
const jmix = jidx - jlow;
const jmix1 = 1.0 - jmix;
jlow = jlow * width * numComps;
jhi = jhi * width * numComps;
for (let i = 0; i < newWidth; i++) {
const ioff = i * numComps;
const iidx = i * iFactor;
let ilow = Math.floor(iidx);
let ihi = Math.ceil(iidx);
if (ihi >= width) {
ihi = width - 1;
}
const imix = iidx - ilow;
ilow *= numComps;
ihi *= numComps;
for (let c = 0; c < numComps; c++) {
if (usingHalf) {
newArray[joff + ioff + c] = HalfFloat.toHalf(HalfFloat.fromHalf(data[idx][jlow + ilow + c]) * jmix1 * (1.0 - imix) + HalfFloat.fromHalf(data[idx][jlow + ihi + c]) * jmix1 * imix + HalfFloat.fromHalf(data[idx][jhi + ilow + c]) * jmix * (1.0 - imix) + HalfFloat.fromHalf(data[idx][jhi + ihi + c]) * jmix * imix);
} else {
newArray[joff + ioff + c] = data[idx][jlow + ilow + c] * jmix1 * (1.0 - imix) + data[idx][jlow + ihi + c] * jmix1 * imix + data[idx][jhi + ilow + c] * jmix * (1.0 - imix) + data[idx][jhi + ihi + c] * jmix * imix;
}
}
}
}
pixData.push(newArray);
model.width = newWidth;
model.height = newHeight;
} else {
pixData.push(null);
}
}
}
// The output has to be filled
if (pixData.length === 0) {
for (let i = 0; i < data.length; i++) {
pixData.push(data[i]);
}
}
return pixData;
}
//----------------------------------------------------------------------------
function useTexStorage(dataType) {
if (model._openGLRenderWindow) {
if (model.resizable || model.renderable?.getResizable()) {
// Cannot use texStorage if the texture is supposed to be resizable.
return false;
}
if (model._openGLRenderWindow.getWebgl2()) {
const webGLInfo = model._openGLRenderWindow.getGLInformations();
if (webGLInfo.RENDERER.value.match(/WebKit/gi) && navigator.platform.match(/Mac/gi) && getNorm16Ext() && (dataType === VtkDataTypes.UNSIGNED_SHORT || dataType === VtkDataTypes.SHORT)) {
// Cannot use texStorage with EXT_texture_norm16 textures on Mac M1 GPU.
// No errors reported but the texture is unusable.
return false;
}
// Use texStorage for WebGL2
return true;
}
return false;
}
return false;
}
//----------------------------------------------------------------------------
publicAPI.create2DFromRaw = ({
width = requiredParam('width'),
height = requiredParam('height'),
numComps = requiredParam('numComps'),
dataType = requiredParam('dataType'),
data = requiredParam('data'),
flip = false
} = {}) => {
// Now determine the texture parameters using the arguments.
publicAPI.getOpenGLDataType(dataType, true);
publicAPI.getInternalFormat(dataType, numComps);
publicAPI.getFormat(dataType, numComps);
if (!model.internalFormat || !model.format || !model.openGLDataType) {
vtkErrorMacro('Failed to determine texture parameters.');
return false;
}
model.target = model.context.TEXTURE_2D;
model.components = numComps;
model.width = width;
model.height = height;
model.depth = 1;
model.numberOfDimensions = 2;
model._openGLRenderWindow.activateTexture(publicAPI);
publicAPI.createTexture();
publicAPI.bind();
// Create an array of texture with one texture
const dataArray = [data];
const pixData = publicAPI.updateArrayDataTypeForGL(dataType, dataArray);
const scaledData = scaleTextureToHighestPowerOfTwo(pixData);
// Source texture data from the PBO.
model.context.pixelStorei(model.context.UNPACK_FLIP_Y_WEBGL, flip);
model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1);
if (useTexStorage(dataType)) {
model.context.texStorage2D(model.target, 1, model.internalFormat, model.width, model.height);
if (scaledData[0] != null) {
model.context.texSubImage2D(model.target, 0, 0, 0, model.width, model.height, model.format, model.openGLDataType, scaledData[0]);
}
} else {
model.context.texImage2D(model.target, 0, model.internalFormat, model.width, model.height, 0, model.format, model.openGLDataType, scaledData[0]);
}
if (model.generateMipmap) {
model.context.generateMipmap(model.target);
}
// always reset the flip
if (flip) {
model.context.pixelStorei(model.context.UNPACK_FLIP_Y_WEBGL, false);
}
model.allocatedGPUMemoryInBytes = model.width * model.height * model.depth * numComps * model._openGLRenderWindow.getDefaultTextureByteSize(dataType, getNorm16Ext(), publicAPI.useHalfFloat());
publicAPI.deactivate();
return true;
};
//----------------------------------------------------------------------------
publicAPI.createCubeFromRaw = ({
width = requiredParam('width'),
height = requiredParam('height'),
numComps = requiredParam('numComps'),
dataType = requiredParam('dataType'),
data = requiredParam('data')
} = {}) => {
// Now determine the texture parameters using the arguments.
publicAPI.getOpenGLDataType(dataType);
publicAPI.getInternalFormat(dataType, numComps);
publicAPI.getFormat(dataType, numComps);
if (!model.internalFormat || !model.format || !model.openGLDataType) {
vtkErrorMacro('Failed to determine texture parameters.');
return false;
}
model.target = model.context.TEXTURE_CUBE_MAP;
model.components = numComps;
model.width = width;
model.height = height;
model.depth = 1;
model.numberOfDimensions = 2;
model._openGLRenderWindow.activateTexture(publicAPI);
model.maxLevel = data.length / 6 - 1;
publicAPI.createTexture();
publicAPI.bind();
const pixData = publicAPI.updateArrayDataTypeForGL(dataType, data);
const scaledData = scaleTextureToHighestPowerOfTwo(pixData);
// invert the data because opengl is messed up with cube maps
// and uses the old renderman standard with Y going down
// even though it is completely at odds with OpenGL standards
const invertedData = [];
let widthLevel = model.width;
let heightLevel = model.height;
for (let i = 0; i < scaledData.length; i++) {
if (i % 6 === 0 && i !== 0) {
widthLevel /= 2;
heightLevel /= 2;
}
invertedData[i] = newTypedArray(dataType, heightLevel * widthLevel * model.components);
for (let y = 0; y < heightLevel; ++y) {
const row1 = y * widthLevel * model.components;
const row2 = (heightLevel - y - 1) * widthLevel * model.components;
invertedData[i].set(scaledData[i].slice(row2, row2 + widthLevel * model.components), row1);
}
}
// Source texture data from the PBO.
model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1);
if (useTexStorage(dataType)) {
model.context.texStorage2D(model.target, 6, model.internalFormat, model.width, model.height);
}
// We get the 6 images
for (let i = 0; i < 6; i++) {
// For each mipmap level
let j = 0;
let w = model.width;
let h = model.height;
while (w >= 1 && h >= 1) {
// In webgl 1, all levels need to be defined. So if the latest level size is
// 8x8, we have to add 3 more null textures (4x4, 2x2, 1x1)
// In webgl 2, the attribute maxLevel will be use.
let tempData = null;
if (j <= model.maxLevel) {
tempData = invertedData[6 * j + i];
}
if (useTexStorage(dataType)) {
if (tempData != null) {
model.context.texSubImage2D(model.context.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, 0, 0, w, h, model.format, model.openGLDataType, tempData);
}
} else {
model.context.texImage2D(model.context.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, model.internalFormat, w, h, 0, model.format, model.openGLDataType, tempData);
}
j++;
w /= 2;
h /= 2;
}
}
model.allocatedGPUMemoryInBytes = model.width * model.height * model.depth * numComps * model._openGLRenderWindow.getDefaultTextureByteSize(dataType, getNorm16Ext(), publicAPI.useHalfFloat());
// generateMipmap must not be called here because we manually upload all levels
// if it is called, all levels will be overwritten
publicAPI.deactivate();
return true;
};
//----------------------------------------------------------------------------
publicAPI.createDepthFromRaw = ({
width = requiredParam('width'),
height = requiredParam('height'),
dataType = requiredParam('dataType'),
data = requiredParam('data')
} = {}) => {
// Now determine the texture parameters using the arguments.
publicAPI.getOpenGLDataType(dataType);
model.format = model.context.DEPTH_COMPONENT;
if (model._openGLRenderWindow.getWebgl2()) {
if (dataType === VtkDataTypes.FLOAT) {
model.internalFormat = model.context.DEPTH_COMPONENT32F;
} else {
model.internalFormat = model.context.DEPTH_COMPONENT16;
}
} else {
model.internalFormat = model.context.DEPTH_COMPONENT;
}
if (!model.internalFormat || !model.format || !model.openGLDataType) {
vtkErrorMacro('Failed to determine texture parameters.');
return false;
}
model.target = model.context.TEXTURE_2D;
model.components = 1;
model.width = width;
model.height = height;
model.depth = 1;
model.numberOfDimensions = 2;
model._openGLRenderWindow.activateTexture(publicAPI);
publicAPI.createTexture();
publicAPI.bind();
// Source texture data from the PBO.
// model.context.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1);
if (useTexStorage(dataType)) {
model.context.texStorage2D(model.target, 1, model.internalFormat, model.width, model.height);
if (data != null) {
model.context.texSubImage2D(model.target, 0, 0, 0, model.width, model.height, model.format, model.openGLDataType, data);
}
} else {
model.context.texImage2D(model.target, 0, model.internalFormat, model.width, model.height, 0, model.format, model.openGLDataType, data);
}
if (model.generateMipmap) {
model.context.generateMipmap(model.target);
}
model.allocatedGPUMemoryInBytes = model.width * model.height * model.depth * model.components * model._openGLRenderWindow.getDefaultTextureByteSize(dataType, getNorm16Ext(), publicAPI.useHalfFloat());
publicAPI.deactivate();
return true;
};
//----------------------------------------------------------------------------
publicAPI.create2DFromImage = image => {
// Determine the texture parameters using the arguments.
publicAPI.getOpenGLDataType(VtkDataTypes.UNSIGNED_CHAR);
publicAPI.getInternalFormat(VtkDataTypes.UNSIGNED_CHAR, 4);
publicAPI.getFormat(VtkDataTypes.UNSIGNED_CHAR, 4);
if (!model.internalFormat || !model.format || !model.openGLDataType) {
vtkErrorMacro('Failed to determine texture parameters.');
return false;
}
model.target = model.context.TEXTURE_2D;
model.components = 4;
model.depth = 1;
model.numberOfDimensions = 2;
model._openGLRenderWindow.activateTexture(publicAPI);
publicAPI.createTexture();
publicAPI.bind();
const needNearestPowerOfTwo = !model._openGLRenderWindow.getWebgl2() && (!isPowerOfTwo(image.width) || !isPowerOfTwo(image.height));
let textureSource = image;
let targetWidth = image.width;
let targetHeight = image.height;
let flipY = true;
// For WebGL1, we need to scale the image to the nearest power of two
// dimensions if the image is not already a power of two. For WebGL2, we can
// use the image as is. Note: Chrome has a perf issue where the path
// HTMLImageElement -> Canvas -> texSubImage2D is faster than
// HTMLImageElement -> texSubImage2D directly. See
// https://issues.chromium.org/issues/41311312#comment7
// Tested on Chrome 137.0.7151.104 Windows 11
const isChrome = window.chrome;
if (needNearestPowerOfTwo || isChrome) {
const canvas = new OffscreenCanvas(nearestPowerOfTwo(image.width), nearestPowerOfTwo(image.height));
targetWidth = canvas.width;
targetHeight = canvas.height;
const ctx = canvas.getContext('2d');
ctx.translate(0, canvas.height);
ctx.scale(1, -1);
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
textureSource = canvas;
flipY = false; // we are flipping the image manually using translate/scale
}
model.width = targetWidth;
model.height = targetHeight;
// Source texture data from the PBO.
model.context.pixelStorei(model.context.UNPACK_FLIP_Y_WEBGL, flipY);
model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1);
if (useTexStorage(VtkDataTypes.UNSIGNED_CHAR)) {
model.context.texStorage2D(model.target, 1, model.internalFormat, model.width, model.height);
model.context.texSubImage2D(model.target, 0, 0, 0, model.width, model.height, model.format, model.openGLDataType, textureSource);
} else {
model.context.texImage2D(model.target, 0, model.internalFormat, model.width, model.height, 0, model.format, model.openGLDataType, textureSource);
}
if (model.generateMipmap) {
model.context.generateMipmap(model.target);
}
model.allocatedGPUMemoryInBytes = model.width * model.height * model.depth * model.components * model._openGLRenderWindow.getDefaultTextureByteSize(VtkDataTypes.UNSIGNED_CHAR, getNorm16Ext(), publicAPI.useHalfFloat());
publicAPI.deactivate();
return true;
};
//----------------------------------------------------------------------------
publicAPI.create2DFromImageBitmap = imageBitmap => {
// Determine the texture parameters.
publicAPI.getOpenGLDataType(VtkDataTypes.UNSIGNED_CHAR);
publicAPI.getInternalFormat(VtkDataTypes.UNSIGNED_CHAR, 4);
publicAPI.getFormat(VtkDataTypes.UNSIGNED_CHAR, 4);
if (!model.internalFormat || !model.format || !model.openGLDataType) {
vtkErrorMacro('Failed to determine texture parameters.');
return false;
}
model.target = model.context.TEXTURE_2D;
model.components = 4;
model.depth = 1;
model.numberOfDimensions = 2;
model._openGLRenderWindow.activateTexture(publicAPI);
publicAPI.createTexture();
publicAPI.bind();
// Prepare texture unpack alignment
model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1);
model.width = imageBitmap.width;
model.height = imageBitmap.height;
if (useTexStorage(VtkDataTypes.UNSIGNED_CHAR)) {
model.context.texStorage2D(model.target, 1, model.internalFormat, model.width, model.height);
model.context.texSubImage2D(model.target, 0, 0, 0, model.width, model.height, model.format, model.openGLDataType, imageBitmap);
} else {
model.context.texImage2D(model.target, 0, model.internalFormat, model.width, model.height, 0, model.format, model.openGLDataType, imageBitmap);
}
if (model.generateMipmap) {
model.context.generateMipmap(model.target);
}
model.allocatedGPUMemoryInBytes = model.width * model.height * model.depth * model.components * model._openGLRenderWindow.getDefaultTextureByteSize(VtkDataTypes.UNSIGNED_CHAR, getNorm16Ext(), publicAPI.useHalfFloat());
publicAPI.deactivate();
return true;
};
// Compute scale and offset per component from min and max per component
function computeScaleOffsets(min, max, numComps) {
const offset = new Array(numComps);
const scale = new Array(numComps);
for (let c = 0; c < numComps; ++c) {
offset[c] = min[c];
scale[c] = max[c] - min[c] || 1.0;
}
return {
scale,
offset
};
}
// HalfFloat only represents numbers between [-2048, 2048] exactly accurate,
// for numbers outside of this range there is a precision limitation
function hasExactHalfFloat(offset, scale) {
// Per Component
for (let c = 0; c < offset.length; c++) {
const min = offset[c];
const max = scale[c] + min;
if (min < -2048 || min > 2048 || max < -2048 || max > 2048) {
return false;
}
}
return true;
}
function setCanUseHalfFloat(dataType, offset, scale, preferSizeOverAccuracy) {
publicAPI.getOpenGLDataType(dataType);
// Don't consider halfFloat and convert back to Float when the range of data does not generate an accurate halfFloat
// AND it is not preferable to have a smaller texture than an exact texture.
const isExactHalfFloat = hasExactHalfFloat(offset, scale) || preferSizeOverAccuracy;
let useHalfFloat = false;
if (model._openGLRenderWindow.getWebgl2()) {
// If OES_texture_float_linear is not available, and using a half float would still be exact, force half floats
// This is because half floats are always texture filterable in webgl2, while full *32F floats are not (unless the extension is present)
const forceHalfFloat = model.openGLDataType === model.context.FLOAT && model.context.getExtension('OES_texture_float_linear') === null && isExactHalfFloat;
useHalfFloat = forceHalfFloat || model.openGLDataType === model.context.HALF_FLOAT;
} else {
const halfFloatExt = model.context.getExtension('OES_texture_half_float');
useHalfFloat = halfFloatExt && model.openGLDataType === halfFloatExt.HALF_FLOAT_OES;
}
model.canUseHalfFloat = useHalfFloat && isExactHalfFloat;
}
function processDataArray(dataArray, preferSizeOverAccuracy) {
const numComps = dataArray.getNumberOfComponents();
const dataType = dataArray.getDataType();
const data = dataArray.getData();
// Compute min max from array
// Using the vtkDataArray.getRange() enables caching
const minArray = new Array(numComps);
const maxArray = new Array(numComps);
for (let c = 0; c < numComps; ++c) {
const [min, max] = dataArray.getRange(c);
minArray[c] = min;
maxArray[c] = max;
}
const scaleOffsets = computeScaleOffsets(minArray, maxArray, numComps);
// preferSizeOverAccuracy will override norm16 due to bug with norm16 implementation
// https://bugs.chromium.org/p/chromium/issues/detail?id=1408247
setCanUseHalfFloat(dataType, scaleOffsets.offset, scaleOffsets.scale, preferSizeOverAccuracy);
// since our default is to use half float, in case that we can't use it
// we need to use another type
if (!publicAPI.useHalfFloat()) {
publicAPI.getOpenGLDataType(dataType, true);
}
return {
numComps,
dataType,
data,
scaleOffsets
};
}
publicAPI.create2DFilterableFromRaw = ({
width = requiredParam('width'),
height = requiredParam('height'),
numComps = requiredParam('numComps'),
dataType = requiredParam('dataType'),
data = requiredParam('data'),
preferSizeOverAccuracy = false,
ranges = undefined
} = {}) => publicAPI.create2DFilterableFromDataArray({
width,
height,
dataArray: vtkDataArray.newInstance({
numberOfComponents: numComps,
dataType,
values: data,
ranges
}),
preferSizeOverAccuracy
});
publicAPI.create2DFilterableFromDataArray = ({
width = requiredParam('width'),
height = requiredParam('height'),
dataArray = requiredParam('dataArray'),
preferSizeOverAccuracy = false
} = {}) => {
const {
numComps,
dataType,
data
} = processDataArray(dataArray, preferSizeOverAccuracy);
publicAPI.create2DFromRaw({
width,
height,
numComps,
dataType,
data
});
};
publicAPI.updateVolumeInfoForGL = (dataType, numComps) => {
let isScalingApplied = false;
const useHalfFloat = publicAPI.useHalfFloat();
// Initialize volume info if it doesn't exist
if (!model.volumeInfo?.scale || !model.volumeInfo?.offset) {
model.volumeInfo = {
scale: new Array(numComps),
offset: new Array(numComps)
};
}
// Default scaling and offset
for (let c = 0; c < numComps; ++c) {
model.volumeInfo.scale[c] = 1.0;
model.volumeInfo.offset[c] = 0.0;
}
// Handle SHORT data type with EXT_texture_norm16 extension
if (getNorm16Ext() && !useHalfFloat && dataType === VtkDataTypes.SHORT) {
for (let c = 0; c < numComps; ++c) {
model.volumeInfo.scale[c] = 32767.0; // Scale to [-1, 1] range
}
isScalingApplied = true;
}
// Handle UNSIGNED_SHORT data type with EXT_texture_norm16 extension
if (getNorm16Ext() && !useHalfFloat && dataType === VtkDataTypes.UNSIGNED_SHORT) {
for (let c = 0; c < numComps; ++c) {
model.volumeInfo.scale[c] = 65535.0; // Scale to [0, 1] range
}
isScalingApplied = true;
}
// Handle UNSIGNED_CHAR data type
if (dataType === VtkDataTypes.UNSIGNED_CHAR) {
for (let c = 0; c < numComps; ++c) {
model.volumeInfo.scale[c] = 255.0; // Scale to [0, 1] range
}
isScalingApplied = true;
}
// No scaling needed for FLOAT or HalfFloat (SHORT/UNSIGNED_SHORT)
if (dataType === VtkDataTypes.FLOAT || useHalfFloat && (dataType === VtkDataTypes.SHORT || dataType === VtkDataTypes.UNSIGNED_SHORT)) {
isScalingApplied = true;
}
return isScalingApplied;
};
//----------------------------------------------------------------------------
publicAPI.create3DFromRaw = ({
width = requiredParam('width'),
height = requiredParam('height'),
depth = requiredParam('depth'),
numComps = requiredParam('numComps'),
dataType = requiredParam