@acransac/vtk.js
Version:
Visualization Toolkit for the Web
1,366 lines (1,234 loc) • 42.2 kB
JavaScript
import Constants from 'vtk.js/Sources/Rendering/OpenGL/Texture/Constants';
import macro from 'vtk.js/Sources/macro';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
import vtkViewNode from 'vtk.js/Sources/Rendering/SceneGraph/ViewNode';
const { Wrap, Filter } = Constants;
const { VtkDataTypes } = vtkDataArray;
const { vtkDebugMacro, vtkErrorMacro, vtkWarningMacro } = macro;
// ----------------------------------------------------------------------------
// vtkOpenGLTexture methods
// ----------------------------------------------------------------------------
function vtkOpenGLTexture(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkOpenGLTexture');
// Renders myself
publicAPI.render = (renWin = null) => {
if (renWin) {
model.openGLRenderWindow = renWin;
} else {
model.openGLRenderer = publicAPI.getFirstAncestorOfType(
'vtkOpenGLRenderer'
);
// sync renderable properties
model.openGLRenderWindow = model.openGLRenderer.getParent();
}
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 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 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(
ext[1] - ext[0] + 1,
ext[3] - ext[2] + 1,
inScalars.getNumberOfComponents(),
inScalars.getDataType(),
data
);
} else {
publicAPI.create2DFromRaw(
ext[1] - ext[0] + 1,
ext[3] - ext[2] + 1,
inScalars.getNumberOfComponents(),
inScalars.getDataType(),
inScalars.getData()
);
}
publicAPI.activate();
publicAPI.sendParameters();
model.textureBuildTime.modified();
}
}
if (model.handle) {
publicAPI.activate();
}
};
//----------------------------------------------------------------------------
publicAPI.destroyTexture = () => {
// deactivate it first
publicAPI.deactivate();
if (model.context && model.handle) {
model.context.deleteTexture(model.handle);
}
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.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;
}
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.internalFormat) {
model.internalFormat = publicAPI.getDefaultInternalFormat(
vtktype,
numComps
);
}
if (!model.internalFormat) {
vtkDebugMacro(
`Unable to find suitable internal format for T=${vtktype} NC= ${numComps}`
);
}
return model.internalFormat;
};
//----------------------------------------------------------------------------
publicAPI.getDefaultInternalFormat = (vtktype, numComps) => {
let result = 0;
// try default next
result = model.openGLRenderWindow.getDefaultTextureInternalFormat(
vtktype,
numComps,
false
);
if (result) {
return result;
}
// try floating point
result = this.openGLRenderWindow.getDefaultTextureInternalFormat(
vtktype,
numComps,
true
);
if (!result) {
vtkDebugMacro('Unsupported internal texture type!');
vtkDebugMacro(
`Unable to find suitable internal format for T=${vtktype} NC= ${numComps}`
);
}
return result;
};
//----------------------------------------------------------------------------
publicAPI.setInternalFormat = (iFormat) => {
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.format = 0;
model.internalFormat = 0;
model.openGLDataType = 0;
};
//----------------------------------------------------------------------------
publicAPI.getDefaultDataType = (vtkScalarType) => {
// 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;
// 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:
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;
}
return model.context.UNSIGNED_BYTE;
}
};
//----------------------------------------------------------------------------
publicAPI.getOpenGLDataType = (vtkScalarType) => {
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:
default:
break;
}
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;
}
};
//----------------------------------------------------------------------------
function updateArrayDataType(dataType, data) {
const pixData = [];
// if the opengl data type is float
// then the data array must be float
if (
dataType !== VtkDataTypes.FLOAT &&
model.openGLDataType === model.context.FLOAT
) {
const pixCount = model.width * model.height * model.components;
for (let idx = 0; idx < data.length; idx++) {
const newArray = new Float32Array(pixCount);
for (let i = 0; i < pixCount; i++) {
newArray[i] = data[idx][i];
}
pixData.push(newArray);
}
}
// 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
) {
const pixCount = model.width * model.height * model.components;
for (let idx = 0; idx < data.length; idx++) {
const newArray = new Uint8Array(pixCount);
for (let i = 0; i < pixCount; i++) {
newArray[i] = data[idx][i];
}
pixData.push(newArray);
}
}
// 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 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 &&
(!vtkMath.isPowerOfTwo(width) || !vtkMath.isPowerOfTwo(height))
) {
// Scale up the texture to the next highest power of two dimensions.
const newWidth = vtkMath.nearestPowerOfTwo(width);
const newHeight = vtkMath.nearestPowerOfTwo(height);
const pixCount = newWidth * newHeight * model.components;
for (let idx = 0; idx < data.length; idx++) {
if (data[idx] !== null) {
let newArray = null;
switch (model.openGLDataType) {
case model.context.FLOAT:
newArray = new Float32Array(pixCount);
break;
default:
case model.context.UNSIGNED_BYTE:
newArray = new Uint8Array(pixCount);
break;
}
const jFactor = height / newHeight;
const iFactor = width / newWidth;
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++) {
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;
}
//----------------------------------------------------------------------------
publicAPI.create2DFromRaw = (width, height, numComps, dataType, 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_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 = updateArrayDataType(dataType, dataArray);
const scaledData = scaleTextureToHighestPowerOfTwo(pixData);
// Source texture data from the PBO.
// model.context.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1);
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);
}
publicAPI.deactivate();
return true;
};
//----------------------------------------------------------------------------
publicAPI.createCubeFromRaw = (width, height, numComps, dataType, 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 = updateArrayDataType(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] = new window[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);
// 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];
}
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;
}
}
// 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, height, dataType, 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);
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);
}
publicAPI.deactivate();
return true;
};
//----------------------------------------------------------------------------
publicAPI.create2DFromImage = (image) => {
// Now 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.width = image.width;
model.height = image.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);
// Scale up the texture to the next highest power of two dimensions (if needed) and flip y.
const needNearestPowerOfTwo =
!vtkMath.isPowerOfTwo(image.width) || !vtkMath.isPowerOfTwo(image.height);
const canvas = document.createElement('canvas');
canvas.width = needNearestPowerOfTwo
? vtkMath.nearestPowerOfTwo(image.width)
: image.width;
canvas.height = needNearestPowerOfTwo
? vtkMath.nearestPowerOfTwo(image.height)
: image.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
);
// In Chrome 69 on Windows and Ubuntu, there is a bug that prevents some
// canvases from working properly with webGL textures. By getting any
// image data from the canvas, this works around the bug. See
// https://bugs.chromium.org/p/chromium/issues/detail?id=896307
if (navigator.userAgent.indexOf('Chrome/69') >= 0) {
ctx.getImageData(0, 0, 1, 1);
}
const safeImage = canvas;
model.context.texImage2D(
model.target,
0,
model.internalFormat,
model.format,
model.openGLDataType,
safeImage
);
if (model.generateMipmap) {
model.context.generateMipmap(model.target);
}
publicAPI.deactivate();
return true;
};
//----------------------------------------------------------------------------
publicAPI.create3DFromRaw = (
width,
height,
depth,
numComps,
dataType,
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_3D;
model.components = numComps;
model.width = width;
model.height = height;
model.depth = depth;
model.numberOfDimensions = 3;
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);
model.context.texImage3D(
model.target,
0,
model.internalFormat,
model.width,
model.height,
model.depth,
0,
model.format,
model.openGLDataType,
data
);
if (model.generateMipmap) {
model.context.generateMipmap(model.target);
}
publicAPI.deactivate();
return true;
};
function computeScaleOffsets(numComps, numPixelsIn, data) {
// compute min and max values per component
const min = [];
const max = [];
for (let c = 0; c < numComps; ++c) {
min[c] = data[c];
max[c] = data[c];
}
let count = 0;
for (let i = 0; i < numPixelsIn; ++i) {
for (let c = 0; c < numComps; ++c) {
if (data[count] < min[c]) {
min[c] = data[count];
}
if (data[count] > max[c]) {
max[c] = data[count];
}
count++;
}
}
const offset = [];
const scale = [];
for (let c = 0; c < numComps; ++c) {
if (min[c] === max[c]) {
max[c] = min[c] + 1.0;
}
offset[c] = min[c];
scale[c] = max[c] - min[c];
}
return { scale, offset };
}
//----------------------------------------------------------------------------
// This method simulates a 3D texture using 2D
publicAPI.create3DFilterableFromRaw = (
width,
height,
depth,
numComps,
dataType,
data
) => {
const numPixelsIn = width * height * depth;
// initialize offset/scale
const offset = [];
const scale = [];
for (let c = 0; c < numComps; ++c) {
offset[c] = 0.0;
scale[c] = 1.0;
}
// store the information, we will need it later
// offset and scale are the offset and scale required to get
// the texture value back to data values ala
// data = texture * scale + offset
// and texture = (data - offset)/scale
model.volumeInfo = { scale, offset, width, height, depth };
// WebGL2 path, we have 3d textures etc
if (model.openGLRenderWindow.getWebgl2()) {
if (dataType === VtkDataTypes.FLOAT) {
return publicAPI.create3DFromRaw(
width,
height,
depth,
numComps,
dataType,
data
);
}
if (dataType === VtkDataTypes.UNSIGNED_CHAR) {
for (let c = 0; c < numComps; ++c) {
model.volumeInfo.scale[c] = 255.0;
}
return publicAPI.create3DFromRaw(
width,
height,
depth,
numComps,
dataType,
data
);
}
// otherwise convert to float
const newArray = new Float32Array(numPixelsIn * numComps);
// compute min and max values
const {
offset: computedOffset,
scale: computedScale,
} = computeScaleOffsets(numComps, numPixelsIn, data);
model.volumeInfo.offset = computedOffset;
model.volumeInfo.scale = computedScale;
let count = 0;
const scaleInverse = computedScale.map((s) => 1 / s);
for (let i = 0; i < numPixelsIn; i++) {
for (let nc = 0; nc < numComps; nc++) {
newArray[count] =
(data[count] - computedOffset[nc]) * scaleInverse[nc];
count++;
}
}
return publicAPI.create3DFromRaw(
width,
height,
depth,
numComps,
VtkDataTypes.FLOAT,
newArray
);
}
// not webgl2, deal with webgl1, no 3d textures
// and maybe no float textures
// compute min and max values
const res = computeScaleOffsets(numComps, numPixelsIn, data);
let volCopyData = (outArray, outIdx, inValue, smin, smax) => {
outArray[outIdx] = inValue;
};
let dataTypeToUse = VtkDataTypes.UNSIGNED_CHAR;
// unsigned char gets used as is
if (dataType === VtkDataTypes.UNSIGNED_CHAR) {
for (let c = 0; c < numComps; ++c) {
res.offset[c] = 0.0;
res.scale[c] = 255.0;
}
} else if (
model.context.getExtension('OES_texture_float') &&
model.context.getExtension('OES_texture_float_linear')
) {
// use float textures scaled to 0.0 to 1.0
dataTypeToUse = VtkDataTypes.FLOAT;
volCopyData = (outArray, outIdx, inValue, soffset, sscale) => {
outArray[outIdx] = (inValue - soffset) / sscale;
};
} else {
// worst case, scale data to uchar
dataTypeToUse = VtkDataTypes.UNSIGNED_CHAR;
volCopyData = (outArray, outIdx, inValue, soffset, sscale) => {
outArray[outIdx] = (255.0 * (inValue - soffset)) / sscale;
};
}
// Now determine the texture parameters using the arguments.
publicAPI.getOpenGLDataType(dataTypeToUse);
publicAPI.getInternalFormat(dataTypeToUse, numComps);
publicAPI.getFormat(dataTypeToUse, numComps);
if (!model.internalFormat || !model.format || !model.openGLDataType) {
vtkErrorMacro('Failed to determine texture parameters.');
return false;
}
// have to pack this 3D texture into pot 2D texture
model.target = model.context.TEXTURE_2D;
model.components = numComps;
model.depth = 1;
model.numberOfDimensions = 2;
// MAX_TEXTURE_SIZE gives the max dimensions that can be supported by the GPU,
// but it doesn't mean it will fit in memory. If we have to use a float data type
// or 4 components, there are good chances that the texture size will blow up
// and could not fit in the GPU memory. Use a smaller texture size in that case,
// which will force a downsampling of the dataset.
// That problem does not occur when using webGL2 since we can pack the data in
// denser textures based on our data type.
// TODO: try to fit in the biggest supported texture, catch the gl error if it
// does not fix (OUT_OF_MEMORY), then attempt with smaller texture
let maxTexDim = model.context.getParameter(model.context.MAX_TEXTURE_SIZE);
if (
maxTexDim > 4096 &&
(dataTypeToUse === VtkDataTypes.FLOAT || numComps >= 3)
) {
maxTexDim = 4096;
}
// compute estimate for XY subsample
let xstride = 1;
let ystride = 1;
if (numPixelsIn > maxTexDim * maxTexDim) {
xstride = Math.ceil(Math.sqrt(numPixelsIn / (maxTexDim * maxTexDim)));
ystride = xstride;
}
let targetWidth = Math.sqrt(numPixelsIn) / xstride;
targetWidth = vtkMath.nearestPowerOfTwo(targetWidth);
// determine X reps
const xreps = Math.floor((targetWidth * xstride) / width);
const yreps = Math.ceil(depth / xreps);
const targetHeight = vtkMath.nearestPowerOfTwo((height * yreps) / ystride);
model.width = targetWidth;
model.height = targetHeight;
model.openGLRenderWindow.activateTexture(publicAPI);
publicAPI.createTexture();
publicAPI.bind();
// store the information, we will need it later
model.volumeInfo.xreps = xreps;
model.volumeInfo.yreps = yreps;
model.volumeInfo.xstride = xstride;
model.volumeInfo.ystride = ystride;
model.volumeInfo.offset = res.offset;
model.volumeInfo.scale = res.scale;
// OK stuff the data into the 2d TEXTURE
// first allocate the new texture
let newArray;
const pixCount = targetWidth * targetHeight * numComps;
if (dataTypeToUse === VtkDataTypes.FLOAT) {
newArray = new Float32Array(pixCount);
} else {
newArray = new Uint8Array(pixCount);
}
// then stuff the data into it, nothing fancy right now
// for stride
let outIdx = 0;
const tileWidth = Math.floor(width / xstride);
const tileHeight = Math.floor(height / ystride);
for (let yRep = 0; yRep < yreps; yRep++) {
const xrepsThisRow = Math.min(xreps, depth - yRep * xreps);
const outXContIncr =
numComps * (model.width - xrepsThisRow * Math.floor(width / xstride));
for (let tileY = 0; tileY < tileHeight; tileY++) {
for (let xRep = 0; xRep < xrepsThisRow; xRep++) {
const inOffset =
numComps *
((yRep * xreps + xRep) * width * height + ystride * tileY * width);
for (let tileX = 0; tileX < tileWidth; tileX++) {
// copy value
for (let nc = 0; nc < numComps; nc++) {
volCopyData(
newArray,
outIdx,
data[inOffset + xstride * tileX * numComps + nc],
res.offset[nc],
res.scale[nc]
);
outIdx++;
}
}
}
outIdx += outXContIncr;
}
}
// Source texture data from the PBO.
// model.context.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1);
model.context.texImage2D(
model.target,
0,
model.internalFormat,
model.width,
model.height,
0,
model.format,
model.openGLDataType,
newArray
);
publicAPI.deactivate();
return true;
};
publicAPI.setOpenGLRenderWindow = (rw) => {
if (model.openGLRenderWindow === rw) {
return;
}
publicAPI.releaseGraphicsResources();
model.openGLRenderWindow = rw;
model.context = null;
if (rw) {
model.context = model.openGLRenderWindow.getContext();
}
};
//----------------------------------------------------------------------------
publicAPI.getMaximumTextureSize = (ctx) => {
if (ctx && ctx.isCurrent()) {
return ctx.getIntegerv(ctx.MAX_TEXTURE_SIZE);
}
return -1;
};
}
// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------
const DEFAULT_VALUES = {
openGLRenderWindow: null,
context: null,
handle: 0,
sendParametersTime: null,
textureBuildTime: null,
numberOfDimensions: 0,
target: 0,
format: 0,
openGLDataType: 0,
components: 0,
width: 0,
height: 0,
depth: 0,
autoParameters: true,
wrapS: Wrap.CLAMP_TO_EDGE,
wrapT: Wrap.CLAMP_TO_EDGE,
wrapR: Wrap.CLAMP_TO_EDGE,
minificationFilter: Filter.NEAREST,
magnificationFilter: Filter.NEAREST,
minLOD: -1000.0,
maxLOD: 1000.0,
baseLevel: 0,
maxLevel: 1000,
generateMipmap: false,
};
// ----------------------------------------------------------------------------
export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Inheritance
vtkViewNode.extend(publicAPI, model, initialValues);
model.sendParametersTime = {};
macro.obj(model.sendParametersTime, { mtime: 0 });
model.textureBuildTime = {};
macro.obj(model.textureBuildTime, { mtime: 0 });
// Build VTK API
macro.set(publicAPI, model, ['format', 'openGLDataType']);
macro.setGet(publicAPI, model, [
'keyMatrixTime',
'minificationFilter',
'magnificationFilter',
'wrapS',
'wrapT',
'wrapR',
'generateMipmap',
]);
macro.get(publicAPI, model, [
'width',
'height',
'volumeInfo',
'components',
'handle',
'target',
]);
// Object methods
vtkOpenGLTexture(publicAPI, model);
}
// ----------------------------------------------------------------------------
export const newInstance = macro.newInstance(extend, 'vtkOpenGLTexture');
// ----------------------------------------------------------------------------
export default { newInstance, extend, ...Constants };