Skip to content

Commit c5b1b53

Browse files
committed
feat(VolumeMapper): allow tool-driven auto sample distance control
fixes #3275
1 parent 9d00695 commit c5b1b53

5 files changed

Lines changed: 240 additions & 98 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import macro from 'vtk.js/Sources/macros';
2+
3+
function getFrameRate(source) {
4+
if (!source) {
5+
return null;
6+
}
7+
if (source.getRecentAnimationFrameRate) {
8+
return source.getRecentAnimationFrameRate();
9+
}
10+
if (source.getFrameRate) {
11+
return source.getFrameRate();
12+
}
13+
return null;
14+
}
15+
16+
function getDesiredUpdateRate(source) {
17+
if (!source?.getDesiredUpdateRate) {
18+
return null;
19+
}
20+
return source.getDesiredUpdateRate();
21+
}
22+
23+
function unsubscribe(subscription) {
24+
subscription?.unsubscribe?.();
25+
}
26+
27+
export function implementAutoAdjustSampleDistances(publicAPI, model) {
28+
function getDefaultImageSampleDistanceScale() {
29+
if (model.autoAdjustSampleDistances) {
30+
return model.initialInteractionScale || 1.0;
31+
}
32+
return model.imageSampleDistance * model.imageSampleDistance;
33+
}
34+
35+
function ensureImageSampleDistanceScale() {
36+
if (model._currentImageSampleDistanceScale == null) {
37+
model._currentImageSampleDistanceScale =
38+
getDefaultImageSampleDistanceScale();
39+
}
40+
return model._currentImageSampleDistanceScale;
41+
}
42+
43+
function updateFromCurrentSource() {
44+
const frameRate = getFrameRate(model.autoAdjustSampleDistancesSource);
45+
const desiredUpdateRate = getDesiredUpdateRate(
46+
model.autoAdjustSampleDistancesSource
47+
);
48+
49+
publicAPI.updateAutoAdjustSampleDistances(frameRate, desiredUpdateRate);
50+
}
51+
52+
function updateSourceSubscription() {
53+
unsubscribe(model._autoAdjustSampleDistancesSubscription);
54+
model._autoAdjustSampleDistancesSubscription = null;
55+
56+
if (model.autoAdjustSampleDistancesSource?.onAnimationFrameRateUpdate) {
57+
model._autoAdjustSampleDistancesSubscription =
58+
model.autoAdjustSampleDistancesSource.onAnimationFrameRateUpdate(
59+
updateFromCurrentSource
60+
);
61+
}
62+
}
63+
64+
publicAPI.getAutoAdjustSampleDistancesSource = () =>
65+
model.autoAdjustSampleDistancesSource;
66+
67+
publicAPI.setAutoAdjustSampleDistancesSource = (source) => {
68+
if (model.autoAdjustSampleDistancesSource === source) {
69+
return false;
70+
}
71+
72+
model.autoAdjustSampleDistancesSource = source;
73+
updateSourceSubscription();
74+
publicAPI.modified();
75+
return true;
76+
};
77+
78+
publicAPI.isAutoAdjustSampleDistancesSourceAnimating = () =>
79+
!!model.autoAdjustSampleDistancesSource?.isAnimating?.();
80+
81+
publicAPI.getCurrentImageSampleDistanceScale = () => {
82+
if (!model.autoAdjustSampleDistances) {
83+
return model.imageSampleDistance * model.imageSampleDistance;
84+
}
85+
86+
return ensureImageSampleDistanceScale();
87+
};
88+
89+
publicAPI.getUseSmallViewport = () =>
90+
publicAPI.isAutoAdjustSampleDistancesSourceAnimating() &&
91+
publicAPI.getCurrentImageSampleDistanceScale() > 1.5;
92+
93+
publicAPI.getCurrentSampleDistance = () => {
94+
const baseSampleDistance = model.sampleDistance;
95+
if (publicAPI.isAutoAdjustSampleDistancesSourceAnimating()) {
96+
return baseSampleDistance * model.interactionSampleDistanceFactor;
97+
}
98+
return baseSampleDistance;
99+
};
100+
101+
publicAPI.updateAutoAdjustSampleDistances = (frameRate, desiredUpdateRate) => {
102+
if (!model.autoAdjustSampleDistances) {
103+
model._currentImageSampleDistanceScale =
104+
model.imageSampleDistance * model.imageSampleDistance;
105+
return model._currentImageSampleDistanceScale;
106+
}
107+
108+
const currentScale = ensureImageSampleDistanceScale();
109+
if (!(frameRate > 0) || !(desiredUpdateRate > 0)) {
110+
return currentScale;
111+
}
112+
113+
const adjustment = desiredUpdateRate / frameRate;
114+
115+
// Ignore minor noise in measured frame rates.
116+
if (adjustment > 1.15 || adjustment < 0.85) {
117+
model._currentImageSampleDistanceScale = currentScale * adjustment;
118+
}
119+
120+
if (model._currentImageSampleDistanceScale > 400) {
121+
model._currentImageSampleDistanceScale = 400;
122+
}
123+
if (model._currentImageSampleDistanceScale < 1.5) {
124+
model._currentImageSampleDistanceScale = 1.5;
125+
}
126+
127+
return model._currentImageSampleDistanceScale;
128+
};
129+
130+
const superSetAutoAdjustSampleDistances =
131+
publicAPI.setAutoAdjustSampleDistances;
132+
publicAPI.setAutoAdjustSampleDistances = (autoAdjustSampleDistances) => {
133+
const changed = superSetAutoAdjustSampleDistances(autoAdjustSampleDistances);
134+
if (changed) {
135+
model._currentImageSampleDistanceScale =
136+
getDefaultImageSampleDistanceScale();
137+
}
138+
return changed;
139+
};
140+
141+
const superSetImageSampleDistance = publicAPI.setImageSampleDistance;
142+
publicAPI.setImageSampleDistance = (imageSampleDistance) => {
143+
const changed = superSetImageSampleDistance(imageSampleDistance);
144+
if (changed && !model.autoAdjustSampleDistances) {
145+
model._currentImageSampleDistanceScale =
146+
imageSampleDistance * imageSampleDistance;
147+
}
148+
return changed;
149+
};
150+
151+
publicAPI.delete = macro.chain(
152+
() => {
153+
unsubscribe(model._autoAdjustSampleDistancesSubscription);
154+
model._autoAdjustSampleDistancesSubscription = null;
155+
},
156+
publicAPI.delete
157+
);
158+
159+
model._currentImageSampleDistanceScale = getDefaultImageSampleDistanceScale();
160+
updateSourceSubscription();
161+
}
162+
163+
export default {
164+
implementAutoAdjustSampleDistances,
165+
};

