diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 488e33060f2..5c58df6fd7d 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,6 +1,8 @@ ## From 32.x to 33 - **vtkMapper**: many properties have moved to `vtkVolumeProperty`. The full list of changed methods is: `getAnisotropy`, `getComputeNormalFromOpacity`, `getFilterMode`, `getFilterModeAsString`, `getGlobalIlluminationReach`, `getIpScalarRange`, `getIpScalarRangeByReference`, `getLAOKernelRadius`, `getLAOKernelSize`, `getLocalAmbientOcclusion`, `getPreferSizeOverAccuracy`, `getVolumetricScatteringBlending`, `setAnisotropy`, `setAverageIPScalarRange`, `setComputeNormalFromOpacity`, `setFilterMode`, `setFilterModeToNormalized`, `setFilterModeToOff`, `setFilterModeToRaw`, `setGlobalIlluminationReach`, `setIpScalarRange`, `setIpScalarRangeFrom`, `setLAOKernelRadius`, `setLAOKernelSize`, `setLocalAmbientOcclusion`, `setPreferSizeOverAccuracy`, `setVolumetricScatteringBlending`. +- **vtkOpenGLTexture**: The public `create2D*` and `create3D*` methods used to have positional parameters. These methods now use named parameters via passing in an object record. + ## From 31.x to 32 - **vtkMapper**: remove `mapScalarsToTexture` from the public API. The function becomes protected and its API changes. This shouldn't cause any issue in most cases. diff --git a/Sources/Common/Core/DataArray/index.d.ts b/Sources/Common/Core/DataArray/index.d.ts index e3054c53e19..e3f31f5a07c 100644 --- a/Sources/Common/Core/DataArray/index.d.ts +++ b/Sources/Common/Core/DataArray/index.d.ts @@ -83,6 +83,23 @@ export interface vtkDataArray extends vtkObject { */ setRange(rangeValue: vtkRange, componentIndex: number): Range; + /** + * Returns an array of the ranges for each component of the DataArray. + * Defaults to computing all the ranges if they aren't already computed. + * + * If the number of components is greater than 1, the last element in the + * ranges array is the min,max magnitude of the dataset. This is the same as + * calling `getRange(-1)`. + * + * Passing `getRanges(false)` will return a clone of the ranges that have + * already been computed. This is useful when you want to avoid recomputing + * the ranges, which can be expensive. + * + * @param {boolean} [computeRanges] (default: true) + * @returns {vtkRange[]} + */ + getRanges(computeRanges: boolean): vtkRange[]; + /** * Set the given tuple at the given index. * @param {Number} idx diff --git a/Sources/Common/Core/DataArray/index.js b/Sources/Common/Core/DataArray/index.js index 8bdba544a0a..405029bf238 100644 --- a/Sources/Common/Core/DataArray/index.js +++ b/Sources/Common/Core/DataArray/index.js @@ -281,6 +281,35 @@ function vtkDataArray(publicAPI, model) { return model.rangeTuple; }; + publicAPI.getRanges = (computeRanges = true) => { + if (!computeRanges) { + return structuredClone(model.ranges); + } + /** @type {import('../../../interfaces').vtkRange[]} */ + const ranges = []; + for (let i = 0; i < model.numberOfComponents; i++) { + const [min, max] = publicAPI.getRange(i); + /** @type {import('../../../interfaces').vtkRange} */ + const range = { + min, + max, + }; + ranges.push(range); + } + // where the number of components is greater than 1, the last element in + // the range array is the min,max magnitude of the entire dataset. + if (model.numberOfComponents > 1) { + const [min, max] = publicAPI.getRange(-1); + /** @type {import('../../../interfaces').vtkRange} */ + const range = { + min, + max, + }; + ranges.push(range); + } + return ranges; + }; + publicAPI.setTuple = (idx, tuple) => { const offset = idx * model.numberOfComponents; for (let i = 0; i < model.numberOfComponents; i++) { @@ -447,12 +476,18 @@ function vtkDataArray(publicAPI, model) { return sortedObj; }; + /** + * @param {import("./index").vtkDataArray} other + */ publicAPI.deepCopy = (other) => { // Retain current dataType and array reference before shallowCopy call. const currentType = publicAPI.getDataType(); const currentArray = model.values; publicAPI.shallowCopy(other); + // set the ranges + model.ranges = structuredClone(other.getRanges()); + // Avoid array reallocation if size already sufficient // and dataTypes match. if ( diff --git a/Sources/Common/Core/DataArray/test/testDataArray.js b/Sources/Common/Core/DataArray/test/testDataArray.js index 5a25ae08b9c..9b0cb2b9ac1 100644 --- a/Sources/Common/Core/DataArray/test/testDataArray.js +++ b/Sources/Common/Core/DataArray/test/testDataArray.js @@ -131,6 +131,36 @@ test('Test vtkDataArray getRange function with NaN values.', (t) => { t.end(); }); +test('Test vtkDataArray getRanges function with single-channel data.', (t) => { + // create a data array with a single channel. + const newArray = new Uint16Array(256 * 3); + + // fill the new array with the pattern 0,1,2,3,4,5, ..., 767. + for (let i = 0; i < 256 * 3; ++i) { + newArray[i] = i; + } + + const da = vtkDataArray.newInstance({ + numberOfComponents: 1, + values: newArray, + }); + + t.ok( + da.getRanges().length === 1, + 'getRanges should return an array of 1 vtkRange objects' + ); + t.ok( + da.getRanges()[0].min === 0, + 'the first component returned by getRanges minimum value should be 0' + ); + t.ok( + da.getRanges()[0].max === 767, + 'the first component returned by getRanges maximum value should be 767' + ); + + t.end(); +}); + test('Test vtkDataArray getTuple', (t) => { const da = vtkDataArray.newInstance({ numberOfComponents: 3, @@ -202,6 +232,98 @@ test('Test vtkDataArray getRange function with multi-channel data.', (t) => { t.end(); }); +test('Test vtkDataArray getRanges function with multi-channel data.', (t) => { + // create a data array with 3 channel data. + const numberOfPixels = 10; + const numberOfComponents = 4; + const newArray = new Uint16Array(numberOfPixels * numberOfComponents); + + // fill the new array with the pattern 1,2,3, 1,2,3 + // such that each channel has 1,1,1 2,2,2 3,3,3 respectively. + for (let i = 0; i < numberOfPixels; ++i) { + newArray[i * numberOfComponents] = i; + newArray[i * numberOfComponents + 1] = i * 2; + newArray[i * numberOfComponents + 2] = i * 3; + newArray[i * numberOfComponents + 3] = i * 4; + } + + const da = vtkDataArray.newInstance({ + numberOfComponents, + values: newArray, + }); + + const ranges = da.getRanges(); + + t.ok( + ranges.length === numberOfComponents + 1, + 'getRanges should return an array of 5 vtkRange objects' + ); + t.ok(ranges[0].min === 0, 'component:0 minimum value should be 0'); + t.ok(ranges[0].max === 9, 'component:0 maximum value should be 9'); + t.ok(ranges[1].min === 0, 'component:1 minimum value should be 0'); + t.ok(ranges[1].max === 18, 'component:1 maximum value should be 18'); + t.ok(ranges[2].min === 0, 'component:2 minimum value should be 0'); + t.ok(ranges[2].max === 27, 'component:2 maximum value should be 27 '); + t.ok( + ranges[2].min === 0, + 'component:-1 vector magnitude minimum should be 0' + ); + t.ok( + ranges[3].max === 36, + 'component:-1 vector magnitude maximum should be 36' + ); + + t.end(); +}); + +test('Test vtkDataArray getRanges(false) (`computeRanges=false`) function with multi-channel data', (t) => { + // create a data array with 3 channel data. + const numberOfPixels = 10; + const numberOfComponents = 4; + const newArray = new Uint16Array(numberOfPixels * numberOfComponents); + + // fill the new array with the pattern 1,2,3, 1,2,3 + // such that each channel has 1,1,1 2,2,2 3,3,3 respectively. + for (let i = 0; i < numberOfPixels; ++i) { + newArray[i * numberOfComponents] = i; + newArray[i * numberOfComponents + 1] = i * 2; + newArray[i * numberOfComponents + 2] = i * 3; + newArray[i * numberOfComponents + 3] = i * 4; + } + + const da = vtkDataArray.newInstance({ + numberOfComponents, + values: newArray, + }); + + // set `computeRanges` to false. This will prevent the ranges from being + // computed and will return only the ranges previously computer (if any). + const ranges = da.getRanges(false); + + t.ok(ranges === undefined, `getRanges should return undefined`); + + // now fetch the range for component 0. + da.getRange(0); + + // now fetch the ranges again with `computeRanges` set to false. + const updatedRanges = da.getRanges(false); + + // `updatedRanges` should now be only the range for component 0. because if + // was computed in `da.getRange(0)` + t.ok( + updatedRanges.length === numberOfComponents + 1, + 'getRanges should return an array of 5 vtkRange objects' + ); + t.ok(updatedRanges[0].min === 0, 'component:0 minimum value should be 0'); + t.ok(updatedRanges[0].max === 9, 'component:0 maximum value should be 9'); + t.ok(updatedRanges[1] === null, 'component:1 should be null'); + t.ok(updatedRanges[2] === null, 'component:2 should be null'); + t.ok(updatedRanges[3] === null, 'component:3 should be null'); + t.ok(updatedRanges[4] === null, 'component:-1 should be null'); + + t.end(); +}); + test('Test vtkDataArray insertNextTuple', (t) => { const dataArray = vtkDataArray.newInstance({ dataType: VtkDataTypes.UNSIGNED_CHAR, diff --git a/Sources/Rendering/Core/AbstractImageMapper/index.d.ts b/Sources/Rendering/Core/AbstractImageMapper/index.d.ts index 8bd657484ec..1d48ad9078a 100644 --- a/Sources/Rendering/Core/AbstractImageMapper/index.d.ts +++ b/Sources/Rendering/Core/AbstractImageMapper/index.d.ts @@ -122,6 +122,87 @@ export interface vtkAbstractImageMapper extends vtkAbstractMapper3D { * @param customDisplayExtent */ setCustomDisplayExtentFrom(customDisplayExtent: number[]): boolean; + + /** + * Set the opacity texture width. + * + * The default width (1024) should be fine in most instances. + * Only set this property if your opacity function range width is + * larger than 1024. + * + * A reasonable max texture size would be either 2048 or 4096, as those + * widths are supported by the vast majority of devices. Any width larger + * than that will have issues with device support. + * + * Specifying a width that is less than or equal to 0 will use the largest + * possible texture width on the device. Use this with caution! The max texture + * width of one device may not be the same for another device. + * + * You can find more information about supported texture widths at the following link: + * https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE + * + * @param {Number} width the texture width (defaults to 1024) + */ + setOpacityTextureWidth(width: number): boolean; + + /** + * Get the opacity texture width. + */ + getOpacityTextureWidth(): number; + + /** + * Set the color texture width. + * + * The default width (1024) should be fine in most instances. + * Only set this property if your color transfer function range width is + * larger than 1024. + * + * A reasonable max texture size would be either 2048 or 4096, as those + * widths are supported by the vast majority of devices. Any width larger + * than that will have issues with device support. + * + * Specifying a width that is less than or equal to 0 will use the largest + * possible texture width on the device. Use this with caution! The max texture + * width of one device may not be the same for another device. + * + * You can find more information about supported texture widths at the following link: + * https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE + * + * @param {Number} width the texture width (defaults to 1024) + */ + setColorTextureWidth(width: number): boolean; + + /** + * Get the color texture width. + */ + getColorTextureWidth(): number; + + /** + * Set the label outline texture width. + * + * The default width (1024) should be fine in most instances. + * Only set this property if you have more than 1024 labels + * that you want to render with thickness. + * + * A reasonable max texture size would be either 2048 or 4096, as those + * widths are supported by the vast majority of devices. Any width larger + * than that will have issues with device support. + * + * Specifying a width that is less than or equal to 0 will use the largest + * possible texture width on the device. Use this with caution! The max texture + * width of one device may not be the same for another device. + * + * You can find more information about supported texture widths at the following link: + * https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE + * + * @param {Number} width the texture width (defaults to 1024) + */ + setLabelOutlineTextureWidth(width: number): boolean; + + /** + * Get the label outline texture width. + */ + getLabelOutlineTextureWidth(): number; } /** diff --git a/Sources/Rendering/Core/AbstractImageMapper/index.js b/Sources/Rendering/Core/AbstractImageMapper/index.js index f202c506057..2fe3a9e22f8 100644 --- a/Sources/Rendering/Core/AbstractImageMapper/index.js +++ b/Sources/Rendering/Core/AbstractImageMapper/index.js @@ -30,6 +30,9 @@ const DEFAULT_VALUES = { customDisplayExtent: [0, 0, 0, 0, 0, 0], useCustomExtents: false, backgroundColor: [0, 0, 0, 1], + colorTextureWidth: 1024, + opacityTextureWidth: 1024, + labelOutlineTextureWidth: 1024, }; // ---------------------------------------------------------------------------- @@ -40,7 +43,13 @@ export function extend(publicAPI, model, initialValues = {}) { // Build VTK API vtkAbstractMapper3D.extend(publicAPI, model, initialValues); - macro.setGet(publicAPI, model, ['slice', 'useCustomExtents']); + macro.setGet(publicAPI, model, [ + 'slice', + 'useCustomExtents', + 'colorTextureWidth', + 'opacityTextureWidth', + 'labelOutlineTextureWidth', + ]); macro.setGetArray(publicAPI, model, ['customDisplayExtent'], 6); macro.setGetArray(publicAPI, model, ['backgroundColor'], 4); diff --git a/Sources/Rendering/Core/AbstractPicker/index.d.ts b/Sources/Rendering/Core/AbstractPicker/index.d.ts index d6164a6f256..b113a76c051 100755 --- a/Sources/Rendering/Core/AbstractPicker/index.d.ts +++ b/Sources/Rendering/Core/AbstractPicker/index.d.ts @@ -1,6 +1,6 @@ import { vtkObject } from '../../../interfaces'; import { Vector3 } from '../../../types'; -import vtkActor from '../Actor'; +import vtkProp3D from '../Prop3D'; import vtkRenderer from '../Renderer'; /** @@ -10,8 +10,8 @@ export interface IAbstractPickerInitialValues { renderer?: vtkRenderer; selectionPoint?: Vector3; pickPosition?: Vector3; - pickFromList?: number; - pickList?: vtkActor[]; + pickFromList?: boolean; + pickList?: vtkProp3D[]; } /** @@ -20,15 +20,15 @@ export interface IAbstractPickerInitialValues { export interface vtkAbstractPicker extends vtkObject { /** * - * @param {vtkActor} actor + * @param {vtkProp3D} prop */ - addPickList(actor: vtkActor): void; + addPickList(prop: vtkProp3D): void; /** * - * @param {vtkActor} actor + * @param {vtkProp3D} prop */ - deletePickList(actor: vtkActor): void; + deletePickList(prop: vtkProp3D): void; /** * @@ -38,7 +38,7 @@ export interface vtkAbstractPicker extends vtkObject { /** * */ - getPickList(): boolean; + getPickList(): vtkProp3D[]; /** * Get the picked position @@ -82,17 +82,17 @@ export interface vtkAbstractPicker extends vtkObject { /** * - * @param {Number} pickFromList - * @default 0 + * @param {Boolean} pickFromList + * @default false */ - setPickFromList(pickFromList: number): boolean; + setPickFromList(pickFromList: boolean): boolean; /** * - * @param {vtkActor[]} pickList + * @param {vtkProp3D[]} pickList * @default [] */ - setPickList(pickList: vtkActor[]): boolean; + setPickList(pickList: vtkProp3D[]): boolean; } /** diff --git a/Sources/Rendering/Core/AbstractPicker/index.js b/Sources/Rendering/Core/AbstractPicker/index.js index fea9b0410f9..39fd684c716 100644 --- a/Sources/Rendering/Core/AbstractPicker/index.js +++ b/Sources/Rendering/Core/AbstractPicker/index.js @@ -44,7 +44,7 @@ const DEFAULT_VALUES = { renderer: null, selectionPoint: [0.0, 0.0, 0.0], pickPosition: [0.0, 0.0, 0.0], - pickFromList: 0, + pickFromList: false, pickList: [], }; diff --git a/Sources/Rendering/Core/Actor2D/index.d.ts b/Sources/Rendering/Core/Actor2D/index.d.ts index d02aaf44724..ac008e80e9e 100755 --- a/Sources/Rendering/Core/Actor2D/index.d.ts +++ b/Sources/Rendering/Core/Actor2D/index.d.ts @@ -54,6 +54,28 @@ export interface vtkActor2D extends vtkProp { */ getMapper(): vtkMapper2D; + /** + * Set the layer number for this 2D actor. + * The scenegraph uses this layer number to sort actor 2D overlays/underlays on top of each other. + * The actor2D with the highest layer number is going to be rendered at the very front i.e. it is + * the top-most layer. + * If two actor2D instances share the same layer number, they are rendered in the order in which + * they were added to the renderer via `addActor` or `addActor2D`. + * By default, each actor2D has a layer number of 0. + */ + setLayerNumber(layer: number): void; + + /** + * Get the layer number for this 2D actor. + * The scenegraph uses this layer number to sort actor 2D overlays/underlays on top of each other. + * The actor2D with the highest layer number is going to be rendered at the very front i.e. it is + * the top-most layer. + * If two actor2D instances share the same layer number, they are rendered in the order in which + * they were added to the renderer via `addActor` or `addActor2D`. + * By default, each actor2D has a layer number of 0. + */ + getLayerNumber(): number; + /** * */ diff --git a/Sources/Rendering/Core/Actor2D/index.js b/Sources/Rendering/Core/Actor2D/index.js index d66fdedbd8b..4c87b159160 100644 --- a/Sources/Rendering/Core/Actor2D/index.js +++ b/Sources/Rendering/Core/Actor2D/index.js @@ -162,7 +162,7 @@ export function extend(publicAPI, model, initialValues = {}) { // Build VTK API macro.set(publicAPI, model, ['property']); - macro.setGet(publicAPI, model, ['mapper']); + macro.setGet(publicAPI, model, ['mapper', 'layerNumber']); // Object methods vtkActor2D(publicAPI, model); diff --git a/Sources/Rendering/Core/Actor2D/test/testActor2D.js b/Sources/Rendering/Core/Actor2D/test/testActor2D.js index eaf9fe8ec7d..ae2e8ba80a8 100644 --- a/Sources/Rendering/Core/Actor2D/test/testActor2D.js +++ b/Sources/Rendering/Core/Actor2D/test/testActor2D.js @@ -43,17 +43,27 @@ test.onlyIfWebGL('Test Actor2D', (t) => { actor2D.getProperty().setOpacity(0.3); actor2D.getProperty().setDisplayLocation(DisplayLocation.FOREGROUND); actor2D.getProperty().setRepresentation(Representation.SURFACE); + actor2D.setLayerNumber(2); renderer.addActor2D(actor2D); const actor2D1 = gc.registerResource(vtkActor2D.newInstance()); actor2D1.getProperty().setColor([0.1, 0.8, 0.5]); actor2D1.getProperty().setDisplayLocation(DisplayLocation.BACKGROUND); actor2D1.getProperty().setRepresentation(Representation.SURFACE); renderer.addActor2D(actor2D1); + actor2D1.setLayerNumber(1); + const actor2D2 = gc.registerResource(vtkActor2D.newInstance()); + actor2D2.getProperty().setColor([0.8, 0.4, 0.4]); + actor2D2.getProperty().setOpacity(1.0); + actor2D2.getProperty().setDisplayLocation(DisplayLocation.FOREGROUND); + actor2D2.getProperty().setRepresentation(Representation.SURFACE); + actor2D2.setLayerNumber(1); + renderer.addActor2D(actor2D2); const mapper = gc.registerResource(vtkMapper.newInstance()); actor.setMapper(mapper); const mapper2D = gc.registerResource(vtkMapper2D.newInstance()); const mapper2D1 = gc.registerResource(vtkMapper2D.newInstance()); + const mapper2D2 = gc.registerResource(vtkMapper2D.newInstance()); const c = vtkCoordinate.newInstance(); c.setCoordinateSystemToWorld(); mapper2D.setTransformCoordinate(c); @@ -62,21 +72,30 @@ test.onlyIfWebGL('Test Actor2D', (t) => { mapper2D1.setTransformCoordinate(c); mapper2D1.setScalarVisibility(false); actor2D1.setMapper(mapper2D1); + mapper2D2.setTransformCoordinate(c); + mapper2D2.setScalarVisibility(false); + actor2D2.setMapper(mapper2D2); const cubeSource = gc.registerResource(vtkCubeSource.newInstance()); mapper.setInputConnection(cubeSource.getOutputPort()); const sphereSource = gc.registerResource(vtkSphereSource.newInstance()); sphereSource.setCenter(-0.5, 0.0, 0.0); - sphereSource.setRadius(0.3); + sphereSource.setRadius(0.35); sphereSource.setThetaResolution(25); sphereSource.setPhiResolution(25); mapper2D.setInputConnection(sphereSource.getOutputPort()); const sphereSource1 = gc.registerResource(vtkSphereSource.newInstance()); - sphereSource1.setCenter(0.5, -0.3, 0.0); - sphereSource1.setRadius(0.3); + sphereSource1.setCenter(0, -0.5, 0.0); + sphereSource1.setRadius(0.45); sphereSource1.setThetaResolution(30); sphereSource1.setPhiResolution(30); mapper2D1.setInputConnection(sphereSource1.getOutputPort()); + const sphereSource2 = gc.registerResource(vtkSphereSource.newInstance()); + sphereSource2.setCenter(-0.2, -0.3, 0.0); + sphereSource2.setRadius(0.35); + sphereSource2.setThetaResolution(30); + sphereSource2.setPhiResolution(30); + mapper2D2.setInputConnection(sphereSource2.getOutputPort()); renderer.getActiveCamera().azimuth(25); renderer.getActiveCamera().roll(25); diff --git a/Sources/Rendering/Core/Actor2D/test/testActor2D.png b/Sources/Rendering/Core/Actor2D/test/testActor2D.png index 72082108d1f..1dd748b8cdd 100644 Binary files a/Sources/Rendering/Core/Actor2D/test/testActor2D.png and b/Sources/Rendering/Core/Actor2D/test/testActor2D.png differ diff --git a/Sources/Rendering/Core/CellPicker/index.js b/Sources/Rendering/Core/CellPicker/index.js index 61de00af763..28940f809c7 100644 --- a/Sources/Rendering/Core/CellPicker/index.js +++ b/Sources/Rendering/Core/CellPicker/index.js @@ -277,7 +277,10 @@ function vtkCellPicker(publicAPI, model) { // calculate opacity table const numIComps = 1; - const oWidth = 1024; + let oWidth = mapper.getOpacityTextureWidth(); + if (oWidth <= 0) { + oWidth = 1024; + } const tmpTable = new Float32Array(oWidth); const opacityArray = new Float32Array(oWidth); let ofun; diff --git a/Sources/Rendering/Core/ImageCPRMapper/index.js b/Sources/Rendering/Core/ImageCPRMapper/index.js index de5d842869d..2db3873a27c 100644 --- a/Sources/Rendering/Core/ImageCPRMapper/index.js +++ b/Sources/Rendering/Core/ImageCPRMapper/index.js @@ -331,7 +331,7 @@ function vtkImageCPRMapper(publicAPI, model) { // Object factory // ---------------------------------------------------------------------------- -const DEFAULT_VALUES = { +const defaultValues = (initialValues) => ({ width: 10, uniformOrientation: [0, 0, 0, 1], useUniformOrientation: false, @@ -344,12 +344,13 @@ const DEFAULT_VALUES = { projectionSlabThickness: 1, projectionSlabNumberOfSamples: 1, projectionMode: ProjectionMode.MAX, -}; + ...initialValues, +}); // ---------------------------------------------------------------------------- export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, DEFAULT_VALUES, initialValues); + Object.assign(model, defaultValues(initialValues)); // Inheritance vtkAbstractImageMapper.extend(publicAPI, model, initialValues); diff --git a/Sources/Rendering/Core/ImageProperty/index.d.ts b/Sources/Rendering/Core/ImageProperty/index.d.ts index 09c01cf2351..0d2e8e9c985 100755 --- a/Sources/Rendering/Core/ImageProperty/index.d.ts +++ b/Sources/Rendering/Core/ImageProperty/index.d.ts @@ -1,5 +1,5 @@ import { vtkObject } from '../../../interfaces'; -import { Nullable } from '../../../types'; +import { Extent, Nullable } from '../../../types'; import vtkColorTransferFunction from '../ColorTransferFunction'; import vtkPiecewiseFunction from '../../../Common/DataModel/PiecewiseFunction'; import { InterpolationType } from './Constants'; @@ -231,6 +231,25 @@ export interface vtkImageProperty extends vtkObject { * @param {Boolean} useLookupTableScalarRange */ setUseLookupTableScalarRange(useLookupTableScalarRange: boolean): boolean; + + /** + * Informs the mapper to only update the specified extents at the next render. + * + * If there are zero extents, the mapper updates the entire volume texture. + * Otherwise, the mapper will only update the texture by the specified extents + * during the next render call. + * + * This array is cleared after a successful render. + * @param extents + */ + setUpdatedExtents(extents: Extent[]): boolean; + + /** + * Retrieves the updated extents. + * + * This array is cleared after every successful render. + */ + getUpdatedExtents(): Extent[]; } /** diff --git a/Sources/Rendering/Core/ImageProperty/index.js b/Sources/Rendering/Core/ImageProperty/index.js index 88e5b807efd..0e651ea92e1 100644 --- a/Sources/Rendering/Core/ImageProperty/index.js +++ b/Sources/Rendering/Core/ImageProperty/index.js @@ -130,7 +130,7 @@ function vtkImageProperty(publicAPI, model) { // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- -const DEFAULT_VALUES = { +const defaultValues = (initialValues) => ({ independentComponents: false, interpolationType: InterpolationType.LINEAR, colorWindow: 255, @@ -142,12 +142,14 @@ const DEFAULT_VALUES = { useLabelOutline: false, labelOutlineThickness: [1], labelOutlineOpacity: 1.0, -}; + updatedExtents: [], + ...initialValues, +}); // ---------------------------------------------------------------------------- export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, DEFAULT_VALUES, initialValues); + Object.assign(model, defaultValues(initialValues)); // Build VTK API macro.obj(publicAPI, model); @@ -174,6 +176,7 @@ export function extend(publicAPI, model, initialValues = {}) { 'useLookupTableScalarRange', 'useLabelOutline', 'labelOutlineOpacity', + 'updatedExtents', ]); macro.setGetArray(publicAPI, model, ['labelOutlineThickness']); diff --git a/Sources/Rendering/Core/ImageResliceMapper/index.d.ts b/Sources/Rendering/Core/ImageResliceMapper/index.d.ts index 012dbb97452..5e45a29a930 100755 --- a/Sources/Rendering/Core/ImageResliceMapper/index.d.ts +++ b/Sources/Rendering/Core/ImageResliceMapper/index.d.ts @@ -1,10 +1,9 @@ import vtkAbstractImageMapper, { IAbstractImageMapperInitialValues, } from '../AbstractImageMapper'; -import vtkImageData from '../../../Common/DataModel/ImageData'; import vtkPlane from '../../../Common/DataModel/Plane'; import vtkPolyData from '../../../Common/DataModel/PolyData'; -import { Bounds, Nullable, Vector3 } from '../../../types'; +import { Bounds } from '../../../types'; import { SlabTypes } from './Constants'; import CoincidentTopologyHelper, { StaticCoincidentTopologyMethods, diff --git a/Sources/Rendering/Core/ImageResliceMapper/index.js b/Sources/Rendering/Core/ImageResliceMapper/index.js index 66136a4c85a..0f3ffe0f810 100644 --- a/Sources/Rendering/Core/ImageResliceMapper/index.js +++ b/Sources/Rendering/Core/ImageResliceMapper/index.js @@ -39,18 +39,19 @@ function vtkImageResliceMapper(publicAPI, model) { // Object factory // ---------------------------------------------------------------------------- -const DEFAULT_VALUES = { +const defaultValues = (initialValues) => ({ slabThickness: 0.0, slabTrapezoidIntegration: 0, slabType: SlabTypes.MEAN, slicePlane: null, slicePolyData: null, -}; + ...initialValues, +}); // ---------------------------------------------------------------------------- export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, DEFAULT_VALUES, initialValues); + Object.assign(model, defaultValues(initialValues)); // Build VTK API vtkAbstractImageMapper.extend(publicAPI, model, initialValues); diff --git a/Sources/Rendering/Core/Viewport/index.js b/Sources/Rendering/Core/Viewport/index.js index d212239b80e..ad7fe218f63 100644 --- a/Sources/Rendering/Core/Viewport/index.js +++ b/Sources/Rendering/Core/Viewport/index.js @@ -47,10 +47,20 @@ function vtkViewport(publicAPI, model) { } publicAPI.getViewPropsWithNestedProps = () => { - const allPropsArray = []; - for (let i = 0; i < model.props.length; i++) { - gatherProps(model.props[i], allPropsArray); + let allPropsArray = []; + // Handle actor2D instances separately so that they can be overlayed and layered + const actors2D = publicAPI.getActors2D(); + // Sort the actor2D list using its layer number + actors2D.sort((a, b) => a.getLayerNumber() - b.getLayerNumber()); + // Filter out all the actor2D instances + const newPropList = model.props.filter((item) => !actors2D.includes(item)); + for (let i = 0; i < newPropList.length; i++) { + gatherProps(newPropList[i], allPropsArray); } + // Finally, add the actor2D props at the end of the list + // This works because, when traversing the render pass in vtkOpenGLRenderer, the children are + // traversed in the order that they are added to the list + allPropsArray = allPropsArray.concat(actors2D); return allPropsArray; }; diff --git a/Sources/Rendering/Core/VolumeMapper/index.d.ts b/Sources/Rendering/Core/VolumeMapper/index.d.ts index b6d2a7f15a8..67fc06b07e2 100755 --- a/Sources/Rendering/Core/VolumeMapper/index.d.ts +++ b/Sources/Rendering/Core/VolumeMapper/index.d.ts @@ -162,6 +162,76 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D { * */ update(): void; + + /** + * Set the opacity texture width. + * + * The default width (1024) should be fine in most instances. + * Only set this property if your opacity function range width is + * larger than 1024. + * + * @param {Number} width the texture width (defaults to 1024) + */ + setOpacityTextureWidth(width: number): boolean; + + /** + * Get the opacity texture width. + */ + getOpacityTextureWidth(): number; + + /** + * Set the color texture width. + * + * The default width (1024) should be fine in most instances. + * Only set this property if your color transfer function range width is + * larger than 1024. + * + * A reasonable max texture size would be either 2048 or 4096, as those + * widths are supported by the vast majority of devices. Any width larger + * than that will have issues with device support. + * + * Specifying a width that is less than or equal to 0 will use the largest + * possible texture width on the device. Use this with caution! The max texture + * width of one device may not be the same for another device. + * + * You can find more information about supported texture widths at the following link: + * https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE + * + * @param {Number} width the texture width (defaults to 1024) + */ + setColorTextureWidth(width: number): boolean; + + /** + * Get the color texture width. + */ + getColorTextureWidth(): number; + + /** + * Set the label outline texture width. + * + * The default width (1024) should be fine in most instances. + * Only set this property if you have more than 1024 labels + * that you want to render with thickness. + * + * A reasonable max texture size would be either 2048 or 4096, as those + * widths are supported by the vast majority of devices. Any width larger + * than that will have issues with device support. + * + * Specifying a width that is less than or equal to 0 will use the largest + * possible texture width on the device. Use this with caution! The max texture + * width of one device may not be the same for another device. + * + * You can find more information about supported texture widths at the following link: + * https://web3dsurvey.com/webgl/parameters/MAX_TEXTURE_SIZE + * + * @param {Number} width the texture width (defaults to 1024) + */ + setLabelOutlineTextureWidth(width: number): boolean; + + /** + * Get the label outline texture width. + */ + getLabelOutlineTextureWidth(): number; } /** diff --git a/Sources/Rendering/Core/VolumeMapper/index.js b/Sources/Rendering/Core/VolumeMapper/index.js index a4f169a48b8..bd505839ea7 100644 --- a/Sources/Rendering/Core/VolumeMapper/index.js +++ b/Sources/Rendering/Core/VolumeMapper/index.js @@ -137,7 +137,8 @@ function vtkVolumeMapper(publicAPI, model) { // Object factory // ---------------------------------------------------------------------------- -const DEFAULT_VALUES = { +// TODO: what values to use for averageIPScalarRange to get GLSL to use max / min values like [-Math.inf, Math.inf]? +const defaultValues = (initialValues) => ({ bounds: [...vtkBoundingBox.INIT_BOUNDS], sampleDistance: 1.0, imageSampleDistance: 1.0, @@ -147,12 +148,16 @@ const DEFAULT_VALUES = { interactionSampleDistanceFactor: 1.0, blendMode: BlendMode.COMPOSITE_BLEND, volumeShadowSamplingDistFactor: 5.0, -}; + colorTextureWidth: 1024, + opacityTextureWidth: 1024, + labelOutlineTextureWidth: 1024, + ...initialValues, +}); // ---------------------------------------------------------------------------- export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, DEFAULT_VALUES, initialValues); + Object.assign(model, defaultValues(initialValues)); vtkAbstractMapper3D.extend(publicAPI, model, initialValues); @@ -165,6 +170,9 @@ export function extend(publicAPI, model, initialValues = {}) { 'interactionSampleDistanceFactor', 'blendMode', 'volumeShadowSamplingDistFactor', + 'colorTextureWidth', + 'opacityTextureWidth', + 'labelOutlineTextureWidth', ]); macro.event(publicAPI, model, 'lightingActivated'); diff --git a/Sources/Rendering/Core/VolumeProperty/index.d.ts b/Sources/Rendering/Core/VolumeProperty/index.d.ts index d2e624833a4..5ef91d521fa 100755 --- a/Sources/Rendering/Core/VolumeProperty/index.d.ts +++ b/Sources/Rendering/Core/VolumeProperty/index.d.ts @@ -1,6 +1,6 @@ import vtkPiecewiseFunction from '../../../Common/DataModel/PiecewiseFunction'; import { vtkObject } from '../../../interfaces'; -import { Nullable } from '../../../types'; +import { Extent, Nullable } from '../../../types'; import vtkColorTransferFunction from '../ColorTransferFunction'; import { ColorMixPreset, InterpolationType, OpacityMode } from './Constants'; @@ -486,6 +486,25 @@ export interface vtkVolumeProperty extends vtkObject { * @param LAOKernelRadius */ setLAOKernelRadius(LAOKernelRadius: number): void; + + /** + * Informs the mapper to only update the specified extents at the next render. + * + * If there are zero extents, the mapper updates the entire volume texture. + * Otherwise, the mapper will only update the texture by the specified extents + * during the next render call. + * + * This array is cleared after a successful render. + * @param extents + */ + setUpdatedExtents(extents: Extent[]): boolean; + + /** + * Retrieves the updated extents. + * + * This array is cleared after every successful render. + */ + getUpdatedExtents(): Extent[]; } /** diff --git a/Sources/Rendering/Core/VolumeProperty/index.js b/Sources/Rendering/Core/VolumeProperty/index.js index 5574212cf01..7c6d507139f 100644 --- a/Sources/Rendering/Core/VolumeProperty/index.js +++ b/Sources/Rendering/Core/VolumeProperty/index.js @@ -286,7 +286,7 @@ function vtkVolumeProperty(publicAPI, model) { // Object factory // ---------------------------------------------------------------------------- -const DEFAULT_VALUES = { +const defaultValues = (initialValues) => ({ colorMixPreset: ColorMixPreset.DEFAULT, independentComponents: true, interpolationType: InterpolationType.FAST_LINEAR, @@ -312,12 +312,15 @@ const DEFAULT_VALUES = { localAmbientOcclusion: false, LAOKernelSize: 15, LAOKernelRadius: 7, -}; + updatedExtents: [], + + ...initialValues, +}); // ---------------------------------------------------------------------------- export function extend(publicAPI, model, initialValues = {}) { - Object.assign(model, DEFAULT_VALUES, initialValues); + Object.assign(model, defaultValues(initialValues)); // Build VTK API macro.obj(publicAPI, model); @@ -366,6 +369,7 @@ export function extend(publicAPI, model, initialValues = {}) { 'localAmbientOcclusion', 'LAOKernelSize', 'LAOKernelRadius', + 'updatedExtents', ]); // Property moved from volume mapper diff --git a/Sources/Rendering/Misc/SynchronizableRenderWindow/BehaviorManager/CameraSynchronizer.js b/Sources/Rendering/Misc/SynchronizableRenderWindow/BehaviorManager/CameraSynchronizer.js index da31aca9e77..b1c7d78184a 100644 --- a/Sources/Rendering/Misc/SynchronizableRenderWindow/BehaviorManager/CameraSynchronizer.js +++ b/Sources/Rendering/Misc/SynchronizableRenderWindow/BehaviorManager/CameraSynchronizer.js @@ -41,8 +41,8 @@ function vtkCameraSynchronizer(publicAPI, model) { } // Update listeners automatically - model._srcRendererChanged = updateListeners; - model._dstRendererChanged = updateListeners; + model._onSrcRendererChanged = updateListeners; + model._onDstRendererChanged = updateListeners; function updatePreviousValues(position, focalPoint, viewUp) { if ( diff --git a/Sources/Rendering/OpenGL/ForwardPass/index.js b/Sources/Rendering/OpenGL/ForwardPass/index.js index c6570217ffc..11756b221c3 100644 --- a/Sources/Rendering/OpenGL/ForwardPass/index.js +++ b/Sources/Rendering/OpenGL/ForwardPass/index.js @@ -40,8 +40,8 @@ function vtkForwardPass(publicAPI, model) { model.translucentActorCount = 0; model.volumeCount = 0; model.overlayActorCount = 0; - publicAPI.setCurrentOperation('queryPass'); + publicAPI.setCurrentOperation('queryPass'); renNode.traverse(publicAPI); // do we need to capture a zbuffer? diff --git a/Sources/Rendering/OpenGL/Framebuffer/index.js b/Sources/Rendering/OpenGL/Framebuffer/index.js index 2cd9787e203..045eaea3466 100644 --- a/Sources/Rendering/OpenGL/Framebuffer/index.js +++ b/Sources/Rendering/OpenGL/Framebuffer/index.js @@ -244,13 +244,13 @@ function vtkFramebuffer(publicAPI, model) { texture.setOpenGLRenderWindow(model._openGLRenderWindow); texture.setMinificationFilter(Filter.LINEAR); texture.setMagnificationFilter(Filter.LINEAR); - texture.create2DFromRaw( - model.glFramebuffer.width, - model.glFramebuffer.height, - 4, - VtkDataTypes.UNSIGNED_CHAR, - null - ); + texture.create2DFromRaw({ + width: model.glFramebuffer.width, + height: model.glFramebuffer.height, + numComps: 4, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: null, + }); publicAPI.setColorBuffer(texture); // for now do not count on having a depth buffer texture diff --git a/Sources/Rendering/OpenGL/ImageCPRMapper/index.js b/Sources/Rendering/OpenGL/ImageCPRMapper/index.js index f30bc8eac57..4307e271a46 100644 --- a/Sources/Rendering/OpenGL/ImageCPRMapper/index.js +++ b/Sources/Rendering/OpenGL/ImageCPRMapper/index.js @@ -187,6 +187,7 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) { publicAPI.buildBufferObjects = (ren, actor) => { const image = model.currentImageDataInput; const centerline = model.currentCenterlineInput; + const property = actor.getProperty(); // Rebuild the volumeTexture if the data has changed const scalars = image?.getPointData()?.getScalars(); @@ -199,6 +200,9 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) { const reBuildTex = !cachedScalarsEntry?.oglObject?.getHandle() || cachedScalarsEntry?.hash !== volumeTextureHash; + const updatedExtents = property.getUpdatedExtents(); + const hasUpdatedExtents = !!updatedExtents.length; + if (reBuildTex) { model.volumeTexture = vtkOpenGLTexture.newInstance(); model.volumeTexture.setOpenGLRenderWindow(model._openGLRenderWindow); @@ -209,13 +213,13 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) { model.context.getExtension('EXT_texture_norm16') ); model.volumeTexture.resetFormatAndType(); - model.volumeTexture.create3DFilterableFromDataArray( - dims[0], - dims[1], - dims[2], - scalars, - model.renderable.getPreferSizeOverAccuracy() - ); + model.volumeTexture.create3DFilterableFromDataArray({ + width: dims[0], + height: dims[1], + depth: dims[2], + dataArray: scalars, + preferSizeOverAccuracy: model.renderable.getPreferSizeOverAccuracy(), + }); model._openGLRenderWindow.setGraphicsResourceForObject( scalars, model.volumeTexture, @@ -236,6 +240,21 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) { model.volumeTexture = cachedScalarsEntry.oglObject; } + if (hasUpdatedExtents) { + // If hasUpdatedExtents, then the texture is partially updated. + // clear the array to acknowledge the update. + property.setUpdatedExtents([]); + + const dims = image.getDimensions(); + model.volumeTexture.create3DFilterableFromDataArray({ + width: dims[0], + height: dims[1], + depth: dims[2], + dataArray: scalars, + updatedExtents, + }); + } + // Rebuild the color texture if needed const numComp = scalars.getNumberOfComponents(); const ppty = actor.getProperty(); @@ -262,7 +281,10 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) { !cachedColorEntry?.oglObject?.getHandle() || cachedColorEntry?.hash !== colorTextureHash; if (reBuildColorTexture) { - const cWidth = 1024; + let cWidth = model.renderable.getColorTextureWidth(); + if (cWidth <= 0) { + cWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE); + } const cSize = cWidth * textureHeight * 3; const cTable = new Uint8ClampedArray(cSize); model.colorTexture = vtkOpenGLTexture.newInstance(); @@ -286,13 +308,13 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) { } } model.colorTexture.resetFormatAndType(); - model.colorTexture.create2DFromRaw( - cWidth, - textureHeight, - 3, - VtkDataTypes.UNSIGNED_CHAR, - cTable - ); + model.colorTexture.create2DFromRaw({ + width: cWidth, + height: textureHeight, + numComps: 3, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: cTable, + }); } else { for (let i = 0; i < cWidth * 3; ++i) { cTable[i] = (255.0 * i) / ((cWidth - 1) * 3); @@ -300,13 +322,13 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) { cTable[i + 2] = (255.0 * i) / ((cWidth - 1) * 3); } model.colorTexture.resetFormatAndType(); - model.colorTexture.create2DFromRaw( - cWidth, - 1, - 3, - VtkDataTypes.UNSIGNED_CHAR, - cTable - ); + model.colorTexture.create2DFromRaw({ + width: cWidth, + height: 1, + numComps: 3, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: cTable, + }); } if (firstColorTransferFunc) { @@ -350,7 +372,10 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) { !cachedPwfEntry?.oglObject?.getHandle() || cachedPwfEntry?.hash !== pwfTextureHash; if (reBuildPwf) { - const pwfWidth = 1024; + let pwfWidth = model.renderable.getOpacityTextureWidth(); + if (pwfWidth <= 0) { + pwfWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE); + } const pwfSize = pwfWidth * textureHeight; const pwfTable = new Uint8ClampedArray(pwfSize); model.pwfTexture = vtkOpenGLTexture.newInstance(); @@ -381,24 +406,24 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) { } } model.pwfTexture.resetFormatAndType(); - model.pwfTexture.create2DFromRaw( - pwfWidth, - textureHeight, - 1, - VtkDataTypes.FLOAT, - pwfFloatTable - ); + model.pwfTexture.create2DFromRaw({ + width: pwfWidth, + height: textureHeight, + numComps: 1, + dataType: VtkDataTypes.FLOAT, + data: pwfFloatTable, + }); } else { // default is opaque pwfTable.fill(255.0); model.pwfTexture.resetFormatAndType(); - model.pwfTexture.create2DFromRaw( - pwfWidth, - 1, - 1, - VtkDataTypes.UNSIGNED_CHAR, - pwfTable - ); + model.pwfTexture.create2DFromRaw({ + width: pwfWidth, + height: 1, + numComps: 1, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: pwfTable, + }); } if (firstPwFunc) { model._openGLRenderWindow.setGraphicsResourceForObject( diff --git a/Sources/Rendering/OpenGL/ImageMapper/index.js b/Sources/Rendering/OpenGL/ImageMapper/index.js index 86ca93b9ac0..fe54f387cf4 100644 --- a/Sources/Rendering/OpenGL/ImageMapper/index.js +++ b/Sources/Rendering/OpenGL/ImageMapper/index.js @@ -967,7 +967,10 @@ function vtkOpenGLImageMapper(publicAPI, model) { resizable: true, }); model.colorTexture.setOpenGLRenderWindow(model._openGLRenderWindow); - const cWidth = 1024; + let cWidth = model.renderable.getColorTextureWidth(); + if (cWidth <= 0) { + cWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE); + } const cSize = cWidth * textureHeight * 3; const cTable = new Uint8ClampedArray(cSize); // set interpolation on the texture based on property setting @@ -998,26 +1001,26 @@ function vtkOpenGLImageMapper(publicAPI, model) { } } model.colorTexture.resetFormatAndType(); - model.colorTexture.create2DFromRaw( - cWidth, - textureHeight, - 3, - VtkDataTypes.UNSIGNED_CHAR, - cTable - ); + model.colorTexture.create2DFromRaw({ + width: cWidth, + height: textureHeight, + numComps: 3, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: cTable, + }); } else { for (let i = 0; i < cWidth * 3; ++i) { cTable[i] = (255.0 * i) / ((cWidth - 1) * 3); cTable[i + 1] = (255.0 * i) / ((cWidth - 1) * 3); cTable[i + 2] = (255.0 * i) / ((cWidth - 1) * 3); } - model.colorTexture.create2DFromRaw( - cWidth, - 1, - 3, - VtkDataTypes.UNSIGNED_CHAR, - cTable - ); + model.colorTexture.create2DFromRaw({ + width: cWidth, + height: 1, + numComps: 3, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: cTable, + }); } if (firstColorTransferFunc) { @@ -1061,7 +1064,10 @@ function vtkOpenGLImageMapper(publicAPI, model) { const reBuildPwf = !pwfTex?.oglObject?.getHandle() || pwfTex?.hash !== pwfunToString; if (reBuildPwf) { - const pwfWidth = 1024; + let pwfWidth = model.renderable.getOpacityTextureWidth(); + if (pwfWidth <= 0) { + pwfWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE); + } const pwfSize = pwfWidth * textureHeight; const pwfTable = new Uint8ClampedArray(pwfSize); model.pwfTexture = vtkOpenGLTexture.newInstance({ @@ -1103,23 +1109,23 @@ function vtkOpenGLImageMapper(publicAPI, model) { } } model.pwfTexture.resetFormatAndType(); - model.pwfTexture.create2DFromRaw( - pwfWidth, - textureHeight, - 1, - VtkDataTypes.FLOAT, - pwfFloatTable - ); + model.pwfTexture.create2DFromRaw({ + width: pwfWidth, + height: textureHeight, + numComps: 1, + dataType: VtkDataTypes.FLOAT, + data: pwfFloatTable, + }); } else { // default is opaque pwfTable.fill(255.0); - model.pwfTexture.create2DFromRaw( - pwfWidth, - 1, - 1, - VtkDataTypes.UNSIGNED_CHAR, - pwfTable - ); + model.pwfTexture.create2DFromRaw({ + width: pwfWidth, + height: 1, + numComps: 1, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: pwfTable, + }); } if (firstPwFunc) { @@ -1320,17 +1326,34 @@ function vtkOpenGLImageMapper(publicAPI, model) { vtkErrorMacro('Reformat slicing not yet supported.'); } + /** + * + * Fetch the ranges of the source volume, `imgScalars`, and use them when + * creating the texture. Whilst the pre-calculated ranges may not be + * strictly correct for the slice, it is guaranteed to be within the + * source volume's range. + * + * There is a significant performance improvement by pre-setting the range + * of the scalars array particularly when scrolling through the source + * volume as there is no need to calculate the range of the slice scalar. + * + * @type{ import("../../../interfaces").vtkRange[] } + */ + const ranges = imgScalars.getRanges(); + // Don't share this resource as `scalars` is created in this function // so it is impossible to share model.openGLTexture.resetFormatAndType(); - model.openGLTexture.create2DFilterableFromRaw( - dims[0], - dims[1], - numComp, - imgScalars.getDataType(), - scalars, - model.renderable.getPreferSizeOverAccuracy?.() - ); + model.openGLTexture.create2DFilterableFromRaw({ + width: dims[0], + height: dims[1], + numComps: numComp, + dataType: imgScalars.getDataType(), + data: scalars, + preferSizeOverAccuracy: + !!model.renderable.getPreferSizeOverAccuracy?.(), + ranges, + }); model.openGLTexture.activate(); model.openGLTexture.sendParameters(); model.openGLTexture.deactivate(); @@ -1387,7 +1410,10 @@ function vtkOpenGLImageMapper(publicAPI, model) { const reBuildL = !lTex?.oglObject?.getHandle() || lTex?.hash !== toString; if (reBuildL) { - const lWidth = 1024; + let lWidth = model.renderable.getLabelOutlineTextureWidth(); + if (lWidth <= 0) { + lWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE); + } const lHeight = 1; const lSize = lWidth * lHeight; const lTable = new Uint8Array(lSize); @@ -1414,13 +1440,13 @@ function vtkOpenGLImageMapper(publicAPI, model) { model.labelOutlineThicknessTexture.setMagnificationFilter(Filter.NEAREST); // Create a 2D texture (acting as 1D) from the raw data - model.labelOutlineThicknessTexture.create2DFromRaw( - lWidth, - lHeight, - 1, - VtkDataTypes.UNSIGNED_CHAR, - lTable - ); + model.labelOutlineThicknessTexture.create2DFromRaw({ + width: lWidth, + height: lHeight, + numComps: 1, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: lTable, + }); if (labelOutlineThicknessArray) { model._openGLRenderWindow.setGraphicsResourceForObject( diff --git a/Sources/Rendering/OpenGL/ImageResliceMapper/index.js b/Sources/Rendering/OpenGL/ImageResliceMapper/index.js index eff6fcba449..e8feede12c0 100644 --- a/Sources/Rendering/OpenGL/ImageResliceMapper/index.js +++ b/Sources/Rendering/OpenGL/ImageResliceMapper/index.js @@ -293,6 +293,8 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { !model.pwfTexture?.getHandle(); publicAPI.buildBufferObjects = (ren, actor) => { + const actorProperties = actor.getProperties(); + model.currentValidInputs.forEach(({ imageData }, component) => { // rebuild the scalarTexture if the data has changed const scalars = imageData.getPointData().getScalars(); @@ -301,7 +303,12 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { const scalarsHash = getImageDataHash(imageData, scalars); const reBuildTex = !tex?.oglObject?.getHandle() || tex?.hash !== scalarsHash; - if (reBuildTex) { + + const actorProperty = actorProperties[component]; + const updatedExtents = actorProperty.getUpdatedExtents(); + const hasUpdatedExtents = !!updatedExtents.length; + + if (reBuildTex && !hasUpdatedExtents) { const newScalarTexture = vtkOpenGLTexture.newInstance(); newScalarTexture.setOpenGLRenderWindow(model._openGLRenderWindow); // Build the textures @@ -311,12 +318,12 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { model.context.getExtension('EXT_texture_norm16') ); newScalarTexture.resetFormatAndType(); - newScalarTexture.create3DFilterableFromDataArray( - dims[0], - dims[1], - dims[2], - scalars - ); + newScalarTexture.create3DFilterableFromDataArray({ + width: dims[0], + height: dims[1], + depth: dims[2], + dataArray: scalars, + }); model._openGLRenderWindow.setGraphicsResourceForObject( scalars, newScalarTexture, @@ -326,6 +333,22 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { } else { model.scalarTextures[component] = tex.oglObject; } + + if (hasUpdatedExtents) { + // If hasUpdatedExtents, then the texture is partially updated. + // clear the array to acknowledge the update. + actorProperty.setUpdatedExtents([]); + + const dims = imageData.getDimensions(); + model.scalarTextures[component].create3DFilterableFromDataArray({ + width: dims[0], + height: dims[1], + depth: dims[2], + dataArray: scalars, + updatedExtents, + }); + } + replaceGraphicsResource( model._openGLRenderWindow, model._scalarTexturesCore[component], @@ -335,7 +358,6 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { }); const firstValidInput = model.currentValidInputs[0]; - const actorProperties = actor.getProperties(); const firstActorProperty = actorProperties[firstValidInput.inputIndex]; const iComps = firstActorProperty.getIndependentComponents(); const numIComps = iComps ? model.numberOfComponents : 1; @@ -359,7 +381,10 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { const reBuildC = !cTex?.oglObject?.getHandle() || cTex?.hash !== colorFuncHash; if (reBuildC) { - const cWidth = 1024; + let cWidth = model.renderable.getColorTextureWidth(); + if (cWidth <= 0) { + cWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE); + } const cSize = cWidth * textureHeight * 3; const cTable = new Uint8ClampedArray(cSize); const newColorTexture = vtkOpenGLTexture.newInstance(); @@ -383,13 +408,13 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { } } newColorTexture.resetFormatAndType(); - newColorTexture.create2DFromRaw( - cWidth, - textureHeight, - 3, - VtkDataTypes.UNSIGNED_CHAR, - cTable - ); + newColorTexture.create2DFromRaw({ + width: cWidth, + height: textureHeight, + numComps: 3, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: cTable, + }); } else { for (let column = 0; column < cWidth * 3; ++column) { const opacity = (255.0 * column) / ((cWidth - 1) * 3); @@ -401,13 +426,13 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { } } newColorTexture.resetFormatAndType(); - newColorTexture.create2DFromRaw( - cWidth, - 1, - 3, - VtkDataTypes.UNSIGNED_CHAR, - cTable - ); + newColorTexture.create2DFromRaw({ + width: cWidth, + height: 1, + numComps: 3, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: cTable, + }); } if (firstColorTransferFunc) { @@ -446,7 +471,10 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { const reBuildPwf = !pwfTex?.oglObject?.getHandle() || pwfTex?.hash !== opacityFuncHash; if (reBuildPwf) { - const pwfWidth = 1024; + let pwfWidth = model.renderable.getOpacityTextureWidth(); + if (pwfWidth <= 0) { + pwfWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE); + } const pwfSize = pwfWidth * textureHeight; const pwfTable = new Uint8ClampedArray(pwfSize); const newOpacityTexture = vtkOpenGLTexture.newInstance(); @@ -477,24 +505,24 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) { } } newOpacityTexture.resetFormatAndType(); - newOpacityTexture.create2DFromRaw( - pwfWidth, - textureHeight, - 1, - VtkDataTypes.FLOAT, - pwfFloatTable - ); + newOpacityTexture.create2DFromRaw({ + width: pwfWidth, + height: textureHeight, + numComps: 1, + dataType: VtkDataTypes.FLOAT, + data: pwfFloatTable, + }); } else { // default is opaque pwfTable.fill(255.0); newOpacityTexture.resetFormatAndType(); - newOpacityTexture.create2DFromRaw( - pwfWidth, - textureHeight, - 1, - VtkDataTypes.UNSIGNED_CHAR, - pwfTable - ); + newOpacityTexture.create2DFromRaw({ + width: pwfWidth, + height: textureHeight, + numComps: 1, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: pwfTable, + }); } if (firstPwFunc) { model._openGLRenderWindow.setGraphicsResourceForObject( diff --git a/Sources/Rendering/OpenGL/OrderIndependentTranslucentPass/index.js b/Sources/Rendering/OpenGL/OrderIndependentTranslucentPass/index.js index e4384395358..a64560b94a3 100644 --- a/Sources/Rendering/OpenGL/OrderIndependentTranslucentPass/index.js +++ b/Sources/Rendering/OpenGL/OrderIndependentTranslucentPass/index.js @@ -103,35 +103,35 @@ function vtkOpenGLOrderIndependentTranslucentPass(publicAPI, model) { model.translucentRGBATexture.setFormat(gl.RGBA); model.translucentRGBATexture.setOpenGLDataType(gl.HALF_FLOAT); model.translucentRGBATexture.setOpenGLRenderWindow(viewNode); - model.translucentRGBATexture.create2DFromRaw( - size[0], - size[1], - 4, - 'Float32Array', - null - ); + model.translucentRGBATexture.create2DFromRaw({ + width: size[0], + height: size[1], + numComps: 4, + dataType: 'Float32Array', + data: null, + }); model.translucentRTexture = vtkOpenGLTexture.newInstance(); model.translucentRTexture.setInternalFormat(gl.R16F); model.translucentRTexture.setFormat(gl.RED); model.translucentRTexture.setOpenGLDataType(gl.HALF_FLOAT); model.translucentRTexture.setOpenGLRenderWindow(viewNode); - model.translucentRTexture.create2DFromRaw( - size[0], - size[1], - 1, - 'Float32Array', - null - ); + model.translucentRTexture.create2DFromRaw({ + width: size[0], + height: size[1], + numComps: 1, + dataType: 'Float32Array', + data: null, + }); model.translucentZTexture = vtkOpenGLTexture.newInstance(); model.translucentZTexture.setOpenGLRenderWindow(viewNode); - model.translucentZTexture.createDepthFromRaw( - size[0], - size[1], - 'Float32Array', - null - ); + model.translucentZTexture.createDepthFromRaw({ + width: size[0], + height: size[1], + dataType: 'Float32Array', + data: null, + }); model.framebuffer.setColorBuffer(model.translucentRGBATexture, 0); model.framebuffer.setColorBuffer(model.translucentRTexture, 1); diff --git a/Sources/Rendering/OpenGL/PolyDataMapper/index.js b/Sources/Rendering/OpenGL/PolyDataMapper/index.js index d4dbd29ae1a..a59f7786e63 100755 --- a/Sources/Rendering/OpenGL/PolyDataMapper/index.js +++ b/Sources/Rendering/OpenGL/PolyDataMapper/index.js @@ -1843,13 +1843,13 @@ function vtkOpenGLPolyDataMapper(publicAPI, model) { const input = model.renderable.getColorTextureMap(); const ext = input.getExtent(); const inScalars = input.getPointData().getScalars(); - tex.create2DFromRaw( - ext[1] - ext[0] + 1, - ext[3] - ext[2] + 1, - inScalars.getNumberOfComponents(), - inScalars.getDataType(), - inScalars.getData() - ); + tex.create2DFromRaw({ + width: ext[1] - ext[0] + 1, + height: ext[3] - ext[2] + 1, + numComps: inScalars.getNumberOfComponents(), + dataType: inScalars.getDataType(), + data: inScalars.getData(), + }); tex.activate(); tex.sendParameters(); tex.deactivate(); diff --git a/Sources/Rendering/OpenGL/Renderer/index.js b/Sources/Rendering/OpenGL/Renderer/index.js index aef9b98fe9a..42c942abe67 100644 --- a/Sources/Rendering/OpenGL/Renderer/index.js +++ b/Sources/Rendering/OpenGL/Renderer/index.js @@ -24,7 +24,10 @@ function vtkOpenGLRenderer(publicAPI, model) { publicAPI.updateLights(); publicAPI.prepareNodes(); publicAPI.addMissingNode(model.renderable.getActiveCamera()); - publicAPI.addMissingNodes(model.renderable.getViewPropsWithNestedProps()); + publicAPI.addMissingNodes( + model.renderable.getViewPropsWithNestedProps(), + true + ); publicAPI.removeUnusedNodes(); } }; diff --git a/Sources/Rendering/OpenGL/SurfaceLIC/LineIntegralConvolution2D/pingpong.js b/Sources/Rendering/OpenGL/SurfaceLIC/LineIntegralConvolution2D/pingpong.js index 782cfa7b07f..2bfef328aac 100644 --- a/Sources/Rendering/OpenGL/SurfaceLIC/LineIntegralConvolution2D/pingpong.js +++ b/Sources/Rendering/OpenGL/SurfaceLIC/LineIntegralConvolution2D/pingpong.js @@ -66,7 +66,13 @@ function allocateBuffer(openGLRenderWindow, [width, height], filter, wrapping) { }); texture.setOpenGLRenderWindow(openGLRenderWindow); texture.setInternalFormat(gl.RGBA32F); - texture.create2DFromRaw(width, height, 4, 'Float32Array', null); + texture.create2DFromRaw({ + width, + height, + numComps: 4, + dataType: 'Float32Array', + data: null, + }); texture.activate(); texture.sendParameters(); texture.deactivate(); diff --git a/Sources/Rendering/OpenGL/SurfaceLIC/SurfaceLICInterface/index.js b/Sources/Rendering/OpenGL/SurfaceLIC/SurfaceLICInterface/index.js index 012cc269377..fa78ed12957 100644 --- a/Sources/Rendering/OpenGL/SurfaceLIC/SurfaceLICInterface/index.js +++ b/Sources/Rendering/OpenGL/SurfaceLIC/SurfaceLICInterface/index.js @@ -268,7 +268,13 @@ function vtkOpenGLSurfaceLICInterface(publicAPI, model) { autoParameters: false, }); texture.setOpenGLRenderWindow(model._openGLRenderWindow); - texture.create2DFromRaw(length, length, 4, 'Float32Array', values); + texture.create2DFromRaw({ + width: length, + height: length, + numComps: 4, + dataType: 'Float32Array', + data: values, + }); texture.activate(); texture.sendParameters(); texture.deactivate(); @@ -328,7 +334,13 @@ function vtkOpenGLSurfaceLICInterface(publicAPI, model) { }); texture.setOpenGLRenderWindow(openGLRenderWindow); texture.setInternalFormat(gl.RGBA32F); - texture.create2DFromRaw(...model.size, 4, 'Float32Array', null); + texture.create2DFromRaw({ + width: model.size[0], + height: model.size[1], + numComps: 4, + dataType: 'Float32Array', + data: null, + }); texture.activate(); texture.sendParameters(); texture.deactivate(); @@ -343,7 +355,12 @@ function vtkOpenGLSurfaceLICInterface(publicAPI, model) { autoParameters: false, }); texture.setOpenGLRenderWindow(openGLRenderWindow); - texture.createDepthFromRaw(...model.size, 'Float32Array', null); + texture.createDepthFromRaw({ + width: model.size[0], + height: model.size[1], + dataType: 'Float32Array', + data: null, + }); texture.activate(); texture.sendParameters(); texture.deactivate(); diff --git a/Sources/Rendering/OpenGL/Texture/index.d.ts b/Sources/Rendering/OpenGL/Texture/index.d.ts index 8cfcaaeffd5..9dbda5f55c7 100644 --- a/Sources/Rendering/OpenGL/Texture/index.d.ts +++ b/Sources/Rendering/OpenGL/Texture/index.d.ts @@ -1,9 +1,9 @@ import { Wrap, Filter } from './Constants'; import vtkOpenGLRenderWindow from '../RenderWindow'; -import { Nullable } from '../../../types'; +import { Extent, Nullable } from '../../../types'; import { VtkDataTypes } from '../../../Common/Core/DataArray'; import { vtkViewNode } from '../../../Rendering/SceneGraph/ViewNode'; -import { vtkObject } from '../../../interfaces'; +import { vtkObject, vtkRange } from '../../../interfaces'; /** * Initial values for creating a new instance of vtkOpenGLTexture. @@ -199,17 +199,24 @@ export interface vtkOpenGLTexture extends vtkViewNode { * @param numComps The number of components in the texture. * @param dataType The data type of the texture. * @param data The raw data for the texture. - * @param flip Whether to flip the texture vertically. + * @param flip Whether to flip the texture vertically. Defaults to false. * @returns {boolean} True if the texture was successfully created, false otherwise. */ - create2DFromRaw( - width: number, - height: number, - numComps: number, - dataType: VtkDataTypes, - data: any, - flip: boolean - ): boolean; + create2DFromRaw({ + width, + height, + numComps, + dataType, + data, + flip, + }: { + width: number; + height: number; + numComps: number; + dataType: VtkDataTypes; + data: any; + flip?: boolean; + }): boolean; /** * Creates a cube texture from raw data. @@ -218,17 +225,21 @@ export interface vtkOpenGLTexture extends vtkViewNode { * @param numComps The number of components in the texture. * @param dataType The data type of the texture. * @param data The raw data for the texture. - * @param flip Whether to flip the texture vertically. * @returns {boolean} True if the cube texture was successfully created, false otherwise. */ - createCubeFromRaw( - width: number, - height: number, - numComps: number, - dataType: VtkDataTypes, - data: any, - flip: boolean - ): boolean; + createCubeFromRaw({ + width, + height, + numComps, + dataType, + data, + }: { + width: number; + height: number; + numComps: number; + dataType: VtkDataTypes; + data: any; + }): boolean; /** * Creates a 2D texture from an image. @@ -244,17 +255,27 @@ export interface vtkOpenGLTexture extends vtkViewNode { * @param numComps The number of components in the texture. * @param dataType The data type of the texture. * @param data The raw data for the texture. - * @param preferSizeOverAccuracy Whether to prefer texture size over accuracy. + * @param [preferSizeOverAccuracy=false] Whether to prefer texture size over accuracy. Defaults to false. + * @param [ranges] The precomputed ranges of the data (optional). Provided to prevent computation of the data ranges. * @returns {boolean} True if the texture was successfully created, false otherwise. */ - create2DFilterableFromRaw( - width: number, - height: number, - numComps: number, - dataType: VtkDataTypes, - data: any, - preferSizeOverAccuracy: boolean - ): boolean; + create2DFilterableFromRaw({ + width, + height, + numComps, + dataType, + data, + preferSizeOverAccuracy, + ranges, + }: { + width: number; + height: number; + numComps: number; + dataType: VtkDataTypes; + data: any; + preferSizeOverAccuracy?: boolean; + ranges?: vtkRange[]; + }): boolean; /** * Creates a 2D filterable texture from a data array, with a preference for size over accuracy if necessary. @@ -264,69 +285,117 @@ export interface vtkOpenGLTexture extends vtkViewNode { * @param preferSizeOverAccuracy Whether to prefer texture size over accuracy. * @returns {boolean} True if the texture was successfully created, false otherwise. */ - create2DFilterableFromDataArray( - width: number, - height: number, - dataArray: any, - preferSizeOverAccuracy: boolean - ): boolean; + create2DFilterableFromDataArray({ + width, + height, + dataArray, + preferSizeOverAccuracy, + }: { + width: number; + height: number; + dataArray: any; + preferSizeOverAccuracy?: boolean; + }): boolean; /** * Creates a 3D texture from raw data. + * + * updatedExtents is currently incompatible with webgl1, since there's no extent scaling. + * * @param width The width of the texture. * @param height The height of the texture. * @param depth The depth of the texture. * @param numComps The number of components in the texture. * @param dataType The data type of the texture. * @param data The raw data for the texture. + * @param updatedExtents Only update the specified extents (default: []) * @returns {boolean} True if the texture was successfully created, false otherwise. */ - create3DFromRaw( - width: number, - height: number, - depth: number, - numComps: number, - dataType: VtkDataTypes, - data: any - ): boolean; + create3DFromRaw({ + width, + height, + depth, + numComps, + dataType, + data, + updatedExtents, + }: { + width: number; + height: number; + depth: number; + numComps: number; + dataType: VtkDataTypes; + data: any; + updatedExtents?: Extent[]; + }): boolean; /** * Creates a 3D filterable texture from raw data, with a preference for size over accuracy if necessary. + * + * updatedExtents is currently incompatible with webgl1, since there's no extent scaling. + * * @param width The width of the texture. * @param height The height of the texture. * @param depth The depth of the texture. * @param numComps The number of components in the texture. * @param dataType The data type of the texture. - * @param values The raw data for the texture. + * @param data The raw data for the texture. * @param preferSizeOverAccuracy Whether to prefer texture size over accuracy. - * @returns {boolean} True if the texture was successfully created, false otherwise. - */ - create3DFilterableFromRaw( - width: number, - height: number, - depth: number, - numComps: number, - dataType: VtkDataTypes, - values: any, - preferSizeOverAccuracy: boolean - ): boolean; + * @param [ranges] The precomputed ranges of the data (optional). Provided to + * @param updatedExtents Only update the specified extents (default: []) + * prevent computation of the data ranges. + * @returns {boolean} True if the texture was successfully created, false + * otherwise. + */ + create3DFilterableFromRaw({ + width, + height, + depth, + numComps, + dataType, + data, + preferSizeOverAccuracy, + ranges, + updatedExtents, + }: { + width: number; + height: number; + depth: number; + numComps: number; + dataType: VtkDataTypes; + data: any; + preferSizeOverAccuracy?: boolean; + ranges?: vtkRange[]; + updatedExtents?: Extent[]; + }): boolean; /** * Creates a 3D filterable texture from a data array, with a preference for size over accuracy if necessary. + * + * updatedExtents is currently incompatible with webgl1, since there's no extent scaling. + * * @param width The width of the texture. * @param height The height of the texture. * @param depth The depth of the texture. * @param dataArray The data array to use for the texture. * @param preferSizeOverAccuracy Whether to prefer texture size over accuracy. + * @param updatedExtents Only update the specified extents (default: []) * @returns {boolean} True if the texture was successfully created, false otherwise. */ - create3DFilterableFromDataArray( - width: number, - height: number, - depth: number, - dataArray: any, - preferSizeOverAccuracy: boolean - ): boolean; + create3DFilterableFromDataArray({ + width, + height, + depth, + dataArray, + preferSizeOverAccuracy, + }: { + width: number; + height: number; + depth: number; + dataArray: any; + preferSizeOverAccuracy?: boolean; + updatedExtents?: Extent[]; + }): boolean; /** * Sets the OpenGL render window in which the texture will be used. diff --git a/Sources/Rendering/OpenGL/Texture/index.js b/Sources/Rendering/OpenGL/Texture/index.js index ce0b7d2bda1..b1e1ca89b00 100644 --- a/Sources/Rendering/OpenGL/Texture/index.js +++ b/Sources/Rendering/OpenGL/Texture/index.js @@ -1,3 +1,4 @@ +import deepEqual from 'fast-deep-equal'; import Constants from 'vtk.js/Sources/Rendering/OpenGL/Texture/Constants'; import HalfFloat from 'vtk.js/Sources/Common/Core/HalfFloat'; import * as macro from 'vtk.js/Sources/macros'; @@ -11,7 +12,7 @@ import supportsNorm16Linear from './supportsNorm16Linear'; const { Wrap, Filter } = Constants; const { VtkDataTypes } = vtkDataArray; -const { vtkDebugMacro, vtkErrorMacro, vtkWarningMacro } = macro; +const { vtkDebugMacro, vtkErrorMacro, vtkWarningMacro, requiredParam } = macro; const { toHalf } = HalfFloat; // ---------------------------------------------------------------------------- @@ -21,6 +22,17 @@ const { toHalf } = HalfFloat; 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) { @@ -80,14 +92,14 @@ function vtkOpenGLTexture(publicAPI, model) { publicAPI.setMinificationFilter(Filter.LINEAR_MIPMAP_LINEAR); } const canvas = model.renderable.getCanvas(); - publicAPI.create2DFromRaw( - canvas.width, - canvas.height, - 4, - VtkDataTypes.UNSIGNED_CHAR, - canvas, - true - ); + 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(); @@ -99,14 +111,14 @@ function vtkOpenGLTexture(publicAPI, model) { model.generateMipmap = true; publicAPI.setMinificationFilter(Filter.LINEAR_MIPMAP_LINEAR); } - publicAPI.create2DFromRaw( - jsid.width, - jsid.height, - 4, - VtkDataTypes.UNSIGNED_CHAR, - jsid.data, - true - ); + 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(); @@ -136,21 +148,21 @@ function vtkOpenGLTexture(publicAPI, model) { 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 - ); + publicAPI.createCubeFromRaw({ + width: ext[1] - ext[0] + 1, + height: ext[3] - ext[2] + 1, + numComps: inScalars.getNumberOfComponents(), + dataType: inScalars.getDataType(), + data, + }); } else { - publicAPI.create2DFromRaw( - ext[1] - ext[0] + 1, - ext[3] - ext[2] + 1, - inScalars.getNumberOfComponents(), - inScalars.getDataType(), - inScalars.getData() - ); + 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(); @@ -181,6 +193,7 @@ function vtkOpenGLTexture(publicAPI, model) { if (model.context && model.handle) { model.context.deleteTexture(model.handle); } + model._prevTexParams = null; model.handle = 0; model.numberOfDimensions = 0; model.target = 0; @@ -265,6 +278,7 @@ function vtkOpenGLTexture(publicAPI, model) { rwin.activateTexture(publicAPI); rwin.deactivateTexture(publicAPI); model.context.deleteTexture(model.handle); + model._prevTexParams = null; model.handle = 0; model.numberOfDimensions = 0; model.target = 0; @@ -473,6 +487,7 @@ function vtkOpenGLTexture(publicAPI, model) { //---------------------------------------------------------------------------- publicAPI.resetFormatAndType = () => { + model._prevTexParams = null; model.format = 0; model.internalFormat = 0; model._forceInternalFormat = false; @@ -627,6 +642,95 @@ function vtkOpenGLTexture(publicAPI, model) { } }; + //---------------------------------------------------------------------------- + + /** + * 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. * @@ -636,15 +740,23 @@ function vtkOpenGLTexture(publicAPI, model) { * @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} 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) => { + 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 ( @@ -653,11 +765,15 @@ function vtkOpenGLTexture(publicAPI, model) { ) { for (let idx = 0; idx < data.length; idx++) { if (data[idx]) { - const dataArrayToCopy = - data[idx].length > pixCount - ? data[idx].subarray(0, pixCount) - : data[idx]; - pixData.push(new Float32Array(dataArrayToCopy)); + 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); } @@ -672,11 +788,15 @@ function vtkOpenGLTexture(publicAPI, model) { ) { for (let idx = 0; idx < data.length; idx++) { if (data[idx]) { - const dataArrayToCopy = - data[idx].length > pixCount - ? data[idx].subarray(0, pixCount) - : data[idx]; - pixData.push(new Uint8Array(dataArrayToCopy)); + 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); } @@ -697,9 +817,14 @@ function vtkOpenGLTexture(publicAPI, model) { if (halfFloat) { for (let idx = 0; idx < data.length; idx++) { if (data[idx]) { - const newArray = new Uint16Array(pixCount); - const src = data[idx]; - for (let i = 0; i < pixCount; i++) { + 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); @@ -712,7 +837,11 @@ function vtkOpenGLTexture(publicAPI, model) { // The output has to be filled if (pixData.length === 0) { for (let i = 0; i < data.length; i++) { - pixData.push(data[i]); + pixData.push( + onlyUpdateExtents && data[i] + ? readExtents(data[i], imageExtents) + : data[i] + ); } } @@ -850,14 +979,15 @@ function vtkOpenGLTexture(publicAPI, model) { } //---------------------------------------------------------------------------- - publicAPI.create2DFromRaw = ( - width, - height, - numComps, - dataType, - data, - flip = 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); @@ -946,7 +1076,13 @@ function vtkOpenGLTexture(publicAPI, model) { }; //---------------------------------------------------------------------------- - publicAPI.createCubeFromRaw = (width, height, numComps, dataType, data) => { + 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); @@ -1073,7 +1209,12 @@ function vtkOpenGLTexture(publicAPI, model) { }; //---------------------------------------------------------------------------- - publicAPI.createDepthFromRaw = (width, height, dataType, data) => { + 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; @@ -1362,37 +1503,39 @@ function vtkOpenGLTexture(publicAPI, model) { }; } - publicAPI.create2DFilterableFromRaw = ( - width, - height, - numberOfComponents, - dataType, - values, - preferSizeOverAccuracy = false - ) => - publicAPI.create2DFilterableFromDataArray( + 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, - vtkDataArray.newInstance({ - numberOfComponents, + dataArray: vtkDataArray.newInstance({ + numComps, dataType, - values, + values: data, + ranges, }), - preferSizeOverAccuracy - ); - - publicAPI.create2DFilterableFromDataArray = ( - width, - height, - dataArray, - preferSizeOverAccuracy = false - ) => { + 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.create2DFromRaw({ width, height, numComps, dataType, data }); }; publicAPI.updateVolumeInfoForGL = (dataType, numComps) => { @@ -1455,14 +1598,15 @@ function vtkOpenGLTexture(publicAPI, model) { }; //---------------------------------------------------------------------------- - publicAPI.create3DFromRaw = ( - width, - height, - depth, - numComps, - dataType, - data - ) => { + publicAPI.create3DFromRaw = ({ + width = requiredParam('width'), + height = requiredParam('height'), + depth = requiredParam('depth'), + numComps = requiredParam('numComps'), + dataType = requiredParam('dataType'), + data = requiredParam('data'), + updatedExtents = [], + } = {}) => { let dataTypeToUse = dataType; let dataToUse = data; @@ -1512,13 +1656,22 @@ function vtkOpenGLTexture(publicAPI, model) { model._openGLRenderWindow.activateTexture(publicAPI); publicAPI.createTexture(); publicAPI.bind(); + + const hasUpdatedExtents = updatedExtents.length > 0; + + // It's possible for the texture parameters to change while + // streaming, so check for such a change. + const rebuildEntireTexture = + !hasUpdatedExtents || !deepEqual(model._prevTexParams, getTexParams()); + // Create an array of texture with one texture const dataArray = [dataToUse]; const is3DArray = true; const pixData = publicAPI.updateArrayDataTypeForGL( dataTypeToUse, dataArray, - is3DArray + is3DArray, + rebuildEntireTexture ? [] : updatedExtents ); const scaledData = scaleTextureToHighestPowerOfTwo(pixData); @@ -1526,45 +1679,75 @@ function vtkOpenGLTexture(publicAPI, model) { // model.context.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); model.context.pixelStorei(model.context.UNPACK_ALIGNMENT, 1); - // openGLDataType - - if (useTexStorage(dataTypeToUse)) { - model.context.texStorage3D( - model.target, - 1, - model.internalFormat, - model.width, - model.height, - model.depth - ); - if (scaledData[0] != null) { - model.context.texSubImage3D( + if (rebuildEntireTexture) { + if (useTexStorage(dataTypeToUse)) { + model.context.texStorage3D( + model.target, + 1, + model.internalFormat, + model.width, + model.height, + model.depth + ); + if (scaledData[0] != null) { + model.context.texSubImage3D( + model.target, + 0, + 0, + 0, + 0, + model.width, + model.height, + model.depth, + model.format, + model.openGLDataType, + scaledData[0] + ); + } + } else { + model.context.texImage3D( model.target, 0, - 0, - 0, - 0, + model.internalFormat, model.width, model.height, model.depth, + 0, model.format, model.openGLDataType, scaledData[0] ); } - } else { - model.context.texImage3D( - model.target, - 0, - model.internalFormat, - model.width, - model.height, - model.depth, - 0, - model.format, - model.openGLDataType, - scaledData[0] - ); + + model._prevTexParams = getTexParams(); + } else if (hasUpdatedExtents) { + const extentPixels = scaledData[0]; + let readOffset = 0; + for (let i = 0; i < updatedExtents.length; i++) { + const extent = updatedExtents[i]; + const extentSize = getExtentSize(extent); + const extentPixelCount = getExtentPixelCount(extent); + const textureData = new extentPixels.constructor( + extentPixels.buffer, + readOffset, + extentPixelCount + ); + readOffset += textureData.byteLength; + + model.context.texSubImage3D( + model.target, + 0, + extent[0], + extent[2], + extent[4], + extentSize[0], + extentSize[1], + extentSize[2], + model.format, + model.openGLDataType, + textureData + ); + } } if (model.generateMipmap) { @@ -1589,36 +1772,41 @@ function vtkOpenGLTexture(publicAPI, model) { //---------------------------------------------------------------------------- // This method simulates a 3D texture using 2D // Prefer create3DFilterableFromDataArray to enable caching of min and max values - publicAPI.create3DFilterableFromRaw = ( - width, - height, - depth, - numberOfComponents, - dataType, - values, - preferSizeOverAccuracy = false - ) => - publicAPI.create3DFilterableFromDataArray( + publicAPI.create3DFilterableFromRaw = ({ + width = requiredParam('width'), + height = requiredParam('height'), + depth = requiredParam('depth'), + numComps = requiredParam('numComps'), + dataType = requiredParam('dataType'), + data = requiredParam('data'), + preferSizeOverAccuracy = false, + ranges = undefined, + updatedExtents = [], + } = {}) => + publicAPI.create3DFilterableFromDataArray({ width, height, depth, - vtkDataArray.newInstance({ - numberOfComponents, + dataArray: vtkDataArray.newInstance({ + numComps, dataType, - values, + values: data, + ranges, }), - preferSizeOverAccuracy - ); + preferSizeOverAccuracy, + updatedExtents, + }); //---------------------------------------------------------------------------- // This method create a 3D texture from dimensions and a DataArray - publicAPI.create3DFilterableFromDataArray = ( - width, - height, - depth, - dataArray, - preferSizeOverAccuracy = false - ) => { + publicAPI.create3DFilterableFromDataArray = ({ + width = requiredParam('width'), + height = requiredParam('height'), + depth = requiredParam('depth'), + dataArray = requiredParam('dataArray'), + preferSizeOverAccuracy = false, + updatedExtents = [], + } = {}) => { const { numComps, dataType, data, scaleOffsets } = processDataArray( dataArray, preferSizeOverAccuracy @@ -1652,14 +1840,15 @@ function vtkOpenGLTexture(publicAPI, model) { // WebGL2 path, we have 3d textures etc if (model._openGLRenderWindow.getWebgl2()) { - return publicAPI.create3DFromRaw( + return publicAPI.create3DFromRaw({ width, height, depth, numComps, dataType, - data - ); + data, + updatedExtents, + }); } const numPixelsIn = width * height * depth; @@ -1879,6 +2068,7 @@ function vtkOpenGLTexture(publicAPI, model) { const DEFAULT_VALUES = { _openGLRenderWindow: null, _forceInternalFormat: false, + _prevTexParams: null, context: null, handle: 0, sendParametersTime: null, diff --git a/Sources/Rendering/OpenGL/VolumeMapper/index.js b/Sources/Rendering/OpenGL/VolumeMapper/index.js index dabbd7ce8d8..6fa94134267 100644 --- a/Sources/Rendering/OpenGL/VolumeMapper/index.js +++ b/Sources/Rendering/OpenGL/VolumeMapper/index.js @@ -1514,13 +1514,13 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { } model.jitterTexture.setMinificationFilter(Filter.NEAREST); model.jitterTexture.setMagnificationFilter(Filter.NEAREST); - model.jitterTexture.create2DFromRaw( - 32, - 32, - 1, - VtkDataTypes.FLOAT, - jitterArray - ); + model.jitterTexture.create2DFromRaw({ + width: 32, + height: 32, + numComps: 1, + dataType: VtkDataTypes.FLOAT, + data: jitterArray, + }); } const volumeProperties = actor.getProperties(); @@ -1549,7 +1549,10 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { if (reBuildOp) { const newOpacityTexture = vtkOpenGLTexture.newInstance(); newOpacityTexture.setOpenGLRenderWindow(model._openGLRenderWindow); - const oWidth = 1024; + let oWidth = model.renderable.getOpacityTextureWidth(); + if (oWidth <= 0) { + oWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE); + } const oSize = oWidth * 2 * numIComps; const ofTable = new Float32Array(oSize); const tmpTable = new Float32Array(oWidth); @@ -1583,25 +1586,25 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { (model.context.getExtension('OES_texture_float') && model.context.getExtension('OES_texture_float_linear')) ) { - newOpacityTexture.create2DFromRaw( - oWidth, - 2 * numIComps, - 1, - VtkDataTypes.FLOAT, - ofTable - ); + newOpacityTexture.create2DFromRaw({ + width: oWidth, + height: 2 * numIComps, + numComps: 1, + dataType: VtkDataTypes.FLOAT, + data: ofTable, + }); } else { const oTable = new Uint8ClampedArray(oSize); for (let i = 0; i < oSize; ++i) { oTable[i] = 255.0 * ofTable[i]; } - newOpacityTexture.create2DFromRaw( - oWidth, - 2 * numIComps, - 1, - VtkDataTypes.UNSIGNED_CHAR, - oTable - ); + newOpacityTexture.create2DFromRaw({ + width: oWidth, + height: 2 * numIComps, + numComps: 1, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: oTable, + }); } if (firstScalarOpacityFunc) { model._openGLRenderWindow.setGraphicsResourceForObject( @@ -1642,7 +1645,10 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { if (reBuildC) { const newColorTexture = vtkOpenGLTexture.newInstance(); newColorTexture.setOpenGLRenderWindow(model._openGLRenderWindow); - const cWidth = 1024; + let cWidth = model.renderable.getColorTextureWidth(); + if (cWidth <= 0) { + cWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE); + } const cSize = cWidth * 2 * numIComps * 3; const cTable = new Uint8ClampedArray(cSize); const tmpTable = new Float32Array(cWidth * 3); @@ -1661,13 +1667,13 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { newColorTexture.setMinificationFilter(Filter.LINEAR); newColorTexture.setMagnificationFilter(Filter.LINEAR); - newColorTexture.create2DFromRaw( - cWidth, - 2 * numIComps, - 3, - VtkDataTypes.UNSIGNED_CHAR, - cTable - ); + newColorTexture.create2DFromRaw({ + width: cWidth, + height: 2 * numIComps, + numComps: 3, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: cTable, + }); model._openGLRenderWindow.setGraphicsResourceForObject( firstColorTransferFunc, newColorTexture, @@ -1694,7 +1700,11 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { const scalarsHash = getImageDataHash(imageData, scalars); const reBuildTex = !tex?.oglObject?.getHandle() || tex?.hash !== scalarsHash; - if (reBuildTex) { + + const updatedExtents = volumeProperty.getUpdatedExtents(); + const hasUpdatedExtents = !!updatedExtents.length; + + if (reBuildTex && !hasUpdatedExtents) { const newScalarTexture = vtkOpenGLTexture.newInstance(); newScalarTexture.setOpenGLRenderWindow(model._openGLRenderWindow); // Build the textures @@ -1704,13 +1714,13 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { model.context.getExtension('EXT_texture_norm16') ); newScalarTexture.resetFormatAndType(); - newScalarTexture.create3DFilterableFromDataArray( - dims[0], - dims[1], - dims[2], - scalars, - volumeProperty.getPreferSizeOverAccuracy() - ); + newScalarTexture.create3DFilterableFromDataArray({ + width: dims[0], + height: dims[1], + depth: dims[2], + dataArray: scalars, + preferSizeOverAccuracy: volumeProperty.getPreferSizeOverAccuracy(), + }); model._openGLRenderWindow.setGraphicsResourceForObject( scalars, newScalarTexture, @@ -1720,6 +1730,22 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { } else { model.scalarTextures[component] = tex.oglObject; } + + if (hasUpdatedExtents) { + // If hasUpdatedExtents, then the texture is partially updated. + // clear the array to acknowledge the update. + volumeProperty.setUpdatedExtents([]); + + const dims = imageData.getDimensions(); + model.scalarTextures[component].create3DFilterableFromDataArray({ + width: dims[0], + height: dims[1], + depth: dims[2], + dataArray: scalars, + updatedExtents, + }); + } + replaceGraphicsResource( model._openGLRenderWindow, model._scalarTexturesCore[component], @@ -1742,7 +1768,10 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { newLabelOutlineThicknessTexture.setOpenGLRenderWindow( model._openGLRenderWindow ); - const lWidth = 1024; + let lWidth = model.renderable.getLabelOutlineTextureWidth(); + if (lWidth <= 0) { + lWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE); + } const lHeight = 1; const lSize = lWidth * lHeight; const lTable = new Uint8Array(lSize); @@ -1764,13 +1793,13 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { newLabelOutlineThicknessTexture.setMagnificationFilter(Filter.NEAREST); // Create a 2D texture (acting as 1D) from the raw data - newLabelOutlineThicknessTexture.create2DFromRaw( - lWidth, - lHeight, - 1, - VtkDataTypes.UNSIGNED_CHAR, - lTable - ); + newLabelOutlineThicknessTexture.create2DFromRaw({ + width: lWidth, + height: lHeight, + numComps: 1, + dataType: VtkDataTypes.UNSIGNED_CHAR, + data: lTable, + }); if (labelOutlineThicknessArray) { model._openGLRenderWindow.setGraphicsResourceForObject( diff --git a/Sources/Rendering/OpenGL/VolumeMapper/test/testUpdatedExtents.js b/Sources/Rendering/OpenGL/VolumeMapper/test/testUpdatedExtents.js new file mode 100644 index 00000000000..571ee00e7e8 --- /dev/null +++ b/Sources/Rendering/OpenGL/VolumeMapper/test/testUpdatedExtents.js @@ -0,0 +1,124 @@ +import test from 'tape'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; + +import 'vtk.js/Sources/Rendering/Misc/RenderingAPIs'; + +import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction'; +import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction'; +import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; +import vtkRenderWindowInteractor from 'vtk.js/Sources/Rendering/Core/RenderWindowInteractor'; +import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; +import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume'; +import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; +import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; +import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData'; + +import baseline from './testUpdatedExtents.png'; + +test.onlyIfWebGL('Test Volume Mapper Updated Extents', async (t) => { + const gc = testUtils.createGarbageCollector(t); + t.ok('rendering', 'vtkVolumeMapper UpdatedExtents'); + + // Create some control UI + const container = document.querySelector('body'); + const renderWindowContainer = gc.registerDOMElement( + document.createElement('div') + ); + container.appendChild(renderWindowContainer); + + // create what we will view + const renderWindow = gc.registerResource(vtkRenderWindow.newInstance()); + const renderer = gc.registerResource(vtkRenderer.newInstance()); + renderWindow.addRenderer(renderer); + renderer.setBackground(0.32, 0.34, 0.43); + + const actor = gc.registerResource(vtkVolume.newInstance()); + + const mapper = gc.registerResource(vtkVolumeMapper.newInstance()); + mapper.setSampleDistance(0.5); + actor.setMapper(mapper); + + const sideLen = 100; + + const imageData = vtkImageData.newInstance(); + imageData.setExtent(0, sideLen - 1, 0, sideLen - 1, 0, sideLen - 1); + const pixelData = new Float32Array(sideLen * sideLen * sideLen); + + const da = vtkDataArray.newInstance({ + numberOfComponents: 1, + values: pixelData, + }); + da.setName('scalars'); + imageData.getPointData().setScalars(da); + + mapper.setInputData(imageData); + + // create color and opacity transfer functions + const ctfun = vtkColorTransferFunction.newInstance(); + ctfun.addRGBPoint(0.0, 0.0, 0.0, 0.0); + ctfun.addRGBPoint(1.0, 1.0, 0.0, 0.5); + const ofun = vtkPiecewiseFunction.newInstance(); + ofun.addPoint(0.0, 0.0); + ofun.addPoint(1.0, 1.0); + actor.getProperty().setRGBTransferFunction(0, ctfun); + actor.getProperty().setScalarOpacity(0, ofun); + actor.getProperty().setAmbient(0.5); + actor.getProperty().setShade(1); + + // now create something to view it + const glwindow = gc.registerResource(renderWindow.newAPISpecificView()); + glwindow.setContainer(renderWindowContainer); + renderWindow.addView(glwindow); + glwindow.setSize(400, 400); + + // Interactor + const interactor = vtkRenderWindowInteractor.newInstance(); + interactor.setStillUpdateRate(0.01); + interactor.setView(glwindow); + interactor.initialize(); + interactor.bindEvents(renderWindowContainer); + + renderer.addVolume(actor); + actor.getProperty().setInterpolationTypeToLinear(); + + // Initial render + renderer.resetCamera(); + renderWindow.render(); + + const mid = sideLen / 2; + const radius = mid - 1; + + // Generate a 3D sphere + let i = 0; + for (let z = 0; z < sideLen; z++) { + for (let y = 0; y < sideLen; y++) { + for (let x = 0; x < sideLen; x++) { + const dist = Math.sqrt( + (x - mid) ** 2 + (y - mid) ** 2 + (z - mid) ** 2 + ); + pixelData[i++] = dist < radius ? 1 : 0; + } + } + } + + // Update only a portion of the volume rendering texture + mapper.setUpdatedExtents([ + [0, mid - 1, 0, mid - 1, 0, mid - 1], + [mid - 1, sideLen - 1, mid - 1, sideLen - 1, mid - 1, sideLen - 1], + ]); + + const promise = glwindow + .captureNextImage() + .then((image) => + testUtils.compareImages( + image, + [baseline], + 'Rendering/Core/VolumeMapper/testUpdatedExtents', + t, + 1.5, + gc.releaseResources + ) + ); + renderWindow.render(); + return promise; +}); diff --git a/Sources/Rendering/OpenGL/VolumeMapper/test/testUpdatedExtents.png b/Sources/Rendering/OpenGL/VolumeMapper/test/testUpdatedExtents.png new file mode 100644 index 00000000000..bdddcabb5c0 Binary files /dev/null and b/Sources/Rendering/OpenGL/VolumeMapper/test/testUpdatedExtents.png differ diff --git a/Sources/Rendering/SceneGraph/ViewNode/index.js b/Sources/Rendering/SceneGraph/ViewNode/index.js index 7718ba841d4..6d508835c52 100644 --- a/Sources/Rendering/SceneGraph/ViewNode/index.js +++ b/Sources/Rendering/SceneGraph/ViewNode/index.js @@ -113,14 +113,27 @@ function vtkViewNode(publicAPI, model) { // add missing nodes/children for the passed in renderables. This should // be called only in between prepareNodes and removeUnusedNodes - publicAPI.addMissingNodes = (dataObjs) => { + publicAPI.addMissingNodes = (dataObjs, enforceOrder = false) => { if (!dataObjs || !dataObjs.length) { return; } for (let index = 0; index < dataObjs.length; ++index) { const dobj = dataObjs[index]; - publicAPI.addMissingNode(dobj); + const node = publicAPI.addMissingNode(dobj); + if ( + enforceOrder && + node !== undefined && + model.children[index] !== node + ) { + for (let i = index + 1; i < model.children.length; ++i) { + if (model.children[i] === node) { + model.children.splice(i, 1); + model.children.splice(index, 0, node); + break; + } + } + } } }; diff --git a/Sources/Rendering/WebXR/RenderWindowHelper/index.js b/Sources/Rendering/WebXR/RenderWindowHelper/index.js index 07dda2bc614..5cf71d9cbf8 100644 --- a/Sources/Rendering/WebXR/RenderWindowHelper/index.js +++ b/Sources/Rendering/WebXR/RenderWindowHelper/index.js @@ -173,6 +173,15 @@ function vtkWebXRRenderWindowHelper(publicAPI, model) { const gl = model.renderWindow.get3DContext(); gl.bindFramebuffer(gl.FRAMEBUFFER, null); + // Remove controllers ray + const ren = model.renderWindow.getRenderable().getRenderers()[0]; + model.xrSession.inputSources.forEach((inputSource) => { + if (model.inputSourceToRay[inputSource.handedness]) { + ren.removeActor(model.inputSourceToRay[inputSource.handedness].actor); + model.inputSourceToRay[inputSource.handedness].visible = false; + } + }); + await model.xrSession.end().catch((error) => { if (!(error instanceof DOMException)) { throw error; diff --git a/Sources/Widgets/Core/WidgetManager/index.d.ts b/Sources/Widgets/Core/WidgetManager/index.d.ts index d47aa44430f..0927b664cad 100644 --- a/Sources/Widgets/Core/WidgetManager/index.d.ts +++ b/Sources/Widgets/Core/WidgetManager/index.d.ts @@ -12,6 +12,14 @@ import { vtkObject } from '../../../interfaces'; import { CaptureOn, ViewTypes } from './Constants'; import { Nullable } from '../../../types'; +export interface IDisplayScaleParams { + dispHeightFactor: number; + cameraPosition: number[]; + cameraDir: number[]; + isParallel: boolean; + rendererPixelDims: number[]; +} + export interface ISelectedData { requestCount: number; propID: number; @@ -45,7 +53,10 @@ export function extractRenderingComponents( * (vertical) distance that matches a display distance of 30px for a coordinate * `coord`, you would compute `30 * getPixelWorldHeightAtCoord(coord)`. */ -export function getPixelWorldHeightAtCoord(coord: []): Number; +export function getPixelWorldHeightAtCoord( + coord: [], + displayScaleParams: IDisplayScaleParams +): Number; export interface vtkWidgetManager extends vtkObject { /** diff --git a/Sources/Widgets/Representations/WidgetRepresentation/index.d.ts b/Sources/Widgets/Representations/WidgetRepresentation/index.d.ts index 6f95958f36a..214c8e7dac5 100644 --- a/Sources/Widgets/Representations/WidgetRepresentation/index.d.ts +++ b/Sources/Widgets/Representations/WidgetRepresentation/index.d.ts @@ -2,13 +2,7 @@ import vtkDataArray from '../../../Common/Core/DataArray'; import vtkPolyData from '../../../Common/DataModel/PolyData'; import { vtkObject } from '../../../interfaces'; import vtkProp from '../../../Rendering/Core/Prop'; -export interface IDisplayScaleParams { - dispHeightFactor: number; - cameraPosition: number[]; - cameraDir: number[]; - isParallel: boolean; - rendererPixelDims: number[]; -} +import { IDisplayScaleParams } from '../../../Widgets/Core/WidgetManager'; export interface IWidgetRepresentationInitialValues { labels?: Array; diff --git a/Sources/Widgets/Widgets3D/ResliceCursorWidget/index.d.ts b/Sources/Widgets/Widgets3D/ResliceCursorWidget/index.d.ts index 023ae563be9..5b27c12a4e7 100644 --- a/Sources/Widgets/Widgets3D/ResliceCursorWidget/index.d.ts +++ b/Sources/Widgets/Widgets3D/ResliceCursorWidget/index.d.ts @@ -12,14 +12,7 @@ import vtkRenderer from '../../../Rendering/Core/Renderer'; import vtkPlaneManipulator from '../../Manipulators/PlaneManipulator'; import { ViewTypes } from '../../../Widgets/Core/WidgetManager/Constants'; import { Vector2, Vector3 } from '../../../types'; - -export interface IDisplayScaleParams { - dispHeightFactor: number; - cameraPosition: Vector3; - cameraDir: Vector3; - isParallel: false; - rendererPixelDims: Vector2; -} +import { IDisplayScaleParams } from '../../../Widgets/Core/WidgetManager'; export interface vtkResliceCursorWidget< WidgetInstance extends vtkAbstractWidget = vtkResliceCursorWidgetDefaultInstance diff --git a/Sources/macros.js b/Sources/macros.js index 0268a382dd5..50d162d567d 100644 --- a/Sources/macros.js +++ b/Sources/macros.js @@ -8,6 +8,10 @@ import ClassHierarchy from './Common/Core/ClassHierarchy'; let globalMTime = 0; +export const requiredParam = (name) => { + throw new Error(`Named parameter '${name}' is missing`); +}; + export const VOID = Symbol('void'); function getCurrentGlobalMTime() { @@ -1816,4 +1820,5 @@ export default { vtkWarningMacro, // vtk.js internal use objectSetterMap, + requiredParam, }; diff --git a/package-lock.json b/package-lock.json index c4e4df119da..a46ea950b6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "vtk.js", "version": "0.0.0-semantically-release", - "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "7.22.11", diff --git a/package.json b/package.json index c436f311e92..96892223005 100644 --- a/package.json +++ b/package.json @@ -141,19 +141,20 @@ "reformat-only": "prettier --single-quote --trailing-comma es5 --print-width 80 --arrow-parens always --write", "lint-fix": "eslint --fix Sources Examples", "lint": "eslint Sources Examples", - "doc": "kw-doc -c ./Documentation/config.js", - "doc:www": "npm t -- --single-run && kw-doc -c ./Documentation/config.js -s", - "doc:minified": "kw-doc -c ./Documentation/config.js -m", - "doc:generate-api": "node ./Documentation/generate-api-docs.js", + "doc": "npm run build:pre && kw-doc -c ./Documentation/config.js", + "doc:www": "npm t -- --single-run && npm run build:pre && kw-doc -c ./Documentation/config.js -s", + "doc:minified": "npm run build:pre && kw-doc -c ./Documentation/config.js -m", + "doc:generate-api": "npm run build:pre && node ./Documentation/generate-api-docs.js", "example": "node ./Utilities/ExampleRunner/example-runner-cli.js -c ./Documentation/config.js", "example:https": "node ./Utilities/ExampleRunner/example-runner-cli.js --server-type https -c ./Documentation/config.js", "example:webgpu": "cross-env WEBGPU=1 NO_WEBGL=1 node ./Utilities/ExampleRunner/example-runner-cli.js --server-type https -c ./Documentation/config.js", + "build:pre": "patch-package", "dev:esm": "npm run build:esm -- -w", - "dev:umd": "webpack --watch --config webpack.dev.js --progress", + "dev:umd": "npm run build:pre && webpack --watch --config webpack.dev.js --progress", "build": "npm run build:release", - "build:esm": "rollup -c rollup.config.js", - "build:umd": "webpack --config webpack.prod.js --progress", - "build:release": "npm run lint && concurrently \"cross-env NOLINT=1 npm run build:esm\" \"cross-env NOLINT=1 npm run build:umd\"", + "build:esm": "npm run build:pre && rollup -c rollup.config.js", + "build:umd": "npm run build:pre && webpack --config webpack.prod.js --progress", + "build:release": "npm run lint && npm run build:pre && concurrently \"cross-env NOLINT=1 npm run build:esm\" \"cross-env NOLINT=1 npm run build:umd\"", "release:create-packages": "node ./Utilities/ci/build-npm-package.js", "test": "karma start ./karma.conf.js", "test:headless": "karma start ./karma.conf.js --browsers ChromeHeadlessNoSandbox --single-run", @@ -163,8 +164,7 @@ "test:firefox-debug": "karma start ./karma.conf.js --browsers Firefox --no-single-run", "commit": "git cz", "semantic-release": "semantic-release", - "prepare": "node ./Utilities/prepare.js", - "postinstall": "patch-package" + "prepare": "node ./Utilities/prepare.js" }, "config": { "commitizen": {