Skip to content

Commit

Permalink
Merge branch 'master' into backport-LIC-fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
finetjul authored May 25, 2021
2 parents 37d3750 + c74d238 commit bebaf31
Show file tree
Hide file tree
Showing 23 changed files with 966 additions and 168 deletions.
8 changes: 8 additions & 0 deletions Examples/Volume/VolumeMapperBlendModes/controller.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,12 @@
</select>
</td>
</tr>
<tr class="averageIPScalar" style="display:none;">
<td>Average IP Scalar Min</td>
<td><input class="scalarMin" type="range" min="0" max="1" value="0" step="0.01"></td>
</tr>
<tr class="averageIPScalar" style="display:none;">
<td><p>Average IP Scalar Max</p></td>
<td><input class="scalarMax" type="range" min="0" max="1" value="1" step="0.01"></td>
</tr>
</table>
38 changes: 38 additions & 0 deletions Examples/Volume/VolumeMapperBlendModes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,45 @@ mapper.setInputConnection(reader.getOutputPort());

function updateBlendMode(event) {
const blendMode = parseInt(event.target.value, 10);
const averageIPScalarEls = document.querySelectorAll('.averageIPScalar');

mapper.setBlendMode(blendMode);
mapper.setAverageIPScalarRange(0.0, 1.0);

// if average blend mode
if (blendMode === 3) {
// Show average scalar ui
for (let i = 0; i < averageIPScalarEls.length; i += 1) {
const el = averageIPScalarEls[i];
el.style.display = 'table-row';
}
} else {
// Hide average scalar ui
for (let i = 0; i < averageIPScalarEls.length; i += 1) {
const el = averageIPScalarEls[i];
el.style.display = 'none';
}
}

renderWindow.render();
}

function updateScalarMin(event) {
mapper.setAverageIPScalarRange(
event.target.value,
parseFloat(mapper.getAverageIPScalarRange()[1])
);
renderWindow.render();
}

function updateScalarMax(event) {
mapper.setAverageIPScalarRange(
mapper.getAverageIPScalarRange()[0],
parseFloat(event.target.value)
);
renderWindow.render();
}