Sources/Rendering/Core/VolumeMapper/index.d.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { vtkPiecewiseFunction } from '../../../Common/DataModel/PiecewiseFunction';
2-
import { Bounds } from '../../../types';
2+
import { Bounds, Nullable } from '../../../types';
33
import {
44
vtkAbstractMapper3D,
55
IAbstractMapper3DInitialValues,
@@ -12,13 +12,24 @@ import { BlendMode } from './Constants';
1212
export interface IVolumeMapperInitialValues
1313
extends IAbstractMapper3DInitialValues {
1414
autoAdjustSampleDistances?: boolean;
15+
autoAdjustSampleDistancesSource?: Nullable<vtkVolumeMapperAutoAdjustSource>;
1516
blendMode?: BlendMode;
1617
bounds?: Bounds;
1718
maximumSamplesPerRay?: number;
1819
sampleDistance?: number;
1920
volumeShadowSamplingDistFactor?: number;
2021
}
2122

23+
export interface vtkVolumeMapperAutoAdjustSource {
24+
getDesiredUpdateRate?: () => number;
25+
getFrameRate?: () => number;
26+
getRecentAnimationFrameRate?: () => number;
27+
isAnimating?: () => boolean;
28+
onAnimationFrameRateUpdate?: (
29+
callback: () => void
30+
) => { unsubscribe: () => void };
31+
}
32+
2233
export interface vtkVolumeMapper extends vtkAbstractMapper3D {
2334
/**
2435
* Get the bounds for this mapper as [xmin, xmax, ymin, ymax,zmin, zmax].
@@ -68,6 +79,13 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D {
6879
*/
6980
getAutoAdjustSampleDistances(): boolean;
7081

82+
/**
83+
* Get the external timing/interaction source used to drive automatic sample distance adjustment.
84+
*/
85+
getAutoAdjustSampleDistancesSource():
86+
| vtkVolumeMapperAutoAdjustSource
87+
| null;
88+
7189
/**
7290
* Get at what scale the quality is reduced when interacting for the first time with the volume
7391
* It should should be set before any call to render for this volume
@@ -83,6 +101,21 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D {
83101
*/
84102
getInteractionSampleDistanceFactor(): number;
85103

104+
/**
105+
* Get the current area scale used to reduce the image sampling rate during interaction.
106+
*/
107+
getCurrentImageSampleDistanceScale(): number;
108+
109+
/**
110+
* Get the current sample distance, including any interaction adjustment.
111+
*/
112+
getCurrentSampleDistance(): number;
113+
114+
/**
115+
* Returns whether the mapper should render through a reduced viewport for the current interaction state.
116+
*/
117+
getUseSmallViewport(): boolean;
118+
86119
/**
87120
* Set blend mode to COMPOSITE_BLEND
88121
* @param {BlendMode} blendMode
@@ -145,6 +178,15 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D {
145178
*/
146179
setAutoAdjustSampleDistances(autoAdjustSampleDistances: boolean): boolean;
147180

181+
/**
182+
* Set the source used to drive automatic sample distance adjustment.
183+
* This can be a vtkRenderWindowInteractor or any external controller that exposes
184+
* compatible timing and animation methods.
185+
*/
186+
setAutoAdjustSampleDistancesSource(
187+
autoAdjustSampleDistancesSource: vtkVolumeMapperAutoAdjustSource | null
188+
): boolean;
189+
148190
/**
149191
*
150192
* @param initialInteractionScale
@@ -159,6 +201,14 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D {
159201
interactionSampleDistanceFactor: number
160202
): boolean;
161203

204+
/**
205+
* Update the current interaction scale using externally measured timing data.
206+
*/
207+
updateAutoAdjustSampleDistances(
208+
frameRate: number,
209+
desiredUpdateRate: number
210+
): number;
211+
162212
/**
163213
*
164214
*/

Sources/Rendering/Core/VolumeMapper/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants';
33
import vtkAbstractMapper3D from 'vtk.js/Sources/Rendering/Core/AbstractMapper3D';
44
import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox';
55
import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction';
6+
import AutoAdjustSampleDistancesHelper from 'vtk.js/Sources/Rendering/Core/VolumeMapper/AutoAdjustSampleDistancesHelper';
67

78
const { BlendMode } = Constants;
89

@@ -148,6 +149,7 @@ const defaultValues = (initialValues) => ({
148149
imageSampleDistance: 1.0,
149150
maximumSamplesPerRay: 1000,
150151
autoAdjustSampleDistances: true,
152+
autoAdjustSampleDistancesSource: null,
151153
initialInteractionScale: 1.0,
152154
interactionSampleDistanceFactor: 1.0,
153155
blendMode: BlendMode.COMPOSITE_BLEND,
@@ -181,6 +183,11 @@ export function extend(publicAPI, model, initialValues = {}) {
181183

182184
macro.event(publicAPI, model, 'lightingActivated');
183185

186+
AutoAdjustSampleDistancesHelper.implementAutoAdjustSampleDistances(
187+
publicAPI,
188+
model
189+
);
190+
184191
// Object methods
185192
vtkVolumeMapper(publicAPI, model);
186193
}

Sources/Rendering/OpenGL/VolumeMapper/index.js

Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,14 +1168,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
11681168
}
11691169
};
11701170

1171-
// unsubscribe from our listeners
11721171
publicAPI.delete = macro.chain(
1173-
() => {
1174-
if (model._animationRateSubscription) {
1175-
model._animationRateSubscription.unsubscribe();
1176-
model._animationRateSubscription = null;
1177-
}
1178-
},
11791172
() => {
11801173
if (model._openGLRenderWindow) {
11811174
unregisterGraphicsResources(model._openGLRenderWindow);
@@ -1201,59 +1194,19 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
12011194
return [lowerLeftU, lowerLeftV];
12021195
};
12031196

1204-
publicAPI.getCurrentSampleDistance = (ren) => {
1205-
const rwi = ren.getVTKWindow().getInteractor();
1206-
const baseSampleDistance = model.renderable.getSampleDistance();
1207-
if (rwi.isAnimating()) {
1208-
const factor = model.renderable.getInteractionSampleDistanceFactor();
1209-
return baseSampleDistance * factor;
1210-
}
1211-
return baseSampleDistance;
1212-
};
1197+
publicAPI.getCurrentSampleDistance = () =>
1198+
model.renderable.getCurrentSampleDistance();
12131199

12141200
publicAPI.renderPieceStart = (ren, actor) => {
12151201
const rwi = ren.getVTKWindow().getInteractor();
1216-
1217-
if (!model._lastScale) {
1218-
model._lastScale = model.renderable.getInitialInteractionScale();
1219-
}
1220-
model._useSmallViewport = false;
1221-
if (rwi.isAnimating() && model._lastScale > 1.5) {
1222-
model._useSmallViewport = true;
1223-
}
1224-
1225-
if (!model._animationRateSubscription) {
1226-
// when the animation frame rate changes recompute the scale factor
1227-
model._animationRateSubscription = rwi.onAnimationFrameRateUpdate(() => {
1228-
if (model.renderable.getAutoAdjustSampleDistances()) {
1229-
const frate = rwi.getRecentAnimationFrameRate();
1230-
const adjustment = rwi.getDesiredUpdateRate() / frate;
1231-
1232-
// only change if we are off by 15%
1233-
if (adjustment > 1.15 || adjustment < 0.85) {
1234-
model._lastScale *= adjustment;
1235-
}
1236-
// clamp scale to some reasonable values.
1237-
// Below 1.5 we will just be using full resolution as that is close enough
1238-
// Above 400 seems like a lot so we limit to that 1/20th per axis
1239-
if (model._lastScale > 400) {
1240-
model._lastScale = 400;
1241-
}
1242-
if (model._lastScale < 1.5) {
1243-
model._lastScale = 1.5;
1244-
}
1245-
} else {
1246-
model._lastScale =
1247-
model.renderable.getImageSampleDistance() *
1248-
model.renderable.getImageSampleDistance();
1249-
}
1250-
});
1251-
}
1202+
model.renderable.setAutoAdjustSampleDistancesSource(rwi);
1203+
model._useSmallViewport = model.renderable.getUseSmallViewport();
12521204

12531205
// use/create/resize framebuffer if needed
12541206
if (model._useSmallViewport) {
12551207
const size = model._openGLRenderWindow.getFramebufferSize();
1256-
const scaleFactor = 1 / Math.sqrt(model._lastScale);
1208+
const scaleFactor =
1209+
1 / Math.sqrt(model.renderable.getCurrentImageSampleDistanceScale());
12571210
model._smallViewportWidth = Math.ceil(scaleFactor * size[0]);
12581211
model._smallViewportHeight = Math.ceil(scaleFactor * size[1]);
12591212

0 commit comments

Comments
 (0)