Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 54 additions & 17 deletions Sources/Rendering/OpenGL/ImageCPRMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) {
(coreObject) =>
renderWindow.unregisterGraphicsResourceUser(coreObject, publicAPI)
);
// The default lookup textures are not shared
[model._defaultColorTexture, model._defaultPwfTexture].forEach((entry) =>
entry?.oglObject.releaseGraphicsResources(renderWindow)
);
model._defaultColorTexture = null;
model._defaultPwfTexture = null;
}

publicAPI.buildPass = (prepass) => {
Expand Down Expand Up @@ -275,18 +281,24 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) {
);

const firstColorTransferFunc = ppty.getRGBTransferFunction();
const cachedColorEntry =
model._openGLRenderWindow.getGraphicsResourceForObject(
firstColorTransferFunc
);
let cWidth = model.renderable.getColorTextureWidth();
if (cWidth <= 0) {
cWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE);
}
// Without a transfer function there is no core object to share the
// texture under, so the mapper keeps its own default grayscale ramp.
const colorHash = firstColorTransferFunc
? colorTextureHash
: `default-${cWidth}`;
const cachedColorEntry = firstColorTransferFunc
? model._openGLRenderWindow.getGraphicsResourceForObject(
firstColorTransferFunc
)
: model._defaultColorTexture;
const reBuildColorTexture =
!cachedColorEntry?.oglObject?.getHandle() ||
cachedColorEntry?.hash !== colorTextureHash;
cachedColorEntry?.hash !== colorHash;
if (reBuildColorTexture) {
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();
Expand Down Expand Up @@ -350,6 +362,14 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) {
);
}
model._colorTransferFunc = firstColorTransferFunc;
} else {
model._defaultColorTexture?.oglObject.releaseGraphicsResources(
model._openGLRenderWindow
);
model._defaultColorTexture = {
oglObject: model.colorTexture,
hash: colorHash,
};
}
} else {
model.colorTexture = cachedColorEntry.oglObject;
Expand All @@ -368,16 +388,20 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) {
numIComps
);
const firstPwFunc = ppty.getPiecewiseFunction();
const cachedPwfEntry =
model._openGLRenderWindow.getGraphicsResourceForObject(firstPwFunc);
let pwfWidth = model.renderable.getOpacityTextureWidth();
if (pwfWidth <= 0) {
pwfWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE);
}
// Same as the color texture: without a piecewise function the mapper
// keeps its own fully opaque default.
const pwfHash = firstPwFunc ? pwfTextureHash : `default-${pwfWidth}`;
const cachedPwfEntry = firstPwFunc
? model._openGLRenderWindow.getGraphicsResourceForObject(firstPwFunc)
: model._defaultPwfTexture;
const reBuildPwf =
!cachedPwfEntry?.oglObject?.getHandle() ||
cachedPwfEntry?.hash !== pwfTextureHash;
cachedPwfEntry?.hash !== pwfHash;
if (reBuildPwf) {
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();
Expand Down Expand Up @@ -444,6 +468,14 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) {
);
}
model._pwFunc = firstPwFunc;
} else {
model._defaultPwfTexture?.oglObject.releaseGraphicsResources(
model._openGLRenderWindow
);
model._defaultPwfTexture = {
oglObject: model.pwfTexture,
hash: pwfHash,
};
}
} else {
model.pwfTexture = cachedPwfEntry.oglObject;
Expand Down Expand Up @@ -603,8 +635,11 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) {
// (non indexed) vertex layout where elementCount matches the vertices.
forceFlatten: true,
});
model.VBOBuildTime.modified();
}

// Also mark property or actor only rebuilds, which do not rebuild the
// vertices, so the next render does not rebuild again.
model.VBOBuildTime.modified();
};

