From 5ff9935b98e3998f01734b1aab164ece1298f7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Wed, 3 Apr 2024 16:28:02 +0200 Subject: [PATCH] docs(rw): wip examples --- Examples/Rendering/ManyRenderers/index.js | 131 +++------- .../Core/HardwareSelector/example/index.js | 230 ++++++++++++++---- 2 files changed, 219 insertions(+), 142 deletions(-) diff --git a/Examples/Rendering/ManyRenderers/index.js b/Examples/Rendering/ManyRenderers/index.js index 1b9e3cdc295..fdeeee2fb7e 100644 --- a/Examples/Rendering/ManyRenderers/index.js +++ b/Examples/Rendering/ManyRenderers/index.js @@ -95,45 +95,6 @@ rootContainer.style.top = 0; rootContainer.style.pointerEvents = 'none'; document.body.appendChild(rootContainer); -renderWindowView.setContainer(rootContainer); - -const interactor = vtkRenderWindowInteractor.newInstance(); -interactor.setView(renderWindowView); -interactor.initialize(); -interactor.setInteractorStyle(vtkInteractorStyleTrackballCamera.newInstance()); - -function updateViewPort(element, renderer) { - const { innerHeight, innerWidth } = window; - const { x, y, width, height } = element.getBoundingClientRect(); - const viewport = [ - x / innerWidth, - 1 - (y + height) / innerHeight, - (x + width) / innerWidth, - 1 - y / innerHeight, - ]; - renderer.setViewport(...viewport); -} - -function recomputeViewports() { - const rendererElems = document.querySelectorAll('.renderer'); - for (let i = 0; i < rendererElems.length; i++) { - const elem = rendererElems[i]; - const { id } = elem; - const renderer = RENDERERS[id]; - updateViewPort(elem, renderer); - } - renderWindow.render(); -} - -function resize() { - rootContainer.style.width = `${window.innerWidth}px`; - renderWindowView.setSize(window.innerWidth, window.innerHeight); - recomputeViewports(); -} - -new ResizeObserver(resize).observe(document.body); -document.addEventListener('scroll', recomputeViewports); - // ---------------------------------------------------------------------------- // Renderers // ---------------------------------------------------------------------------- @@ -144,46 +105,21 @@ let bgIndex = 0; let rendererId = 1; function applyStyle(element) { + const width = Math.floor(100 + Math.random() * 200); + const height = Math.floor(100 + Math.random() * 200); element.classList.add('renderer'); - element.style.width = '200px'; - element.style.height = '200px'; + element.style.width = `${width}px`; + element.style.height = `${height}px`; element.style.margin = '20px'; element.style.border = 'solid 1px #333'; - element.style.display = 'inline-block'; + element.style.display = 'inline-flex'; + element.style['align-items'] = 'center'; + element.style['flex-flow'] = 'column'; element.style.boxSizing = 'border'; - element.style.textAlign = 'center'; element.style.color = 'white'; return element; } -let captureCurrentRenderer = false; - -function setCaptureCurrentRenderer(yn) { - captureCurrentRenderer = yn; - if (yn && interactor.getCurrentRenderer()) { - // fix the current renderer to, well, the current renderer - interactor.setCurrentRenderer(interactor.getCurrentRenderer()); - } else { - // remove the fixed current renderer - interactor.setCurrentRenderer(null); - } -} - -function bindInteractor(renderer, el) { - // only change the interactor's container if needed - if (interactor.getContainer() !== el) { - if (interactor.getContainer()) { - interactor.unbindEvents(); - } - if (captureCurrentRenderer) { - interactor.setCurrentRenderer(renderer); - } - if (el) { - interactor.bindEvents(el); - } - } -} - function addRenderer() { const mesh = meshes[meshIndex]; const prop = properties[propertyIndex]; @@ -196,6 +132,11 @@ function addRenderer() { container.id = rendererId++; document.body.appendChild(container); + const nameDiv = document.createElement('div'); + nameDiv.innerHTML = `${mesh.name} ${prop.name}`; + nameDiv.style.position = 'absolute'; + container.appendChild(nameDiv); + const actor = vtkActor.newInstance(); actor.setMapper(mesh.mapper); actor.getProperty().set(prop.properties); @@ -204,16 +145,32 @@ function addRenderer() { actor.getProperty().setSpecularPower(30); actor.getProperty().setSpecularColor(1.0, 1.0, 1.0); const renderer = vtkRenderer.newInstance({ background }); - container.innerHTML = `${mesh.name} ${prop.name}`; - container.addEventListener('pointerenter', () => - bindInteractor(renderer, container) + renderer.addActor(actor); + // Create a new renderwindow + const childRenderWindow = vtkRenderWindow.newInstance(); + renderWindow.addRenderWindow(childRenderWindow); // add the child RW to its parent + renderWindowView.addMissingNode(childRenderWindow); // create the OpenGLRenderWindow for the child + const childrenViews = renderWindowView.getChildren(); + const childRenderWindowView = childrenViews[childrenViews.length - 1]; // get the child's OpenGLRenderWindow + + childRenderWindow.addRenderer(renderer); + childRenderWindowView.setContainer(container); + const containerBounds = container.getBoundingClientRect(); + const pixRatio = window.devicePixelRatio; + childRenderWindowView.setSize( + containerBounds.width * pixRatio, + containerBounds.height * pixRatio + ); + + const interactor = vtkRenderWindowInteractor.newInstance(); + interactor.setView(childRenderWindowView); + interactor.initialize(); + interactor.bindEvents(childRenderWindowView.getCanvas()); + interactor.setInteractorStyle( + vtkInteractorStyleTrackballCamera.newInstance() ); - container.addEventListener('pointerleave', () => bindInteractor(null, null)); - renderer.addActor(actor); - renderWindow.addRenderer(renderer); - updateViewPort(container, renderer); renderer.resetCamera(); // Keep track of renderer @@ -224,25 +181,9 @@ function addRenderer() { // Fill up page // ---------------------------------------------------------------------------- -const checkbox = document.createElement('input'); -checkbox.type = 'checkbox'; -checkbox.name = 'singleRendererCapture'; -const label = document.createElement('label'); -label.for = checkbox.name; -label.innerText = 'Enable single renderer capture'; - -checkbox.addEventListener('input', (ev) => { - setCaptureCurrentRenderer(ev.target.checked); -}); - -document.body.appendChild(checkbox); -document.body.appendChild(label); -document.body.appendChild(document.createElement('br')); - -for (let i = 0; i < 64; i++) { +for (let i = 0; i < 4; i++) { addRenderer(); } -resize(); function updateCamera(renderer) { const camera = renderer.getActiveCamera(); diff --git a/Sources/Rendering/Core/HardwareSelector/example/index.js b/Sources/Rendering/Core/HardwareSelector/example/index.js index 1e9de2ee29c..3397be980c7 100644 --- a/Sources/Rendering/Core/HardwareSelector/example/index.js +++ b/Sources/Rendering/Core/HardwareSelector/example/index.js @@ -6,13 +6,14 @@ 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/OpenGL/Glyph3DMapper'; +import '@kitware/vtk.js/Rendering/Misc/RenderingAPIs'; +import '@kitware/vtk.js/Rendering/Profiles/Volume'; import { throttle } from '@kitware/vtk.js/macros'; import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; import vtkConeSource from '@kitware/vtk.js/Filters/Sources/ConeSource'; 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 vtkGlyph3DMapper from '@kitware/vtk.js/Rendering/Core/Glyph3DMapper'; import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource'; @@ -23,6 +24,18 @@ 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 { Representation } from '@kitware/vtk.js/Rendering/Core/Property/Constants'; +import vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow'; +import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; +import vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWindowInteractor'; +import vtkInteractorStyleTrackballCamera from '@kitware/vtk.js/Interaction/Style/InteractorStyleTrackballCamera'; +import vtkHttpDataSetReader from '@kitware/vtk.js/IO/Core/HttpDataSetReader'; +import vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; +import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; + +// Force the loading of HttpDataAccessHelper to support gzip decompression +import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; // ---------------------------------------------------------------------------- // Constants @@ -53,7 +66,8 @@ tooltipsElem.appendChild(propIdTooltipElem); tooltipsElem.appendChild(fieldIdTooltipElem); tooltipsElem.appendChild(compositeIdTooltipElem); -document.querySelector('body').appendChild(tooltipsElem); +document.body.removeChild(document.querySelector('.content')); +document.body.appendChild(tooltipsElem); // ---------------------------------------------------------------------------- // Create 4 objects @@ -198,47 +212,158 @@ pointerMapper.setInputConnection(pointerSource.getOutputPort()); // Create rendering infrastructure // ---------------------------------------------------------------------------- -const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); -const renderer = fullScreenRenderer.getRenderer(); -const renderWindow = renderer.getRenderWindow(); -const interactor = renderWindow.getInteractor(); -const apiSpecificRenderWindow = interactor.getView(); +const bodyStyle = document.body.style; +bodyStyle.height = '100vh'; +bodyStyle.width = '100vw'; +bodyStyle.margin = '0'; +bodyStyle.display = 'flex'; +bodyStyle['justify-content'] = 'space-around'; +bodyStyle['align-items'] = 'center'; +bodyStyle['flex-wrap'] = 'wrap'; + +const background = [0.32, 0.34, 0.43]; + +const mainRenderWindow = vtkRenderWindow.newInstance(); +const mainView = mainRenderWindow.newAPISpecificView(); +mainRenderWindow.addView(mainView); + +function buildChildRenderWindow() { + // Create child render window and the corresponding view + const renderWindow = vtkRenderWindow.newInstance(); + mainRenderWindow.addRenderWindow(renderWindow); + mainView.addMissingNode(renderWindow); + const allViews = mainView.getChildren(); + const view = allViews[allViews.length - 1]; + + // Create container for the new render window + const container = document.createElement('div'); + container.style.height = `50vh`; + container.style.width = `50vw`; + document.body.appendChild(container); + view.setContainer(container); + + // Set size and css style + const containerBounds = container.getBoundingClientRect(); + view.setSize( + containerBounds.width * devicePixelRatio, + containerBounds.height * devicePixelRatio + ); + + // Create renderer + const renderer = vtkRenderer.newInstance({ background }); + renderWindow.addRenderer(renderer); + + // Create interactor + const interactor = vtkRenderWindowInteractor.newInstance(); + interactor.setView(view); + interactor.initialize(); + interactor.bindEvents(view.getCanvas()); + interactor.setInteractorStyle( + vtkInteractorStyleTrackballCamera.newInstance() + ); + + // Create hardware selector + const hardwareSelector = view.getSelector(); + hardwareSelector.setCaptureZValues(true); + // TODO: bug in FIELD_ASSOCIATION_POINTS mode + // hardwareSelector.setFieldAssociation( + // FieldAssociations.FIELD_ASSOCIATION_POINTS + // ); + hardwareSelector.setFieldAssociation( + FieldAssociations.FIELD_ASSOCIATION_CELLS + ); + + return { renderWindow, view, renderer, interactor, hardwareSelector }; +} -renderer.addActor(sphereActor); -renderer.addActor(cubeActor); -renderer.addActor(spherePointsActor); -renderer.addActor(coneActor); -renderer.addActor(cylinderActor); -renderer.addActor(pointerActor); -renderer.addActor(polyLines); -renderer.addActor(multiPrimitive); +// Initialize the main view before the first "render" that uses a child render windows +// You can alternatively render using the main render window (will render to all views) +// We initialize before building the child render windows because the interactor calls "render" on them +mainView.initialize(); -renderer.resetCamera(); -renderWindow.render(); +const renderingObjects = []; +for (let i = 0; i < 64; ++i) { + renderingObjects.push(buildChildRenderWindow()); +} -// ---------------------------------------------------------------------------- -// Create hardware selector -// ---------------------------------------------------------------------------- +// Resize the context, now that all the windows are set +mainView.resizeFromChildRenderWindows(); + +renderingObjects[0].renderer.addActor(sphereActor); +renderingObjects[0].renderer.addActor(cubeActor); +renderingObjects[0].renderer.addActor(spherePointsActor); +renderingObjects[0].renderer.addActor(coneActor); +renderingObjects[0].renderer.addActor(cylinderActor); -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); +renderingObjects[1].renderer.addActor(polyLines); +renderingObjects[1].renderer.addActor(multiPrimitive); + +renderingObjects.forEach(({ renderer, renderWindow }) => { + renderer.addActor(pointerActor); + renderer.resetCamera(); + renderWindow.render(); +}); + +async function loadVolume() { + const actor = vtkVolume.newInstance(); + const mapper = vtkVolumeMapper.newInstance(); + mapper.setSampleDistance(0.7); + mapper.setVolumetricScatteringBlending(0); + mapper.setLocalAmbientOcclusion(0); + mapper.setLAOKernelSize(10); + mapper.setLAOKernelRadius(5); + mapper.setComputeNormalFromOpacity(true); + actor.setMapper(mapper); + + const ctfun = vtkColorTransferFunction.newInstance(); + ctfun.addRGBPoint(0, 0, 0, 0); + ctfun.addRGBPoint(95, 1.0, 1.0, 1.0); + ctfun.addRGBPoint(225, 0.66, 0.66, 0.5); + ctfun.addRGBPoint(255, 0.3, 0.3, 0.5); + const ofun = vtkPiecewiseFunction.newInstance(); + ofun.addPoint(100.0, 0.0); + ofun.addPoint(255.0, 1.0); + actor.getProperty().setRGBTransferFunction(0, ctfun); + actor.getProperty().setScalarOpacity(0, ofun); + actor.getProperty().setInterpolationTypeToLinear(); + actor.getProperty().setShade(true); + actor.getProperty().setAmbient(0.3); + actor.getProperty().setDiffuse(1); + actor.getProperty().setSpecular(1); + actor.setScale(0.003, 0.003, 0.003); + actor.setPosition(1, 1, -1.1); + + const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true }); + await reader.setUrl(`${__BASE_PATH__}/data/volume/LIDC2.vti`); + await reader.loadData(); + const imageData = reader.getOutputData(); + + mapper.setInputData(imageData); + + renderingObjects.forEach(({ renderer, renderWindow }) => { + renderer.addVolume(actor); + renderer.resetCamera(); + renderWindow.render(); + }); +} + +loadVolume(); // ---------------------------------------------------------------------------- // Create Mouse listener for picking on mouse move // ---------------------------------------------------------------------------- -function eventToWindowXY(event) { +function eventToWindowXY(event, view) { // 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 + const canvas = view.getCanvas(); + const canvasRect = canvas.getBoundingClientRect(); + const normalizedX = (clientX - canvasRect.left) / canvasRect.width; + const normalizedY = (clientY - canvasRect.top) / canvasRect.height; + const [width, height] = view.getSize(); + const x = Math.round(width * normalizedX); + const y = Math.round(height * (1 - normalizedY)); // Need to flip Y return [x, y]; } @@ -278,7 +403,7 @@ const updateCompositeAndPropIdTooltip = (compositeID, propID) => { } }; -const updateCursor = (worldPosition) => { +const updateCursor = (worldPosition, renderWindow) => { if (lastProcessedActor) { pointerActor.setVisibility(true); pointerActor.setPosition(worldPosition); @@ -289,12 +414,14 @@ const updateCursor = (worldPosition) => { updatePositionTooltip(worldPosition); }; -function processSelections(selections) { - renderer.getActors().forEach((a) => a.getProperty().setColor(...WHITE)); +function processSelections(selections, renderingObject) { + renderingObject.renderer + .getActors() + .forEach((a) => a.getProperty().setColor(...WHITE)); if (!selections || selections.length === 0) { lastProcessedActor = null; updateAssociationTooltip(); - updateCursor(); + updateCursor(undefined, renderingObject.renderWindow); updateCompositeAndPropIdTooltip(); return; } @@ -327,7 +454,7 @@ function processSelections(selections) { worldToProp.apply(propPosition); if ( - hardwareSelector.getFieldAssociation() === + renderingObject.hardwareSelector.getFieldAssociation() === FieldAssociations.FIELD_ASSOCIATION_POINTS ) { // Selecting points @@ -357,7 +484,7 @@ function processSelections(selections) { } lastProcessedActor = prop; // Use closestCellPointWorldPosition or rayHitWorldPosition - updateCursor(closestCellPointWorldPosition); + updateCursor(closestCellPointWorldPosition, renderingObject.renderWindow); // Make the picked actor green prop.getProperty().setColor(...GREEN); @@ -373,26 +500,35 @@ function processSelections(selections) { scaleArray.fill(0.5); cylinderPointSet.modified(); } - renderWindow.render(); + renderingObject.renderWindow.render(); } // ---------------------------------------------------------------------------- function pickOnMouseEvent(event) { - if (interactor.isAnimating()) { + const renderingObject = renderingObjects.find(({ view }) => + view.getCanvas().matches(':hover') + ); + if (!renderingObject || renderingObject.interactor.isAnimating()) { // We should not do picking when interacting with the scene return; } - const [x, y] = eventToWindowXY(event); + + const [x, y] = eventToWindowXY(event, renderingObject.view); pointerActor.setVisibility(false); - hardwareSelector.getSourceDataAsync(renderer, x, y, x, y).then((result) => { - if (result) { - processSelections(result.generateSelection(x, y, x, y)); - } else { - processSelections(null); - } - }); + renderingObject.hardwareSelector + .getSourceDataAsync(renderingObject.renderer, x, y, x, y) + .then((result) => { + if (result) { + processSelections( + result.generateSelection(x, y, x, y), + renderingObject + ); + } else { + processSelections(null, renderingObject); + } + }); } const throttleMouseHandler = throttle(pickOnMouseEvent, 20);