Skip to content
Draft
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
167 changes: 167 additions & 0 deletions Sources/Rendering/Core/VolumeMapper/AutoAdjustSampleDistancesHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import macro from 'vtk.js/Sources/macros';

function getFrameRate(source) {
if (!source) {
return null;
}
if (source.getRecentAnimationFrameRate) {
return source.getRecentAnimationFrameRate();
}
if (source.getFrameRate) {
return source.getFrameRate();
}
return null;
}

function getDesiredUpdateRate(source) {
if (!source?.getDesiredUpdateRate) {
return null;
}
return source.getDesiredUpdateRate();
}

function unsubscribe(subscription) {
subscription?.unsubscribe?.();
}

export function implementAutoAdjustSampleDistances(publicAPI, model) {
function getDefaultImageSampleDistanceScale() {
if (model.autoAdjustSampleDistances) {
return model.initialInteractionScale || 1.0;
}
return model.imageSampleDistance * model.imageSampleDistance;
}

function ensureImageSampleDistanceScale() {
if (model._currentImageSampleDistanceScale == null) {
model._currentImageSampleDistanceScale =
getDefaultImageSampleDistanceScale();
}
return model._currentImageSampleDistanceScale;
}

function updateFromCurrentSource() {
const frameRate = getFrameRate(model.autoAdjustSampleDistancesSource);
const desiredUpdateRate = getDesiredUpdateRate(
model.autoAdjustSampleDistancesSource
);

publicAPI.updateAutoAdjustSampleDistances(frameRate, desiredUpdateRate);
}

function updateSourceSubscription() {
unsubscribe(model._autoAdjustSampleDistancesSubscription);
model._autoAdjustSampleDistancesSubscription = null;

if (model.autoAdjustSampleDistancesSource?.onAnimationFrameRateUpdate) {
model._autoAdjustSampleDistancesSubscription =
model.autoAdjustSampleDistancesSource.onAnimationFrameRateUpdate(
updateFromCurrentSource
);
}
}

publicAPI.getAutoAdjustSampleDistancesSource = () =>
model.autoAdjustSampleDistancesSource;

publicAPI.setAutoAdjustSampleDistancesSource = (source) => {
if (model.autoAdjustSampleDistancesSource === source) {
return false;
}

model.autoAdjustSampleDistancesSource = source;
updateSourceSubscription();
publicAPI.modified();
return true;
};

publicAPI.isAutoAdjustSampleDistancesSourceAnimating = () =>
!!model.autoAdjustSampleDistancesSource?.isAnimating?.();

publicAPI.getCurrentImageSampleDistanceScale = () => {
if (!model.autoAdjustSampleDistances) {
return model.imageSampleDistance * model.imageSampleDistance;
}

return ensureImageSampleDistanceScale();
};

publicAPI.getUseSmallViewport = () =>
publicAPI.isAutoAdjustSampleDistancesSourceAnimating() &&
publicAPI.getCurrentImageSampleDistanceScale() > 1.5;

publicAPI.getCurrentSampleDistance = () => {
const baseSampleDistance = model.sampleDistance;
if (publicAPI.isAutoAdjustSampleDistancesSourceAnimating()) {
return baseSampleDistance * model.interactionSampleDistanceFactor;
}
return baseSampleDistance;
};

publicAPI.updateAutoAdjustSampleDistances = (
frameRate,
desiredUpdateRate
) => {
if (!model.autoAdjustSampleDistances) {
model._currentImageSampleDistanceScale =
model.imageSampleDistance * model.imageSampleDistance;
return model._currentImageSampleDistanceScale;
}

const currentScale = ensureImageSampleDistanceScale();
if (!(frameRate > 0) || !(desiredUpdateRate > 0)) {
return currentScale;
}

const adjustment = desiredUpdateRate / frameRate;

// Ignore minor noise in measured frame rates.
if (adjustment > 1.15 || adjustment < 0.85) {
model._currentImageSampleDistanceScale = currentScale * adjustment;
}

if (model._currentImageSampleDistanceScale > 400) {
model._currentImageSampleDistanceScale = 400;
}
if (model._currentImageSampleDistanceScale < 1.5) {
model._currentImageSampleDistanceScale = 1.5;
}

return model._currentImageSampleDistanceScale;
};

const superSetAutoAdjustSampleDistances =
publicAPI.setAutoAdjustSampleDistances;
publicAPI.setAutoAdjustSampleDistances = (autoAdjustSampleDistances) => {
const changed = superSetAutoAdjustSampleDistances(
autoAdjustSampleDistances
);
if (changed) {
model._currentImageSampleDistanceScale =
getDefaultImageSampleDistanceScale();
}
return changed;
};

const superSetImageSampleDistance = publicAPI.setImageSampleDistance;
publicAPI.setImageSampleDistance = (imageSampleDistance) => {
const changed = superSetImageSampleDistance(imageSampleDistance);
if (changed && !model.autoAdjustSampleDistances) {
model._currentImageSampleDistanceScale =
imageSampleDistance * imageSampleDistance;
}
return changed;
};

publicAPI.delete = macro.chain(() => {
unsubscribe(model._autoAdjustSampleDistancesSubscription);
model._autoAdjustSampleDistancesSubscription = null;
}, publicAPI.delete);

model._currentImageSampleDistanceScale = getDefaultImageSampleDistanceScale();
updateSourceSubscription();
}