reader.setUrl(`${__BASE_PATH__}/data/volume/headsq.vti`).then(() => {
reader.loadData().then(() => {
renderer.addVolume(actor);
Expand All @@ -85,6 +119,10 @@ reader.setUrl(`${__BASE_PATH__}/data/volume/headsq.vti`).then(() => {

const el = document.querySelector('.blendMode');
el.addEventListener('change', updateBlendMode);
const scalarMinEl = document.querySelector('.scalarMin');
scalarMinEl.addEventListener('input', updateScalarMin);
const scalarMaxEl = document.querySelector('.scalarMax');
scalarMaxEl.addEventListener('input', updateScalarMax);
});
});

Expand Down
3 changes: 2 additions & 1 deletion Sources/Proxy/Core/ViewProxy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ function vtkViewProxy(publicAPI, model) {

// --------------------------------------------------------------------------

publicAPI.captureImage = () => model.renderWindow.captureImages()[0];
publicAPI.captureImage = ({ format = 'image/png', ...opts } = {}) =>
model.renderWindow.captureImages(format, opts)[0];

// --------------------------------------------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions Sources/Rendering/Core/RenderWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ function vtkRenderWindow(publicAPI, model) {
return results;
};

publicAPI.captureImages = (format = 'image/png') => {
publicAPI.captureImages = (format = 'image/png', opts = {}) => {
macro.setImmediate(publicAPI.render);
return model.views
.map((view) =>
view.captureNextImage ? view.captureNextImage(format) : undefined
view.captureNextImage ? view.captureNextImage(format, opts) : undefined
)
.filter((i) => !!i);
};
Expand Down
13 changes: 13 additions & 0 deletions Sources/Rendering/OpenGL/RenderWindow/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,16 @@ Initialize the rendering window. This will setup all system-specific
resources. This method and Finalize() must be symmetric and it
should be possible to call them multiple times, even changing WindowId
in-between. This is what WindowRemap does.

### captureNextImage(format, options);

Capture a screenshot of the contents of this renderwindow. The options object
can include a `size` array (`[w, h]`) or a `scale` floating point value, as well
as a `resetCamera` boolean. If `size` is provided, the captured screenshot will
be of the given size (and `resetCamera` could be useful in this case if the
aspect ratio of `size` does not match the current renderwindow size). Otherwise,
if `scale` is provided, it will be multiplied by the current renderwindow size
to compute the screenshot size. If no `size` or `scale` are provided, the
current renderwindow size is assumed. The default format is "image/png".

Returns a promise that resolves to the captured screenshot.
98 changes: 94 additions & 4 deletions Sources/Rendering/OpenGL/RenderWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import { VtkDataTypes } from 'vtk.js/Sources/Common/Core/DataArray/Constants';

const { vtkDebugMacro, vtkErrorMacro } = macro;
const IS_CHROME = navigator.userAgent.indexOf('Chrome') !== -1;
const SCREENSHOT_PLACEHOLDER = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
};

function checkRenderTargetSupport(gl, format, type) {
// create temporary frame buffer and texture
Expand Down Expand Up @@ -522,19 +529,102 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
publicAPI.invokeImageReady(screenshot);
}

publicAPI.captureNextImage = (format = 'image/png') => {
publicAPI.captureNextImage = (
format = 'image/png',
{ resetCamera = false, size = null, scale = 1 } = {}
) => {
if (model.deleted) {
return null;
}
model.imageFormat = format;
const previous = model.notifyStartCaptureImage;
model.notifyStartCaptureImage = true;

model._screenshot = {
size:
!!size || scale !== 1
? size || model.size.map((val) => val * scale)
: null,
};

return new Promise((resolve, reject) => {
const subscription = publicAPI.onImageReady((imageURL) => {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
resolve(imageURL);
if (model._screenshot.size === null) {
model.notifyStartCaptureImage = previous;
subscription.unsubscribe();
if (model._screenshot.placeHolder) {
// resize the main canvas back to its original size and show it
model.size = model._screenshot.originalSize;

// process the resize
publicAPI.modified();

// restore the saved camera parameters, if applicable
if (model._screenshot.cameras) {
model._screenshot.cameras.forEach(({ restoreParamsFn, arg }) =>
restoreParamsFn(arg)
);
}

// Trigger a render at the original size
publicAPI.traverseAllPasses();

// Remove and clean up the placeholder, revealing the original
model.el.removeChild(model._screenshot.placeHolder);
model._screenshot.placeHolder.remove();
model._screenshot = null;
}
resolve(imageURL);
} else {
// Create a placeholder image overlay while we resize and render
const tmpImg = document.createElement('img');
tmpImg.style = SCREENSHOT_PLACEHOLDER;
tmpImg.src = imageURL;
model._screenshot.placeHolder = model.el.appendChild(tmpImg);

// hide the main canvas
model.canvas.style.display = 'none';

// remember the main canvas original size, then resize it
model._screenshot.originalSize = model.size;
model.size = model._screenshot.size;
model._screenshot.size = null;

// process the resize
publicAPI.modified();

if (resetCamera) {
// If resetCamera was requested, we first save camera parameters
// from all the renderers, so we can restore them later
model._screenshot.cameras = model.renderable
.getRenderers()
.map((renderer) => {
const camera = renderer.getActiveCamera();
const params = camera.get(
'focalPoint',
'position',
'parallelScale'
);

return {
resetCameraFn: renderer.resetCamera,
restoreParamsFn: camera.set,
// "clone" the params so we don't keep refs to properties
arg: JSON.parse(JSON.stringify(params)),
};
});

// Perform the resetCamera() on each renderer only after capturing
// the params from all active cameras, in case there happen to be
// linked cameras among the renderers.
model._screenshot.cameras.forEach(({ resetCameraFn }) =>
resetCameraFn()
);
}

// Trigger a render at the custom size
publicAPI.traverseAllPasses();
}
});
});
};
Expand Down
12 changes: 9 additions & 3 deletions Sources/Rendering/OpenGL/VolumeMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,18 +232,24 @@ function vtkOpenGLVolumeMapper(publicAPI, model) {
).result;

const averageIPScalarRange = model.renderable.getAverageIPScalarRange();
let min = averageIPScalarRange[0];
let max = averageIPScalarRange[1];

// If min or max is not already a float.
// make them into floats for glsl
min = Number.isInteger(min) ? min.toFixed(1).toString() : min.toString();
max = Number.isInteger(max) ? max.toFixed(1).toString() : max.toString();

// TODO: Adding the .0 at the end feels hacky
FSSource = vtkShaderProgram.substitute(
FSSource,
'//VTK::AverageIPScalarRangeMin',
`${averageIPScalarRange[0]}.0`
min
).result;

FSSource = vtkShaderProgram.substitute(
FSSource,
'//VTK::AverageIPScalarRangeMax',
`${averageIPScalarRange[1]}.0`
max
).result;

shaders.Fragment = FSSource;
Expand Down
54 changes: 28 additions & 26 deletions Sources/Rendering/OpenGL/glsl/vtkVolumeFS.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,27 @@ vec4 getColorForValue(vec4 tValue, vec3 posIS, vec3 tstep)
return tColor;
}


bool valueWithinScalarRange(vec4 val, vec4 min, vec4 max) {
bool withinRange = false;
#if vtkNumComponents == 1
if (val.r >= min.r && val.r <= max.r) {
withinRange = true;
}
#endif
#if defined(vtkIndependentComponentsOn) && vtkNumComponents == 2
if (val.r >= min.r && val.r <= max.r &&
val.g >= min.g && val.g <= max.g) {
withinRange = true;
}
#endif
#if defined(vtkIndependentComponentsOn) && vtkNumComponents >= 3
if (all(greaterThanEqual(val, averageIPScalarRangeMin)) &&
all(lessThanEqual(val, averageIPScalarRangeMax))) {
withinRange = true;
}
#endif
return withinRange;
}

//=======================================================================
// Apply the specified blend mode operation along the ray's path.
Expand Down Expand Up @@ -779,20 +799,16 @@ void applyBlend(vec3 posIS, vec3 endIS, float sampleDistanceIS, vec3 tdims)
//VTK::AverageIPScalarRangeMin,
//VTK::AverageIPScalarRangeMin,
//VTK::AverageIPScalarRangeMin,
1.0);
//VTK::AverageIPScalarRangeMax);
vec4 averageIPScalarRangeMax = vec4(
//VTK::AverageIPScalarRangeMax,
//VTK::AverageIPScalarRangeMax,
//VTK::AverageIPScalarRangeMax,
1.0);
//VTK::AverageIPScalarRangeMax);

vec4 sum = vec4(0.);

averageIPScalarRangeMin.a = tValue.a;
averageIPScalarRangeMax.a = tValue.a;

if (all(greaterThanEqual(tValue, averageIPScalarRangeMin)) &&
all(lessThanEqual(tValue, averageIPScalarRangeMax))) {
if (valueWithinScalarRange(tValue, averageIPScalarRangeMin, averageIPScalarRangeMax)) {
sum += tValue;
}

Expand All @@ -818,23 +834,10 @@ void applyBlend(vec3 posIS, vec3 endIS, float sampleDistanceIS, vec3 tdims)
// - We are comparing all values in the texture to see if any of them
// are outside of the scalar range. In the future we might want to allow
// scalar ranges for each component.
// - We are setting the alpha channel for averageIPScalarRangeMin and
// averageIPScalarRangeMax so that we do not trigger this 'continue'
// based on the alpha channel comparison.
// - There might be a better way to do this. I'm not sure if there is an
// equivalent of 'any' which only operates on RGB, though I suppose
// we could write an 'anyRGB' function and see if that is faster.
averageIPScalarRangeMin.a = tValue.a;
averageIPScalarRangeMax.a = tValue.a;
if (any(lessThan(tValue, averageIPScalarRangeMin)) ||
any(greaterThan(tValue, averageIPScalarRangeMax))) {
continue;
if (valueWithinScalarRange(tValue, averageIPScalarRangeMin, averageIPScalarRangeMax)) {
// Sum the values across each step in the path
sum += tValue;
}

// Sum the values across each step in the path
sum += tValue;

// Otherwise, continue along the ray
stepsTraveled++;
posIS += stepIS;
}
Expand All @@ -847,8 +850,7 @@ void applyBlend(vec3 posIS, vec3 endIS, float sampleDistanceIS, vec3 tdims)
tValue = getTextureValue(posIS);

// One can control the scalar range by setting the AverageIPScalarRange to disregard scalar values, not in the range of interest, from the average computation
if (all(greaterThanEqual(tValue, averageIPScalarRangeMin)) &&
all(lessThanEqual(tValue, averageIPScalarRangeMax))) {
if (valueWithinScalarRange(tValue, averageIPScalarRangeMin, averageIPScalarRangeMax)) {
sum += tValue;

stepsTraveled++;
Expand Down
20 changes: 14 additions & 6 deletions Sources/Rendering/WebGPU/BindGroup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ function vtkWebGPUBindGroup(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkWebGPUBindGroup');

publicAPI.addBindable = (bindable) => {
// only add new bindables
for (let i = 0; i < model.bindables.length; i++) {
if (model.bindables[i] === bindable) {
publicAPI.setBindables = (bindables) => {
// is there a difference between the old and new list?
if (model.bindables.length === bindables.length) {
let allMatch = true;
for (let i = 0; i < model.bindables.length; i++) {
if (model.bindables[i] !== bindables[i]) {
allMatch = false;
}
}
if (allMatch) {
return;
}
}
model.bindables.push(bindable);

// there is a difference
model.bindables = bindables;
publicAPI.modified();
};

Expand All @@ -33,7 +41,7 @@ function vtkWebGPUBindGroup(publicAPI, model) {
// check mtime
let mtime = publicAPI.getMTime();
for (let i = 0; i < model.bindables.length; i++) {
const tm = model.bindables[i].getBindGroupTime();
const tm = model.bindables[i].getBindGroupTime().getMTime();
mtime = tm > mtime ? tm : mtime;
}
if (mtime < model.bindGroupTime.getMTime()) {
Expand Down
Loading

0 comments on commit bebaf31

Please sign in to comment.