publicAPI.getNeedToRebuildShaders = (cellBO, ren, actor) => {
Expand Down Expand Up @@ -1436,6 +1471,8 @@ const DEFAULT_VALUES = {
// _scalars: null,
// _colorTransferFunc: null,
// _pwFunc: null,
// _defaultColorTexture: null,
// _defaultPwfTexture: null,
};

// ----------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { it, expect } from 'vitest';
import macro from 'vtk.js/Sources/macros';
import testUtils from 'vtk.js/Sources/Testing/testUtils';
import { createTrackedRenderView } from 'vtk.js/Sources/Testing/renderTestUtils';

import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkImageCPRMapper from 'vtk.js/Sources/Rendering/Core/ImageCPRMapper';
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkImageSlice from 'vtk.js/Sources/Rendering/Core/ImageSlice';
import vtkOpenGLImageCPRMapper from 'vtk.js/Sources/Rendering/OpenGL/ImageCPRMapper';
import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';
import { registerOverride } from 'vtk.js/Sources/Rendering/OpenGL/ViewNodeFactory';

const sideLen = 32;
const width = 20;

// A CPR mapper whose OpenGL view node counts its buffer object builds
let bufferObjectBuilds = 0;
const newCountingImageCPRMapper = macro.newInstance(
(publicAPI, model, initialValues) => {
vtkImageCPRMapper.extend(publicAPI, model, initialValues);
model.classHierarchy.push('vtkCountingImageCPRMapper');
}
);
registerOverride(
'vtkCountingImageCPRMapper',
macro.newInstance((publicAPI, model, initialValues) => {
vtkOpenGLImageCPRMapper.extend(publicAPI, model, initialValues);
const { buildBufferObjects } = publicAPI;
publicAPI.buildBufferObjects = (ren, actor) => {
bufferObjectBuilds += 1;
buildBufferObjects(ren, actor);
};
})
);

// The property has no transfer functions, so the mapper draws with its default
// grayscale ramp and fully opaque lookup textures.
function renderCPRSlice(gc) {
const tracked = createTrackedRenderView(gc);

const imageData = vtkImageData.newInstance();
imageData.setDimensions(sideLen, sideLen, sideLen);
imageData.getPointData().setScalars(
vtkDataArray.newInstance({
name: 'scalars',
values: Uint16Array.from({ length: sideLen ** 3 }, (_, i) => i % 4096),
})
);

// A straight centerline through the middle of the volume
const centerline = vtkPolyData.newInstance();
centerline
.getPoints()
.setData(Float32Array.from([16, 16, 2, 16, 16, 16, 16, 16, 29]), 3);
centerline.getLines().setData(Uint16Array.from([3, 0, 1, 2]));

const mapper = gc.registerResource(newCountingImageCPRMapper());
mapper.setUseUniformOrientation(true);
mapper.setImageData(imageData);
mapper.setCenterlineData(centerline);
mapper.setWidth(width);
const actor = gc.registerResource(vtkImageSlice.newInstance());
actor.setMapper(mapper);

tracked.renderer.addActor(actor);
tracked.renderer.resetCamera();
tracked.renderWindow.render();
return { ...tracked, actor, mapper };
}

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'rebuilds once after a property change instead of on every render',
() => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderWindow, actor } = renderCPRSlice(gc);
const objectsAfterFirstRender = tracker.count();
const buildsAfterFirstRender = bufferObjectBuilds;

actor.getProperty().setColorWindow(1000);
actor.getProperty().setColorLevel(500);
for (let i = 0; i < 20; ++i) {
renderWindow.render();
}
expect(bufferObjectBuilds - buildsAfterFirstRender).toBe(1);
expect(tracker.count()).toBe(objectsAfterFirstRender);

gc.releaseResources();
}
);

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'reuses its default lookup textures when the mapper changes',
() => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderWindow, mapper } = renderCPRSlice(gc);
const objectsAfterFirstRender = tracker.count();

for (let i = 1; i < 16; ++i) {
mapper.setWidth(width + i);
renderWindow.render();
}
expect(tracker.count()).toBe(objectsAfterFirstRender);

gc.releaseResources();
}
);

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees its default lookup textures when the slice leaves the scene',
() => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderer, renderWindow, actor } = renderCPRSlice(gc);
const objectsInScene = tracker.count();

renderer.removeActor(actor);
renderWindow.render();
// at least the volume, color and opacity textures
expect(objectsInScene - tracker.count()).toBeGreaterThanOrEqual(3);

