Possible bug in hardware selector with sphereMapper

Hello,

I’m trying to adapt the vtk.js hardware selector example to work with vtkSphereMapper as below, instead of vtkGlyph3DMapper. Due to a high number of vertices (more than 300 000) in my model, vtkGlyph3DMapper with spheres runs too slowly. So I changed the code like this, but this raises an error : “Uncaught (in promise) TypeError: model.comntext is undefined”
I’m quite stuck here.

Thanks in advance for your help.

Best regards

/* eslint-disable import/prefer-default-export */ /* eslint-disable import/no-extraneous-dependencies */

import ‘@kitware/vtk.js/favicon’;

// Load the rendering pieces we want to use (for both WebGL and WebGPU)
import ‘@kitware/vtk.js/Rendering/Profiles/Geometry’;

import ‘@kitware/vtk.js/Rendering/Profiles/Molecule’; // necessary for Sphere Mapper

import { throttle } from ‘@kitware/vtk.js/macros’;
import vtkActor from ‘@kitware/vtk.js/Rendering/Core/Actor’;
import vtkCylinderSource from ‘@kitware/vtk.js/Filters/Sources/CylinderSource’;
import vtkDataArray from ‘@kitware/vtk.js/Common/Core/DataArray’;
import vtkFullScreenRenderWindow from ‘@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow’;
import vtkMapper from ‘@kitware/vtk.js/Rendering/Core/Mapper’;
import vtkSphereSource from ‘@kitware/vtk.js/Filters/Sources/SphereSource’;
import vtkMatrixBuilder from ‘@kitware/vtk.js/Common/Core/MatrixBuilder’;
import { mat4 } from ‘gl-matrix’;
import vtkMath from ‘@kitware/vtk.js/Common/Core/Math’;
import { FieldAssociations } from ‘@kitware/vtk.js/Common/DataModel/DataSet/Constants’;

import vtkSphereMapper from ‘@kitware/vtk.js/Rendering/Core/SphereMapper’;
import vtkPolyData from ‘@kitware/vtk.js/Common/DataModel/PolyData’;

// Cylinder -----------------------------------------------

const cylinderSource = vtkCylinderSource.newInstance({
resolution: 10,
radius: 0.4,
height: 0.6,
direction: [1.0, 0.0, 0.0],
});
const cylinderMapper = vtkSphereMapper.newInstance();

const polydata = vtkPolyData.newInstance();
polydata.setPoints(cylinderSource.getOutputData().getPoints());

const gcns_vertices = new Uint32Array(polydata.getNumberOfPoints() + 1);
gcns_vertices[0] = polydata.getNumberOfPoints();
for (let i = 0; i < polydata.getNumberOfPoints(); i++) {
gcns_vertices[i+1] = i;
}
polydata.getVerts().setData(gcns_vertices);

// Add fields to cylinderPointSet
const scalarArray = new Float32Array(polydata.getNumberOfPoints());
scalarArray.fill(0.1);

const scalars = vtkDataArray.newInstance({
name: ‘scalars’,
values: scalarArray,
numberOfComponents: 1
});

polydata.getPointData().setScalars(scalars);

cylinderMapper.setInputData(polydata);
cylinderMapper.setScaleArray(‘scalarArray’);
cylinderMapper.setScaleFactor(1);
const cylinderActor = vtkActor.newInstance({ position: [0, 1, 0] });
cylinderActor.setMapper(cylinderMapper);

// ----------------------------------------------------------------------------
// Create Picking pointer
// ----------------------------------------------------------------------------

const pointerSource = vtkSphereSource.newInstance({
phiResolution: 15,
thetaResolution: 15,
radius: 0.01,
});
const pointerMapper = vtkMapper.newInstance();
const pointerActor = vtkActor.newInstance();
pointerActor.setMapper(pointerMapper);
pointerMapper.setInputConnection(pointerSource.getOutputPort());

// ----------------------------------------------------------------------------
// Create rendering infrastructure
// ----------------------------------------------------------------------------

const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
background: [0, 0, 0],
});
const renderer = fullScreenRenderer.getRenderer();
const renderWindow = renderer.getRenderWindow();
const interactor = renderWindow.getInteractor();
const apiSpecificRenderWindow = interactor.getView();

renderer.addActor(cylinderActor);
renderer.addActor(pointerActor);

renderer.resetCamera();
renderWindow.render();

// ----------------------------------------------------------------------------
// Create hardware selector
// ----------------------------------------------------------------------------

const hardwareSelector = apiSpecificRenderWindow.getSelector();
hardwareSelector.setCaptureZValues(true);
// TODO: bug in FIELD_ASSOCIATION_POINTS mode
// hardwareSelector.setFieldAssociation(
// FieldAssociations.FIELD_ASSOCIATION_POINTS
// );
hardwareSelector.setFieldAssociation(FieldAssociations.FIELD_ASSOCIATION_CELLS);

// ----------------------------------------------------------------------------
// Create Mouse listener for picking on mouse move
// ----------------------------------------------------------------------------

function eventToWindowXY(event) {
// We know we are full screen => window.innerXXX
// Otherwise we can use pixel device ratio or else…
const { clientX, clientY } = event;
const [width, height] = apiSpecificRenderWindow.getSize();
const x = Math.round((width * clientX) / window.innerWidth);
const y = Math.round(height * (1 - clientY / window.innerHeight)); // Need to flip Y
return [x, y];
}

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

let needGlyphCleanup = false;
let lastProcessedActor = null;

const updateCursor = (worldPosition) => {
if (lastProcessedActor) {
pointerActor.setVisibility(true);
pointerActor.setPosition(worldPosition);
} else {
pointerActor.setVisibility(false);
}
renderWindow.render();
};

