Skip to content

Commit 375e389

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

File tree

5 files changed

+245
-106
lines changed

5 files changed

+245
-106
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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 = (
102+
frameRate,
103+
desiredUpdateRate
104+
) => {
105+
if (!model.autoAdjustSampleDistances) {
106+
model._currentImageSampleDistanceScale =
107+
model.imageSampleDistance * model.imageSampleDistance;
108+
return model._currentImageSampleDistanceScale;
109+
}
110+
111+
const currentScale = ensureImageSampleDistanceScale();
112+
if (!(frameRate > 0) || !(desiredUpdateRate > 0)) {
113+
return currentScale;
114+
}
115+
116+
const adjustment = desiredUpdateRate / frameRate;
117+
118+
// Ignore minor noise in measured frame rates.
119+
if (adjustment > 1.15 || adjustment < 0.85) {
120+
model._currentImageSampleDistanceScale = currentScale * adjustment;
121+
}
122+
123+
if (model._currentImageSampleDistanceScale > 400) {
124+
model._currentImageSampleDistanceScale = 400;
125+
}
126+
if (model._currentImageSampleDistanceScale < 1.5) {
127+
model._currentImageSampleDistanceScale = 1.5;
128+
}
129+
130+
return model._currentImageSampleDistanceScale;
131+
};
132+
133+
const superSetAutoAdjustSampleDistances =
134+
publicAPI.setAutoAdjustSampleDistances;
135+
publicAPI.setAutoAdjustSampleDistances = (autoAdjustSampleDistances) => {
136+
const changed = superSetAutoAdjustSampleDistances(
137+
autoAdjustSampleDistances
138+
);
139+
if (changed) {
140+
model._currentImageSampleDistanceScale =
141+
getDefaultImageSampleDistanceScale();
142+
}
143+
return changed;
144+
};
145+
146+
const superSetImageSampleDistance = publicAPI.setImageSampleDistance;
147+
publicAPI.setImageSampleDistance = (imageSampleDistance) => {
148+
const changed = superSetImageSampleDistance(imageSampleDistance);
149+
if (changed && !model.autoAdjustSampleDistances) {
150+
model._currentImageSampleDistanceScale =
151+
imageSampleDistance * imageSampleDistance;
152+
}
153+
return changed;
154+
};
155+
156+
publicAPI.delete = macro.chain(() => {
157+
unsubscribe(model._autoAdjustSampleDistancesSubscription);
158+
model._autoAdjustSampleDistancesSubscription = null;
159+
}, publicAPI.delete);
160+
161+
model._currentImageSampleDistanceScale = getDefaultImageSampleDistanceScale();
162+
updateSourceSubscription();
163+
}
164+
165+
export default {
166+
implementAutoAdjustSampleDistances,
167+
};

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

Lines changed: 49 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?: (callback: () => void) => {
29+
unsubscribe: () => void;
30+
};
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,11 @@ 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(): Nullable<vtkVolumeMapperAutoAdjustSource>;
86+
7187
/**
7288
* Get at what scale the quality is reduced when interacting for the first time with the volume
7389
* It should should be set before any call to render for this volume
@@ -83,6 +99,21 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D {
8399
*/
84100
getInteractionSampleDistanceFactor(): number;
85101

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

179+
/**
180+
* Set the source used to drive automatic sample distance adjustment.
181+
* This can be a vtkRenderWindowInteractor or any external controller that exposes
182+
* compatible timing and animation methods.
183+
*/
184+
setAutoAdjustSampleDistancesSource(
185+
autoAdjustSampleDistancesSource: vtkVolumeMapperAutoAdjustSource | null
186+
): boolean;
187+
148188
/**
149189
*
150190
* @param initialInteractionScale
@@ -159,6 +199,14 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D {
159199
interactionSampleDistanceFactor: number
160200
): boolean;
161201

202+
/**
203+
* Update the current interaction scale using externally measured timing data.
204+
*/
205+
updateAutoAdjustSampleDistances(
206+
frameRate: number,
207+
desiredUpdateRate: number
208+
): number;
209+
162210
/**
163211
*
164212
*/

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: 11 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,21 +1168,11 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
11681168
}
11691169
};
11701170

1171-
// unsubscribe from our listeners
1172-
publicAPI.delete = macro.chain(
1173-
() => {
1174-
if (model._animationRateSubscription) {
1175-
model._animationRateSubscription.unsubscribe();
1176-
model._animationRateSubscription = null;
1177-
}
1178-
},
1179-
() => {
1180-
if (model._openGLRenderWindow) {
1181-
unregisterGraphicsResources(model._openGLRenderWindow);
1182-
}
1183-
},
1184-
publicAPI.delete
1185-
);
1171+
publicAPI.delete = macro.chain(() => {
1172+
if (model._openGLRenderWindow) {
1173+
unregisterGraphicsResources(model._openGLRenderWindow);
1174+
}
1175+
}, publicAPI.delete);
11861176

11871177
publicAPI.getRenderTargetSize = () => {
11881178
if (model._useSmallViewport) {
@@ -1201,59 +1191,19 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
12011191
return [lowerLeftU, lowerLeftV];
12021192
};
12031193

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-
};
1194+
publicAPI.getCurrentSampleDistance = () =>
1195+
model.renderable.getCurrentSampleDistance();
12131196

12141197
publicAPI.renderPieceStart = (ren, actor) => {
12151198
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-
}
1199+
model.renderable.setAutoAdjustSampleDistancesSource(rwi);
1200+
model._useSmallViewport = model.renderable.getUseSmallViewport();
12521201

12531202
// use/create/resize framebuffer if needed
12541203
if (model._useSmallViewport) {
12551204
const size = model._openGLRenderWindow.getFramebufferSize();
1256-
const scaleFactor = 1 / Math.sqrt(model._lastScale);
1205+
const scaleFactor =
1206+
1 / Math.sqrt(model.renderable.getCurrentImageSampleDistanceScale());
12571207
model._smallViewportWidth = Math.ceil(scaleFactor * size[0]);
12581208
model._smallViewportHeight = Math.ceil(scaleFactor * size[1]);
12591209

0 commit comments

Comments
 (0)