gc.releaseResources();
}
);
75 changes: 57 additions & 18 deletions Sources/Rendering/OpenGL/ImageMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ function vtkOpenGLImageMapper(publicAPI, model) {
function unregisterGraphicsResources(renderWindow) {
// The openGLTexture is not shared
model.openGLTexture.releaseGraphicsResources(renderWindow);
// Neither are the default lookup textures used without transfer functions
[model._defaultColorTexture, model._defaultPwfTexture].forEach((entry) =>
entry?.oglObject.releaseGraphicsResources(renderWindow)
);
model._defaultColorTexture = null;
model._defaultPwfTexture = null;
// All these other resources are shared
[
model._colorTransferFunc,
Expand Down Expand Up @@ -977,21 +983,27 @@ function vtkOpenGLImageMapper(publicAPI, model) {
numIComps
);
const firstColorTransferFunc = actorProperty.getRGBTransferFunction();
const cTex = model._openGLRenderWindow.getGraphicsResourceForObject(
firstColorTransferFunc
);

const reBuildC =
!cTex?.oglObject?.getHandle() || cTex?.hash !== cfunToString;
let cWidth = model.renderable.getColorTextureWidth();
if (cWidth <= 0) {
cWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE);
}
// Without a transfer function there is no core object to share the
// texture under, so the mapper keeps its own default grayscale ramp.
const cHash = firstColorTransferFunc
? cfunToString
: `default-${cWidth}-${iType}`;
const cTex = firstColorTransferFunc
? model._openGLRenderWindow.getGraphicsResourceForObject(
firstColorTransferFunc
)
: model._defaultColorTexture;

const reBuildC = !cTex?.oglObject?.getHandle() || cTex?.hash !== cHash;
if (reBuildC) {
model.colorTexture = vtkOpenGLTexture.newInstance({
resizable: true,
});
model.colorTexture.setOpenGLRenderWindow(model._openGLRenderWindow);
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
Expand Down Expand Up @@ -1061,6 +1073,14 @@ function vtkOpenGLImageMapper(publicAPI, model) {
);
}
model._colorTransferFunc = firstColorTransferFunc;
} else {
model._defaultColorTexture?.oglObject.releaseGraphicsResources(
model._openGLRenderWindow
);
model._defaultColorTexture = {
oglObject: model.colorTexture,
hash: cHash,
};
}
} else {
model.colorTexture = cTex.oglObject;
Expand All @@ -1079,16 +1099,22 @@ function vtkOpenGLImageMapper(publicAPI, model) {
numIComps
);
const firstPwFunc = actorProperty.getPiecewiseFunction();
const pwfTex =
model._openGLRenderWindow.getGraphicsResourceForObject(firstPwFunc);
let pwfWidth = model.renderable.getOpacityTextureWidth();
if (pwfWidth <= 0) {
pwfWidth = model.context.getParameter(model.context.MAX_TEXTURE_SIZE);
}
// Same as the color texture: without a piecewise function the mapper
// keeps its own fully opaque default.
const pwfHash = firstPwFunc
? pwfunToString
: `default-${pwfWidth}-${iType}`;
const pwfTex = firstPwFunc
? model._openGLRenderWindow.getGraphicsResourceForObject(firstPwFunc)
: model._defaultPwfTexture;
// rebuild opacity tfun?
const reBuildPwf =
!pwfTex?.oglObject?.getHandle() || pwfTex?.hash !== pwfunToString;
!pwfTex?.oglObject?.getHandle() || pwfTex?.hash !== pwfHash;
if (reBuildPwf) {
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({
Expand Down Expand Up @@ -1166,6 +1192,14 @@ function vtkOpenGLImageMapper(publicAPI, model) {
);
}
model._pwFunc = firstPwFunc;
} else {
model._defaultPwfTexture?.oglObject.releaseGraphicsResources(
model._openGLRenderWindow
);
model._defaultPwfTexture = {
oglObject: model.pwfTexture,
hash: pwfHash,
};
}
} else {
model.pwfTexture = pwfTex.oglObject;
Expand Down Expand Up @@ -1414,9 +1448,12 @@ function vtkOpenGLImageMapper(publicAPI, model) {
// (non indexed) vertex layout where elementCount matches the vertices.
forceFlatten: true,
});
model.VBOBuildTime.modified();
model.VBOBuildString = toString;
}

// Also mark property or actor only rebuilds, which do not change the VBO
// string, so the next render does not rebuild again.
model.VBOBuildTime.modified();
};

publicAPI.updateLabelOutlineOpacityTexture = (image) => {
Expand Down Expand Up @@ -1622,6 +1659,8 @@ const DEFAULT_VALUES = {
// _colorTransferFunc: null,
// _pwFunc: null,
// _labelOutlineThicknessArray: null,
// _defaultColorTexture: null,
// _defaultPwfTexture: null,
};

// ----------------------------------------------------------------------------
Expand Down
Loading
Loading