UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

539 lines (497 loc) 24.5 kB
import { m as macro } from '../../macros2.js'; import vtkAbstractMapper from './AbstractMapper.js'; import vtkDataArray from '../../Common/Core/DataArray.js'; import vtkImageData from '../../Common/DataModel/ImageData.js'; import vtkLookupTable from '../../Common/Core/LookupTable.js'; import vtkScalarsToColors from '../../Common/Core/ScalarsToColors/Constants.js'; import { i as isNan } from '../../Common/Core/Math/index.js'; import Constants from './Mapper/Constants.js'; const { ColorMode, ScalarMode, GetArray } = Constants; const { VectorMode } = vtkScalarsToColors; const { VtkDataTypes } = vtkDataArray; /** * Increase by one the 3D coordinates * It will follow a zigzag pattern so that each coordinate is the neighbor of the next coordinate * This enables interpolation between two texels without issues * Note: texture coordinates can't be interpolated using this pattern * @param {vec3} coordinates The 3D coordinates using integers for each coordinate * @param {vec3} dimensions The 3D dimensions of the volume */ function updateZigzaggingCoordinates(coordinates, dimensions) { const directionX = coordinates[1] % 2 === 0 ? 1 : -1; coordinates[0] += directionX; if (coordinates[0] >= dimensions[0] || coordinates[0] < 0) { const directionY = coordinates[2] % 2 === 0 ? 1 : -1; coordinates[0] -= directionX; coordinates[1] += directionY; if (coordinates[1] >= dimensions[1] || coordinates[1] < 0) { coordinates[1] -= directionY; coordinates[2]++; } } } /** * Returns the index in the array representing the volume from a 3D coordinate * @param {vec3} coordinates The 3D integer coordinates * @param {vec3} dimensions The 3D dimensions of the volume * @returns The index in a flat array representing the volume */ function getIndexFromCoordinates(coordinates, dimensions) { return coordinates[0] + dimensions[0] * (coordinates[1] + dimensions[1] * coordinates[2]); } /** * Write texture coordinates for the given `texelIndexPosition` in `textureCoordinate`. * The `texelIndexPosition` is a floating point number that represents the distance in index space * from the center of the first texel to the final output position. * The output is given in texture coordinates and not in index coordinates (this is done at the very end of the function) * @param {vec3} textureCoordinate The output texture coordinates (to avoid allocating a new Array) * @param {Number} texelIndexPosition The floating point distance from the center of the first texel, following a zigzag pattern * @param {vec3} dimensions The 3D dimensions of the volume */ function getZigZagTextureCoordinatesFromTexelPosition(textureCoordinate, texelIndexPosition, dimensions) { // First compute the integer textureCoordinate const intTexelIndex = Math.floor(texelIndexPosition); const xCoordBeforeWrap = intTexelIndex % (2 * dimensions[0]); let xDirection; let xEndFlag; if (xCoordBeforeWrap < dimensions[0]) { textureCoordinate[0] = xCoordBeforeWrap; xDirection = 1; xEndFlag = textureCoordinate[0] === dimensions[0] - 1; } else { textureCoordinate[0] = 2 * dimensions[0] - 1 - xCoordBeforeWrap; xDirection = -1; xEndFlag = textureCoordinate[0] === 0; } const intRowIndex = Math.floor(intTexelIndex / dimensions[0]); const yCoordBeforeWrap = intRowIndex % (2 * dimensions[1]); let yDirection; let yEndFlag; if (yCoordBeforeWrap < dimensions[1]) { textureCoordinate[1] = yCoordBeforeWrap; yDirection = 1; yEndFlag = textureCoordinate[1] === dimensions[1] - 1; } else { textureCoordinate[1] = 2 * dimensions[1] - 1 - yCoordBeforeWrap; yDirection = -1; yEndFlag = textureCoordinate[1] === 0; } textureCoordinate[2] = Math.floor(intRowIndex / dimensions[1]); // Now add the remainder either in x, y or z const remainder = texelIndexPosition - intTexelIndex; if (xEndFlag) { if (yEndFlag) { textureCoordinate[2] += remainder; } else { textureCoordinate[1] += yDirection * remainder; } } else { textureCoordinate[0] += xDirection * remainder; } // textureCoordinates are in index space, convert to texture space textureCoordinate[0] = (textureCoordinate[0] + 0.5) / dimensions[0]; textureCoordinate[1] = (textureCoordinate[1] + 0.5) / dimensions[1]; textureCoordinate[2] = (textureCoordinate[2] + 0.5) / dimensions[2]; } // Associate an input vtkDataArray to an object { stringHash, textureCoordinates } // A single dataArray only caches one array of texture coordinates, so this cache is useless when // the input data array is used with two different lookup tables (which is very unlikely) const colorTextureCoordinatesCache = new WeakMap(); /** * The minimum of the range is mapped to the center of the first texel excluding min texel (texel at index distance 1) * The maximum of the range is mapped to the center of the last texel excluding max and NaN texels (texel at index distance numberOfColorsInRange) * The result is cached, and is reused if the arguments are the same and the input doesn't change * @param {vtkDataArray} input The input data array used for coloring * @param {Number} component The component of the input data array that is used for coloring (-1 for magnitude of the vectors) * @param {Range} range The range of the scalars * @param {boolean} useLogScale Should the values be transformed to logarithmic scale. When true, the range must already be in logarithmic scale. * @param {Number} numberOfColorsInRange The number of colors that are used in the range * @param {vec3} dimensions The dimensions of the texture * @param {boolean} useZigzagPattern If a zigzag pattern should be used. Otherwise 1 row for colors (including min and max) and 1 row for NaN are used. * @returns A vtkDataArray containing the texture coordinates (2D or 3D) */ function getOrCreateColorTextureCoordinates(input, component, range, useLogScale, numberOfColorsInRange, dimensions, useZigzagPattern) { // Caching using the "arguments" special object (because it is a pure function) const argStrings = new Array(arguments.length); for (let argIndex = 0; argIndex < arguments.length; ++argIndex) { // eslint-disable-next-line prefer-rest-params const arg = arguments[argIndex]; argStrings[argIndex] = arg.getMTime?.() ?? arg; } const stringHash = argStrings.join('/'); const cachedResult = colorTextureCoordinatesCache.get(input); if (cachedResult && cachedResult.stringHash === stringHash) { return cachedResult.textureCoordinates; } // The range used for computing coordinates have to change // slightly to accommodate the special above- and below-range // colors that are the first and last texels, respectively. const scalarTexelWidth = (range[1] - range[0]) / (numberOfColorsInRange - 1); const [paddedRangeMin, paddedRangeMax] = [range[0] - scalarTexelWidth, range[1] + scalarTexelWidth]; // Use the center of the voxel const textureSOrigin = paddedRangeMin - 0.5 * scalarTexelWidth; const textureSCoeff = 1.0 / (paddedRangeMax - paddedRangeMin + scalarTexelWidth); // Compute in index space first const texelIndexOrigin = paddedRangeMin; const texelIndexCoeff = (numberOfColorsInRange + 1) / (paddedRangeMax - paddedRangeMin); const inputV = input.getData(); const numScalars = input.getNumberOfTuples(); const numComps = input.getNumberOfComponents(); const useMagnitude = component < 0 || component >= numComps; const numberOfOutputComponents = dimensions[2] <= 1 ? 2 : 3; const output = vtkDataArray.newInstance({ numberOfComponents: numberOfOutputComponents, values: new Float32Array(numScalars * numberOfOutputComponents) }); const outputV = output.getData(); const nanTextureCoordinate = [0, 0, 0]; // Distance of NaN from the beginning: // min: 0, ...colorsInRange, max: numberOfColorsInRange + 1, NaN = numberOfColorsInRange + 2 getZigZagTextureCoordinatesFromTexelPosition(nanTextureCoordinate, numberOfColorsInRange + 2, dimensions); // Set a texture coordinate in the output for each tuple in the input let inputIdx = 0; let outputIdx = 0; const textureCoordinate = [0.5, 0.5, 0.5]; for (let scalarIdx = 0; scalarIdx < numScalars; ++scalarIdx) { // Get scalar value from magnitude or a single component let scalarValue; if (useMagnitude) { let sum = 0; for (let compIdx = 0; compIdx < numComps; ++compIdx) { const compValue = Number(inputV[inputIdx + compIdx]); sum += compValue * compValue; } scalarValue = Math.sqrt(sum); } else { scalarValue = Number(inputV[inputIdx + component]); } if (useLogScale) { scalarValue = Math.log10(scalarValue); } inputIdx += numComps; // Convert to texture coordinates and update output if (isNan(scalarValue)) { // Last texels are NaN colors (there is at least one NaN color) textureCoordinate[0] = nanTextureCoordinate[0]; textureCoordinate[1] = nanTextureCoordinate[1]; textureCoordinate[2] = nanTextureCoordinate[2]; } else if (useZigzagPattern) { // Texel position is in [0, numberOfColorsInRange + 1] let texelIndexPosition = (scalarValue - texelIndexOrigin) * texelIndexCoeff; if (texelIndexPosition < 1) { // Use min color when smaller than range texelIndexPosition = 0; } else if (texelIndexPosition > numberOfColorsInRange) { // Use max color when greater than range texelIndexPosition = numberOfColorsInRange + 1; } // Convert the texel position into texture coordinate following a zigzag pattern getZigZagTextureCoordinatesFromTexelPosition(textureCoordinate, texelIndexPosition, dimensions); } else { // 0.0 in t coordinate means not NaN. So why am I setting it to 0.49? // Because when you are mapping scalars and you have a NaN adjacent to // anything else, the interpolation everywhere should be NaN. Thus, I // want the NaN color everywhere except right on the non-NaN neighbors. // To simulate this, I set the t coord for the real numbers close to // the threshold so that the interpolation almost immediately looks up // the NaN value. textureCoordinate[1] = 0.49; // Some implementations apparently don't handle relatively large // numbers (compared to the range [0.0, 1.0]) very well. In fact, // values above 1122.0f appear to cause texture wrap-around on // some systems even when edge clamping is enabled. Why 1122.0f? I // don't know. For safety, we'll clamp at +/- 1000. This will // result in incorrect images when the texture value should be // above or below 1000, but I don't have a better solution. const textureS = (scalarValue - textureSOrigin) * textureSCoeff; if (textureS > 1000.0) { textureCoordinate[0] = 1000.0; } else if (textureS < -1000.0) { textureCoordinate[0] = -1000.0; } else { textureCoordinate[0] = textureS; } } for (let i = 0; i < numberOfOutputComponents; ++i) { outputV[outputIdx++] = textureCoordinate[i]; } } colorTextureCoordinatesCache.set(input, { stringHash, textureCoordinates: output }); return output; } // --------------------------------------------------------------------------- // vtkMapper2D methods // --------------------------------------------------------------------------- function vtkMapper2D(publicAPI, model) { // Set out className model.classHierarchy.push('vtkMapper2D'); publicAPI.createDefaultLookupTable = () => { model.lookupTable = vtkLookupTable.newInstance(); }; publicAPI.getColorModeAsString = () => macro.enumToString(ColorMode, model.colorMode); publicAPI.setColorModeToDefault = () => publicAPI.setColorMode(0); publicAPI.setColorModeToMapScalars = () => publicAPI.setColorMode(1); publicAPI.setColorModeToDirectScalars = () => publicAPI.setColorMode(2); publicAPI.getScalarModeAsString = () => macro.enumToString(ScalarMode, model.scalarMode); publicAPI.setScalarModeToDefault = () => publicAPI.setScalarMode(0); publicAPI.setScalarModeToUsePointData = () => publicAPI.setScalarMode(1); publicAPI.setScalarModeToUseCellData = () => publicAPI.setScalarMode(2); publicAPI.setScalarModeToUsePointFieldData = () => publicAPI.setScalarMode(3); publicAPI.setScalarModeToUseCellFieldData = () => publicAPI.setScalarMode(4); publicAPI.setScalarModeToUseFieldData = () => publicAPI.setScalarMode(5); publicAPI.getAbstractScalars = (input, scalarMode, arrayAccessMode, arrayId, arrayName) => { // make sure we have an input if (!input || !model.scalarVisibility) { return { scalars: null, cellFlag: false }; } let scalars = null; let cellFlag = false; // get scalar data and point/cell attribute according to scalar mode if (scalarMode === ScalarMode.DEFAULT) { scalars = input.getPointData().getScalars(); if (!scalars) { scalars = input.getCellData().getScalars(); cellFlag = true; } } else if (scalarMode === ScalarMode.USE_POINT_DATA) { scalars = input.getPointData().getScalars(); } else if (scalarMode === ScalarMode.USE_CELL_DATA) { scalars = input.getCellData().getScalars(); cellFlag = true; } else if (scalarMode === ScalarMode.USE_POINT_FIELD_DATA) { const pd = input.getPointData(); if (arrayAccessMode === GetArray.BY_ID) { scalars = pd.getArrayByIndex(arrayId); } else { scalars = pd.getArrayByName(arrayName); } } else if (scalarMode === ScalarMode.USE_CELL_FIELD_DATA) { const cd = input.getCellData(); cellFlag = true; if (arrayAccessMode === GetArray.BY_ID) { scalars = cd.getArrayByIndex(arrayId); } else { scalars = cd.getArrayByName(arrayName); } } else if (scalarMode === ScalarMode.USE_FIELD_DATA) { const fd = input.getFieldData(); if (arrayAccessMode === GetArray.BY_ID) { scalars = fd.getArrayByIndex(arrayId); } else { scalars = fd.getArrayByName(arrayName); } } return { scalars, cellFlag }; }; publicAPI.getLookupTable = () => { if (!model.lookupTable) { publicAPI.createDefaultLookupTable(); } return model.lookupTable; }; publicAPI.getMTime = () => { let mt = model.mtime; if (model.lookupTable !== null) { const time = model.lookupTable.getMTime(); mt = time > mt ? time : mt; } return mt; }; publicAPI.mapScalars = (input, alpha) => { const { scalars, cellFlag } = publicAPI.getAbstractScalars(input, model.scalarMode, model.arrayAccessMode, model.arrayId, model.colorByArrayName); model.areScalarsMappedFromCells = cellFlag; if (!scalars) { model.colorCoordinates = null; model.colorTextureMap = null; model.colorMapColors = null; return; } // we want to only recompute when something has changed const toString = `${publicAPI.getMTime()}${scalars.getMTime()}${alpha}`; if (model.colorBuildString === toString) return; if (!model.useLookupTableScalarRange) { publicAPI.getLookupTable().setRange(model.scalarRange[0], model.scalarRange[1]); } if (publicAPI.canUseTextureMapForColoring(scalars, cellFlag)) { model.mapScalarsToTexture(scalars, cellFlag, alpha); } else { model.colorCoordinates = null; model.colorTextureMap = null; const lut = publicAPI.getLookupTable(); if (lut) { // Ensure that the lookup table is built lut.build(); model.colorMapColors = lut.mapScalars(scalars, model.colorMode, model.fieldDataTupleId); } } model.colorBuildString = `${publicAPI.getMTime()}${scalars.getMTime()}${alpha}`; }; publicAPI.canUseTextureMapForColoring = (scalars, cellFlag) => { if (cellFlag && !(model.colorMode === ColorMode.DIRECT_SCALARS)) { return true; // cell data always use textures. } // index color does not use textures if (model.lookupTable && model.lookupTable.getIndexedLookup()) { return false; } if (!scalars) { // no scalars on this dataset, we don't care if texture is used at all. return false; } if (model.colorMode === ColorMode.DEFAULT && scalars.getDataType() === VtkDataTypes.UNSIGNED_CHAR || model.colorMode === ColorMode.DIRECT_SCALARS) { // Don't use texture if direct coloring using RGB unsigned chars is // requested. return false; } return true; }; // Protected method model.mapScalarsToTexture = (scalars, cellFlag, alpha) => { const range = model.lookupTable.getRange(); const useLogScale = model.lookupTable.usingLogScale(); const origAlpha = model.lookupTable.getAlpha(); const scaledRange = useLogScale ? [Math.log10(range[0]), Math.log10(range[1])] : range; // Get rid of vertex color array. Only texture or vertex coloring // can be active at one time. The existence of the array is the // signal to use that technique. model.colorMapColors = null; // If the lookup table has changed, then recreate the color texture map. // Set a new lookup table changes this->MTime. if (model.colorTextureMap == null || publicAPI.getMTime() > model.colorTextureMap.getMTime() || model.lookupTable.getMTime() > model.colorTextureMap.getMTime() || model.lookupTable.getAlpha() !== alpha) { model.lookupTable.setAlpha(alpha); model.colorTextureMap = null; // Get the texture map from the lookup table. // Create a dummy ramp of scalars. // In the future, we could extend vtkScalarsToColors. model.lookupTable.build(); const numberOfAvailableColors = model.lookupTable.getNumberOfAvailableColors(); // Maximum dimensions and number of colors in range const maxTextureWidthForCells = 2048; const maxColorsInRangeForCells = maxTextureWidthForCells ** 3 - 3; // 3D but keep a color for min, max and NaN const maxTextureWidthForPoints = 4096; const maxColorsInRangeForPoints = maxTextureWidthForPoints - 2; // 1D but keep a color for min and max (NaN is in a different row) // Minimum number of colors in range (excluding special colors like minColor, maxColor and NaNColor) const minColorsInRange = 2; // Maximum number of colors, limited by the maximum possible texture size const maxColorsInRange = cellFlag ? maxColorsInRangeForCells : maxColorsInRangeForPoints; model.numberOfColorsInRange = Math.min(Math.max(numberOfAvailableColors, minColorsInRange), maxColorsInRange); const numberOfColorsForCells = model.numberOfColorsInRange + 3; // Add min, max and NaN const numberOfColorsInUpperRowForPoints = model.numberOfColorsInRange + 2; // Add min and max ; the lower row will be used for NaN color const textureDimensions = cellFlag ? [Math.min(Math.ceil(numberOfColorsForCells / maxTextureWidthForCells ** 0), maxTextureWidthForCells), Math.min(Math.ceil(numberOfColorsForCells / maxTextureWidthForCells ** 1), maxTextureWidthForCells), Math.min(Math.ceil(numberOfColorsForCells / maxTextureWidthForCells ** 2), maxTextureWidthForCells)] : [numberOfColorsInUpperRowForPoints, 2, 1]; const textureSize = textureDimensions[0] * textureDimensions[1] * textureDimensions[2]; const scalarsArray = new Float64Array(textureSize); // Colors for NaN by default scalarsArray.fill(NaN); // Colors in range // Add 2 to also get color for min and max const numberOfNonSpecialColors = model.numberOfColorsInRange; const numberOfNonNaNColors = numberOfNonSpecialColors + 2; const textureCoordinates = [0, 0, 0]; const rangeMin = scaledRange[0]; const rangeDifference = scaledRange[1] - scaledRange[0]; for (let i = 0; i < numberOfNonNaNColors; ++i) { const scalarsArrayIndex = getIndexFromCoordinates(textureCoordinates, textureDimensions); // Minus 1 start at min color const intermediateValue = rangeMin + rangeDifference * (i - 1) / (numberOfNonSpecialColors - 1); const scalarValue = useLogScale ? 10.0 ** intermediateValue : intermediateValue; scalarsArray[scalarsArrayIndex] = scalarValue; // Colors are zigzagging to allow interpolation between two neighbor colors when coloring cells updateZigzaggingCoordinates(textureCoordinates, textureDimensions); } const scalarsDataArray = vtkDataArray.newInstance({ numberOfComponents: 1, values: scalarsArray }); const colorsDataArray = model.lookupTable.mapScalars(scalarsDataArray, model.colorMode, 0); model.colorTextureMap = vtkImageData.newInstance(); model.colorTextureMap.setDimensions(textureDimensions); model.colorTextureMap.getPointData().setScalars(colorsDataArray); model.lookupTable.setAlpha(origAlpha); } // Although I like the feature of applying magnitude to single component // scalars, it is not how the old MapScalars for vertex coloring works. const scalarComponent = model.lookupTable.getVectorMode() === VectorMode.MAGNITUDE && scalars.getNumberOfComponents() > 1 ? -1 : model.lookupTable.getVectorComponent(); // Create new coordinates if necessary, this function uses cache if possible. // A zigzag pattern can't be used with point data, as interpolation of texture coordinates will be wrong // A zigzag pattern can be used with cell data, as there will be no texture coordinates interpolation // The texture generated using a zigzag pattern in one dimension is the same as without zigzag // Therefore, the same code can be used for texture generation of point/cell data but not for texture coordinates model.colorCoordinates = getOrCreateColorTextureCoordinates(scalars, scalarComponent, scaledRange, useLogScale, model.numberOfColorsInRange, model.colorTextureMap.getDimensions(), cellFlag); }; publicAPI.getPrimitiveCount = () => { const input = publicAPI.getInputData(); const pcount = { points: input.getPoints().getNumberOfValues() / 3, verts: input.getVerts().getNumberOfValues() - input.getVerts().getNumberOfCells(), lines: input.getLines().getNumberOfValues() - 2 * input.getLines().getNumberOfCells(), triangles: input.getPolys().getNumberOfValues() - 3 * input.getPolys().getNumberOfCells() }; return pcount; }; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { static: false, lookupTable: null, scalarVisibility: false, scalarRange: [0, 1], useLookupTableScalarRange: false, colorMode: 0, scalarMode: 0, arrayAccessMode: 1, // By_NAME colorMapColors: null, // Same as this->Colors areScalarsMappedFromCells: false, renderTime: 0, colorByArrayName: null, transformCoordinate: null, viewSpecificProperties: null, customShaderAttributes: [] }; // ---------------------------------------------------------------------------- function extend(publicAPI, model) { let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; Object.assign(model, DEFAULT_VALUES, initialValues); // Inheritance vtkAbstractMapper.extend(publicAPI, model, initialValues); macro.get(publicAPI, model, ['areScalarsMappedFromCells', 'colorCoordinates', 'colorTextureMap', 'colorMapColors']); macro.setGet(publicAPI, model, ['arrayAccessMode', 'colorByArrayName', 'colorMode', 'lookupTable', 'renderTime', 'scalarMode', 'scalarVisibility', 'static', 'transformCoordinate', 'useLookupTableScalarRange', 'viewSpecificProperties', 'customShaderAttributes' // point data array names that will be transferred to the VBO ]); macro.setGetArray(publicAPI, model, ['scalarRange'], 2); if (!model.viewSpecificProperties) { model.viewSpecificProperties = {}; } // Object methods vtkMapper2D(publicAPI, model); } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkMapper2D'); // ---------------------------------------------------------------------------- var vtkMapper2D$1 = { newInstance, extend }; export { vtkMapper2D$1 as default, extend, newInstance };