function processSelections(selections) {
renderer.getActors().forEach((a) => a.getProperty().setColor(1,0,0));
if (!selections || selections.length === 0) {
lastProcessedActor = null;
updateCursor();
return;
}

const {
worldPosition: rayHitWorldPosition,
compositeID,
prop,
propID,
attributeID,
} = selections[0].getProperties();

let closestCellPointWorldPosition = […rayHitWorldPosition];
if (attributeID || attributeID === 0) {
const input = prop.getMapper().getInputData();
if (!input.getCells()) {
input.buildCells();
}

// Get matrices to convert coordinates: (prop coordinates) <-> (world coordinates)
const glTempMat = mat4.fromValues(...prop.getMatrix());
mat4.transpose(glTempMat, glTempMat);
const propToWorld = vtkMatrixBuilder.buildFromDegree().setMatrix(glTempMat);
mat4.invert(glTempMat, glTempMat);
const worldToProp = vtkMatrixBuilder.buildFromDegree().setMatrix(glTempMat);
// Compute the position of the cursor in prop coordinates
const propPosition = [...rayHitWorldPosition];
worldToProp.apply(propPosition);

if (
  hardwareSelector.getFieldAssociation() ===
  FieldAssociations.FIELD_ASSOCIATION_POINTS
) {
  // Selecting points
  closestCellPointWorldPosition = [
    ...input.getPoints().getTuple(attributeID),
  ];
  propToWorld.apply(closestCellPointWorldPosition);
} else {
  // Selecting cells
  const cellPoints = input.getCellPoints(attributeID);
  if (cellPoints) {
    const pointIds = cellPoints.cellPointIds;
    // Find the closest cell point, and use that as cursor position
    const points = Array.from(pointIds).map((pointId) =>
      input.getPoints().getPoint(pointId)
    );
    const distance = (pA, pB) =>
      vtkMath.distance2BetweenPoints(pA, propPosition) -
      vtkMath.distance2BetweenPoints(pB, propPosition);
    const sorted = points.sort(distance);
    closestCellPointWorldPosition = [...sorted[0]];
    propToWorld.apply(closestCellPointWorldPosition);
  }
}

}
lastProcessedActor = prop;
// Use closestCellPointWorldPosition or rayHitWorldPosition
updateCursor(closestCellPointWorldPosition);

// Make the picked actor green
prop.getProperty().setColor(0,1,0);

// We hit the glyph, let’s scale the picked glyph
if (prop === cylinderActor) {
scaleArray.fill(0.5);
scaleArray[compositeID] = 0.7;
cylinderPointSet.modified();
needGlyphCleanup = true;
} else if (needGlyphCleanup) {
needGlyphCleanup = false;
scaleArray.fill(0.5);
cylinderPointSet.modified();
}
renderWindow.render();
}

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

function pickOnMouseEvent(event) {
if (interactor.isAnimating()) {
// We should not do picking when interacting with the scene
return;
}
const [x, y] = eventToWindowXY(event);

pointerActor.setVisibility(false);
hardwareSelector.getSourceDataAsync(renderer, x, y, x, y).then((result) => {
if (result) {
processSelections(result.generateSelection(x, y, x, y));
} else {
processSelections(null);
}
});
}
const throttleMouseHandler = throttle(pickOnMouseEvent, 20);

document.addEventListener(‘mousemove’, throttleMouseHandler);

Not really a bug since that feature was never implemented. But indeed, we should take the time to fix it.

Ok, I understand ! Thank you very much for your reply. This would indeed be a nice to have !
And by the way, I read a lot of your answers on other topics. Always a pleasure to read your posts.

Hello,
This PR could solve your problems @Pierre-yves_Meyer if you still need the feature:

Amazing! Thanks Thibault for this, I’ll try it very soon.

I tryied it and it works perfectly well for my need. Thanks again !

1 Like

Hello all,

so after some days, I could dig further, tryied the new vtkSphereMapper example, but I’m struggling to understand something :

hwSelector.setFieldAssociation(FieldAssociations.FIELD_ASSOCIATION_CELLS);

Why is the field association set to cells and not FIELD_ASSOCIATION_POINTS ? If I set it to FIELD_ASSOCIATION_POINTS, attribute ID is undefined

Anyway, if I keep this, it seems that I can get the point ID that I picked like this :

const pointID = Math.floor(attributeID / 3);

It does the job, but why on earth ?

And I have some difficulties to understand compositeID attribute also, as it seems to be equal to 3 for all the actors in my scene.

I tryied to find some documentation I could read to understand this, but I couldn’t find any that helped me going further.

Any help or readings advice would be very nice,

Best regards

PS : I read and played with the hardware selector example, but It didn’t help me to find the answers to my questions unfortunately.

The PolyDataMapper creates a map called selectionWebGLIdsToVTKIds which maps an OpenGL vertex ID (gl_VertexID + VertexIDOffset) to a VTK cell ID or point ID.

The problem with the Sphere Mapper is that this map is null because it has its own buildBufferObjects function which doesn’t update selectionWebGLIdsToVTKIds as the PolyDataMapper does. If you look at how the VBO is built in the SphereMapper, you can see that each sphere takes 3 vertices. This is why your formula for computing pointID is correct.

If you want to do something cleaner, you could can add the code to update the selectionWebGLIdsToVTKIds in the SphereMapper to VTK.js and do a PR (see createVBO function to see how it is done in the polydatamapper).

Thanks Thibault for your insight, it’s much appreciated.

I’ll try to make something cleaner following your suggestion, but I’m in a rush right now and I don’t now when I’ll be able to do that. I’ll keep you updated.

For the moment, as you confirmed my formula works, I’ll work with this for now.

Thanks again and best regards