export default {
implementAutoAdjustSampleDistances,
};
50 changes: 49 additions & 1 deletion Sources/Rendering/Core/VolumeMapper/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { vtkPiecewiseFunction } from '../../../Common/DataModel/PiecewiseFunction';
import { Bounds } from '../../../types';
import { Bounds, Nullable } from '../../../types';
import {
vtkAbstractMapper3D,
IAbstractMapper3DInitialValues,
Expand All @@ -12,13 +12,24 @@ import { BlendMode } from './Constants';
export interface IVolumeMapperInitialValues
extends IAbstractMapper3DInitialValues {
autoAdjustSampleDistances?: boolean;
autoAdjustSampleDistancesSource?: Nullable<vtkVolumeMapperAutoAdjustSource>;
blendMode?: BlendMode;
bounds?: Bounds;
maximumSamplesPerRay?: number;
sampleDistance?: number;
volumeShadowSamplingDistFactor?: number;
}

export interface vtkVolumeMapperAutoAdjustSource {
getDesiredUpdateRate?: () => number;
getFrameRate?: () => number;
getRecentAnimationFrameRate?: () => number;
isAnimating?: () => boolean;
onAnimationFrameRateUpdate?: (callback: () => void) => {
unsubscribe: () => void;
};
}

export interface vtkVolumeMapper extends vtkAbstractMapper3D {
/**
* Get the bounds for this mapper as [xmin, xmax, ymin, ymax,zmin, zmax].
Expand Down Expand Up @@ -68,6 +79,11 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D {
*/
getAutoAdjustSampleDistances(): boolean;

/**
* Get the external timing/interaction source used to drive automatic sample distance adjustment.
*/
getAutoAdjustSampleDistancesSource(): Nullable<vtkVolumeMapperAutoAdjustSource>;

/**
* Get at what scale the quality is reduced when interacting for the first time with the volume
* It should should be set before any call to render for this volume
Expand All @@ -83,6 +99,21 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D {
*/
getInteractionSampleDistanceFactor(): number;

/**
* Get the current area scale used to reduce the image sampling rate during interaction.
*/
getCurrentImageSampleDistanceScale(): number;

/**
* Get the current sample distance, including any interaction adjustment.
*/
getCurrentSampleDistance(): number;

/**
* Returns whether the mapper should render through a reduced viewport for the current interaction state.
*/
getUseSmallViewport(): boolean;

/**
* Set blend mode to COMPOSITE_BLEND
* @param {BlendMode} blendMode
Expand Down Expand Up @@ -145,6 +176,15 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D {
*/
setAutoAdjustSampleDistances(autoAdjustSampleDistances: boolean): boolean;

/**
* Set the source used to drive automatic sample distance adjustment.
* This can be a vtkRenderWindowInteractor or any external controller that exposes
* compatible timing and animation methods.
*/
setAutoAdjustSampleDistancesSource(
autoAdjustSampleDistancesSource: vtkVolumeMapperAutoAdjustSource | null
): boolean;

/**
*
* @param initialInteractionScale
Expand All @@ -159,6 +199,14 @@ export interface vtkVolumeMapper extends vtkAbstractMapper3D {
interactionSampleDistanceFactor: number
): boolean;

/**
* Update the current interaction scale using externally measured timing data.
*/
updateAutoAdjustSampleDistances(
frameRate: number,
desiredUpdateRate: number
): number;

/**
*
*/
Expand Down
7 changes: 7 additions & 0 deletions Sources/Rendering/Core/VolumeMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants';
import vtkAbstractMapper3D from 'vtk.js/Sources/Rendering/Core/AbstractMapper3D';
import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox';
import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction';
import AutoAdjustSampleDistancesHelper from 'vtk.js/Sources/Rendering/Core/VolumeMapper/AutoAdjustSampleDistancesHelper';

const { BlendMode } = Constants;

Expand Down Expand Up @@ -148,6 +149,7 @@ const defaultValues = (initialValues) => ({
imageSampleDistance: 1.0,
maximumSamplesPerRay: 1000,
autoAdjustSampleDistances: true,
autoAdjustSampleDistancesSource: null,
initialInteractionScale: 1.0,
interactionSampleDistanceFactor: 1.0,
blendMode: BlendMode.COMPOSITE_BLEND,
Expand Down Expand Up @@ -181,6 +183,11 @@ export function extend(publicAPI, model, initialValues = {}) {

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

AutoAdjustSampleDistancesHelper.implementAutoAdjustSampleDistances(
publicAPI,
model
);

// Object methods
vtkVolumeMapper(publicAPI, model);
}
Expand Down
72 changes: 11 additions & 61 deletions Sources/Rendering/OpenGL/VolumeMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1168,21 +1168,11 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
}
};

// unsubscribe from our listeners
publicAPI.delete = macro.chain(
() => {
if (model._animationRateSubscription) {
model._animationRateSubscription.unsubscribe();
model._animationRateSubscription = null;
}
},
() => {
if (model._openGLRenderWindow) {
unregisterGraphicsResources(model._openGLRenderWindow);
}
},
publicAPI.delete
);
publicAPI.delete = macro.chain(() => {
if (model._openGLRenderWindow) {
unregisterGraphicsResources(model._openGLRenderWindow);
}
}, publicAPI.delete);

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

publicAPI.getCurrentSampleDistance = (ren) => {
const rwi = ren.getVTKWindow().getInteractor();
const baseSampleDistance = model.renderable.getSampleDistance();
if (rwi.isAnimating()) {
const factor = model.renderable.getInteractionSampleDistanceFactor();
return baseSampleDistance * factor;
}
return baseSampleDistance;
};
publicAPI.getCurrentSampleDistance = () =>
model.renderable.getCurrentSampleDistance();

publicAPI.renderPieceStart = (ren, actor) => {
const rwi = ren.getVTKWindow().getInteractor();

if (!model._lastScale) {
model._lastScale = model.renderable.getInitialInteractionScale();
}
model._useSmallViewport = false;
if (rwi.isAnimating() && model._lastScale > 1.5) {
model._useSmallViewport = true;
}

if (!model._animationRateSubscription) {
// when the animation frame rate changes recompute the scale factor
model._animationRateSubscription = rwi.onAnimationFrameRateUpdate(() => {
if (model.renderable.getAutoAdjustSampleDistances()) {
const frate = rwi.getRecentAnimationFrameRate();
const adjustment = rwi.getDesiredUpdateRate() / frate;

// only change if we are off by 15%
if (adjustment > 1.15 || adjustment < 0.85) {
model._lastScale *= adjustment;
}
// clamp scale to some reasonable values.
// Below 1.5 we will just be using full resolution as that is close enough
// Above 400 seems like a lot so we limit to that 1/20th per axis
if (model._lastScale > 400) {
model._lastScale = 400;
}
if (model._lastScale < 1.5) {
model._lastScale = 1.5;
}
} else {
model._lastScale =
model.renderable.getImageSampleDistance() *
model.renderable.getImageSampleDistance();
}
});
}
model.renderable.setAutoAdjustSampleDistancesSource(rwi);
model._useSmallViewport = model.renderable.getUseSmallViewport();

// use/create/resize framebuffer if needed
if (model._useSmallViewport) {
const size = model._openGLRenderWindow.getFramebufferSize();
const scaleFactor = 1 / Math.sqrt(model._lastScale);
const scaleFactor =
1 / Math.sqrt(model.renderable.getCurrentImageSampleDistanceScale());
model._smallViewportWidth = Math.ceil(scaleFactor * size[0]);
model._smallViewportHeight = Math.ceil(scaleFactor * size[1]);

Expand Down
Loading
Loading