From cd85bd8ef1dfe78bb1eb35015f414be0489f1f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Fri, 29 Mar 2024 19:07:03 +0100 Subject: [PATCH 01/10] refactor(registerOverride): Remove registerOverride from ViewNodeFactory public API This method changed the static Map `CLASS_MAPPING` of the corresponding backend. It was only used by the API specific render windows. Replace its usage with what is done everywhere else. --- Sources/Rendering/OpenGL/RenderWindow/index.js | 10 ++++++---- .../Rendering/SceneGraph/ViewNodeFactory/index.d.ts | 8 -------- Sources/Rendering/SceneGraph/ViewNodeFactory/index.js | 4 ---- Sources/Rendering/WebGPU/RenderWindow/index.js | 10 ++++++---- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.js b/Sources/Rendering/OpenGL/RenderWindow/index.js index 4494243a3a1..5d8f77ad8e0 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.js +++ b/Sources/Rendering/OpenGL/RenderWindow/index.js @@ -5,7 +5,9 @@ import vtkForwardPass from 'vtk.js/Sources/Rendering/OpenGL/ForwardPass'; import vtkOpenGLHardwareSelector from 'vtk.js/Sources/Rendering/OpenGL/HardwareSelector'; import vtkShaderCache from 'vtk.js/Sources/Rendering/OpenGL/ShaderCache'; import vtkOpenGLTextureUnitManager from 'vtk.js/Sources/Rendering/OpenGL/TextureUnitManager'; -import vtkOpenGLViewNodeFactory from 'vtk.js/Sources/Rendering/OpenGL/ViewNodeFactory'; +import vtkOpenGLViewNodeFactory, { + registerOverride, +} from 'vtk.js/Sources/Rendering/OpenGL/ViewNodeFactory'; import vtkRenderPass from 'vtk.js/Sources/Rendering/SceneGraph/RenderPass'; import vtkRenderWindowViewNode from 'vtk.js/Sources/Rendering/SceneGraph/RenderWindowViewNode'; import { createContextProxyHandler } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow/ContextProxy'; @@ -1206,9 +1208,6 @@ export function extend(publicAPI, model, initialValues = {}) { model._glInformation = null; model.myFactory = vtkOpenGLViewNodeFactory.newInstance(); - /* eslint-disable no-use-before-define */ - model.myFactory.registerOverride('vtkRenderWindow', newInstance); - /* eslint-enable no-use-before-define */ model.shaderCache = vtkShaderCache.newInstance(); model.shaderCache.setOpenGLRenderWindow(publicAPI); @@ -1263,3 +1262,6 @@ export default { pushMonitorGLContextCount, popMonitorGLContextCount, }; + +// Register ourself to OpenGL backend if imported +registerOverride('vtkRenderWindow', newInstance); diff --git a/Sources/Rendering/SceneGraph/ViewNodeFactory/index.d.ts b/Sources/Rendering/SceneGraph/ViewNodeFactory/index.d.ts index 6c075e5bb6d..242e89994eb 100644 --- a/Sources/Rendering/SceneGraph/ViewNodeFactory/index.d.ts +++ b/Sources/Rendering/SceneGraph/ViewNodeFactory/index.d.ts @@ -12,14 +12,6 @@ export interface vtkViewNodeFactory extends vtkObject { * @param dataObject */ createNode(dataObject: any): void; - - /** - * Give a function pointer to a class that will manufacture a vtkViewNode - * when given a class name string. - * @param className - * @param func - */ - registerOverride(className: any, func: any): void; } /** diff --git a/Sources/Rendering/SceneGraph/ViewNodeFactory/index.js b/Sources/Rendering/SceneGraph/ViewNodeFactory/index.js index 06b1497e390..d36b128a38e 100644 --- a/Sources/Rendering/SceneGraph/ViewNodeFactory/index.js +++ b/Sources/Rendering/SceneGraph/ViewNodeFactory/index.js @@ -37,10 +37,6 @@ function vtkViewNodeFactory(publicAPI, model) { vn.setMyFactory(publicAPI); return vn; }; - - publicAPI.registerOverride = (className, func) => { - model.overrides[className] = func; - }; } // ---------------------------------------------------------------------------- diff --git a/Sources/Rendering/WebGPU/RenderWindow/index.js b/Sources/Rendering/WebGPU/RenderWindow/index.js index 92da7b2ca8c..5908e81f299 100644 --- a/Sources/Rendering/WebGPU/RenderWindow/index.js +++ b/Sources/Rendering/WebGPU/RenderWindow/index.js @@ -4,7 +4,9 @@ import vtkForwardPass from 'vtk.js/Sources/Rendering/WebGPU/ForwardPass'; import vtkWebGPUBuffer from 'vtk.js/Sources/Rendering/WebGPU/Buffer'; import vtkWebGPUDevice from 'vtk.js/Sources/Rendering/WebGPU/Device'; import vtkWebGPUHardwareSelector from 'vtk.js/Sources/Rendering/WebGPU/HardwareSelector'; -import vtkWebGPUViewNodeFactory from 'vtk.js/Sources/Rendering/WebGPU/ViewNodeFactory'; +import vtkWebGPUViewNodeFactory, { + registerOverride, +} from 'vtk.js/Sources/Rendering/WebGPU/ViewNodeFactory'; import vtkRenderPass from 'vtk.js/Sources/Rendering/SceneGraph/RenderPass'; import vtkRenderWindowViewNode from 'vtk.js/Sources/Rendering/SceneGraph/RenderWindowViewNode'; import HalfFloat from 'vtk.js/Sources/Common/Core/HalfFloat'; @@ -600,9 +602,6 @@ export function extend(publicAPI, model, initialValues = {}) { vtkRenderWindowViewNode.extend(publicAPI, model, initialValues); model.myFactory = vtkWebGPUViewNodeFactory.newInstance(); - /* eslint-disable no-use-before-define */ - model.myFactory.registerOverride('vtkRenderWindow', newInstance); - /* eslint-enable no-use-before-define */ // setup default forward pass rendering model.renderPasses[0] = vtkForwardPass.newInstance(); @@ -658,3 +657,6 @@ export default { newInstance, extend, }; + +// Register ourself to WebGPU backend if imported +registerOverride('vtkRenderWindow', newInstance); From eb0d2dbf6d32a118e9362cabb1fe951b60ca2b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Wed, 3 Apr 2024 16:13:22 +0200 Subject: [PATCH 02/10] feat(viewNode): Add getLastAncestorOfType method --- Sources/Rendering/SceneGraph/ViewNode/index.d.ts | 6 ++++++ Sources/Rendering/SceneGraph/ViewNode/index.js | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Sources/Rendering/SceneGraph/ViewNode/index.d.ts b/Sources/Rendering/SceneGraph/ViewNode/index.d.ts index c3323ffaa1b..57c3c049015 100644 --- a/Sources/Rendering/SceneGraph/ViewNode/index.d.ts +++ b/Sources/Rendering/SceneGraph/ViewNode/index.d.ts @@ -66,6 +66,12 @@ export interface vtkViewNode extends vtkObject { */ getFirstAncestorOfType(type: any): void; + /** + * Find the last parent/grandparent of the desired type + * @param type + */ + getLastAncestorOfType(type: any): void; + /** * */ diff --git a/Sources/Rendering/SceneGraph/ViewNode/index.js b/Sources/Rendering/SceneGraph/ViewNode/index.js index 39da1abe6b3..5a482219a4e 100644 --- a/Sources/Rendering/SceneGraph/ViewNode/index.js +++ b/Sources/Rendering/SceneGraph/ViewNode/index.js @@ -70,6 +70,20 @@ function vtkViewNode(publicAPI, model) { return model._parent.getFirstAncestorOfType(type); }; + publicAPI.getLastAncestorOfType = (type) => { + if (!model._parent) { + return null; + } + const lastAncestor = model._parent.getLastAncestorOfType(type); + if (lastAncestor) { + return lastAncestor; + } + if (model._parent.isA(type)) { + return model._parent; + } + return null; + }; + // add a missing node/child for the passed in renderables. This should // be called only in between prepareNodes and removeUnusedNodes publicAPI.addMissingNode = (dobj) => { From 2fadb438e7b6486300924bc48f064a02f082261a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Wed, 3 Apr 2024 16:14:57 +0200 Subject: [PATCH 03/10] fix(volumeMapper): Remove variable that prevented caching scalarTextureString didn't have a purpose and prevented caching of GPU resources --- Sources/Rendering/OpenGL/VolumeMapper/index.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Sources/Rendering/OpenGL/VolumeMapper/index.js b/Sources/Rendering/OpenGL/VolumeMapper/index.js index 72314d12f64..eda711a0957 100644 --- a/Sources/Rendering/OpenGL/VolumeMapper/index.js +++ b/Sources/Rendering/OpenGL/VolumeMapper/index.js @@ -1663,10 +1663,7 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { const tex = model._openGLRenderWindow.getGraphicsResourceForObject(scalars); // rebuild the scalarTexture if the data has changed toString = `${image.getMTime()}A${scalars.getMTime()}`; - const reBuildTex = - !tex?.vtkObj || - tex?.hash !== toString || - model.scalarTextureString !== toString; + const reBuildTex = !tex?.vtkObj || tex?.hash !== toString; if (reBuildTex) { // Build the textures const dims = image.getDimensions(); @@ -1683,17 +1680,15 @@ function vtkOpenGLVolumeMapper(publicAPI, model) { scalars, model.renderable.getPreferSizeOverAccuracy() ); - model.scalarTextureString = toString; if (scalars) { model._openGLRenderWindow.setGraphicsResourceForObject( scalars, model.scalarTexture, - model.scalarTextureString + toString ); } } else { model.scalarTexture = tex.vtkObj; - model.scalarTextureString = tex.hash; } if (!model.tris.getCABO().getElementCount()) { From 3e88b2df9e1345a98623642b277f070874cefdf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Wed, 3 Apr 2024 16:15:36 +0200 Subject: [PATCH 04/10] refactor(oglRenderer): Small refactor --- Sources/Rendering/OpenGL/Renderer/index.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Sources/Rendering/OpenGL/Renderer/index.js b/Sources/Rendering/OpenGL/Renderer/index.js index c08b0d72890..aef9b98fe9a 100644 --- a/Sources/Rendering/OpenGL/Renderer/index.js +++ b/Sources/Rendering/OpenGL/Renderer/index.js @@ -137,19 +137,14 @@ function vtkOpenGLRenderer(publicAPI, model) { if (!model.renderable.getTransparent()) { const background = model.renderable.getBackgroundByReference(); // renderable ensures that background has 4 entries. - model.context.clearColor( - background[0], - background[1], - background[2], - background[3] - ); + gl.clearColor(background[0], background[1], background[2], background[3]); clearMask |= gl.COLOR_BUFFER_BIT; } if (!model.renderable.getPreserveDepthBuffer()) { gl.clearDepth(1.0); clearMask |= gl.DEPTH_BUFFER_BIT; - model.context.depthMask(true); + gl.depthMask(true); } gl.colorMask(true, true, true, true); From f3d39100cda13d02e3b29c011a04ef519f66d9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Wed, 3 Apr 2024 16:18:09 +0200 Subject: [PATCH 05/10] fix(forwardPass): Only get renderers, not child render windows A new feature will add the ability to use a render window as child of another render window This change prevents using a render window where a renderer is needed --- Sources/Rendering/OpenGL/ForwardPass/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Rendering/OpenGL/ForwardPass/index.js b/Sources/Rendering/OpenGL/ForwardPass/index.js index 3dfb7028e4e..c6570217ffc 100644 --- a/Sources/Rendering/OpenGL/ForwardPass/index.js +++ b/Sources/Rendering/OpenGL/ForwardPass/index.js @@ -28,11 +28,11 @@ function vtkForwardPass(publicAPI, model) { const numlayers = viewNode.getRenderable().getNumberOfLayers(); // iterate over renderers - const renderers = viewNode.getChildren(); + const renderers = viewNode.getRenderable().getRenderersByReference(); for (let i = 0; i < numlayers; i++) { for (let index = 0; index < renderers.length; index++) { - const renNode = renderers[index]; - const ren = viewNode.getRenderable().getRenderers()[index]; + const ren = renderers[index]; + const renNode = viewNode.getViewNodeFor(ren); if (ren.getDraw() && ren.getLayer() === i) { // check for both opaque and volume actors From c0854e371493fc51d45329f5a73e6e47496f36dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Mon, 8 Apr 2024 18:02:00 +0200 Subject: [PATCH 06/10] feat(ViewNode): Return the node in `addMissingNode` --- .../Rendering/SceneGraph/ViewNode/index.d.ts | 6 ++- .../Rendering/SceneGraph/ViewNode/index.js | 42 ++++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/Sources/Rendering/SceneGraph/ViewNode/index.d.ts b/Sources/Rendering/SceneGraph/ViewNode/index.d.ts index 57c3c049015..256a67b4090 100644 --- a/Sources/Rendering/SceneGraph/ViewNode/index.d.ts +++ b/Sources/Rendering/SceneGraph/ViewNode/index.d.ts @@ -20,10 +20,12 @@ export interface IViewNodeInitialValues { export interface vtkViewNode extends vtkObject { /** - * + * Add a child view node to this node, created from the renderable given as argument + * If the node creation fails or the argument is falsy, returns undefined + * Otherwise, returns the newly created node or the existing node * @param dobj */ - addMissingNode(dobj: any): void; + addMissingNode(dobj: any): vtkViewNode | undefined; /** * diff --git a/Sources/Rendering/SceneGraph/ViewNode/index.js b/Sources/Rendering/SceneGraph/ViewNode/index.js index 5a482219a4e..c1ac0198c5b 100644 --- a/Sources/Rendering/SceneGraph/ViewNode/index.js +++ b/Sources/Rendering/SceneGraph/ViewNode/index.js @@ -88,22 +88,27 @@ function vtkViewNode(publicAPI, model) { // be called only in between prepareNodes and removeUnusedNodes publicAPI.addMissingNode = (dobj) => { if (!dobj) { - return; + return undefined; } - const result = model._renderableChildMap.get(dobj); + // if found just mark as visited + const result = model._renderableChildMap.get(dobj); if (result !== undefined) { result.setVisited(true); - } else { - // otherwise create a node - const newNode = publicAPI.createViewNode(dobj); - if (newNode) { - newNode.setParent(publicAPI); - newNode.setVisited(true); - model._renderableChildMap.set(dobj, newNode); - model.children.push(newNode); - } + return result; + } + + // otherwise create a node + const newNode = publicAPI.createViewNode(dobj); + if (newNode) { + newNode.setParent(publicAPI); + newNode.setVisited(true); + model._renderableChildMap.set(dobj, newNode); + model.children.push(newNode); + return newNode; } + + return undefined; }; // add missing nodes/children for the passed in renderables. This should @@ -115,20 +120,7 @@ function vtkViewNode(publicAPI, model) { for (let index = 0; index < dataObjs.length; ++index) { const dobj = dataObjs[index]; - const result = model._renderableChildMap.get(dobj); - // if found just mark as visited - if (result !== undefined) { - result.setVisited(true); - } else { - // otherwise create a node - const newNode = publicAPI.createViewNode(dobj); - if (newNode) { - newNode.setParent(publicAPI); - newNode.setVisited(true); - model._renderableChildMap.set(dobj, newNode); - model.children.push(newNode); - } - } + publicAPI.addMissingNode(dobj); } }; From 8fb9f4b7af3525064c23643fe891388fba5b1325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Mon, 8 Apr 2024 18:35:42 +0200 Subject: [PATCH 07/10] feat(RenderWindow): Add sharing of GPU resources accross render windows To share resources accross render windows, they need to share a common context To do so, the render windows can now have a parent that will contain the shared context The child render windows will proxy some methods of their parent to do the rendering The content of the parent canvas is then copied to the child canvas using a 2D context --- .../Rendering/Core/RenderWindow/index.d.ts | 17 ++ Sources/Rendering/Core/RenderWindow/index.js | 12 +- .../Rendering/OpenGL/RenderWindow/index.d.ts | 47 ++++- .../Rendering/OpenGL/RenderWindow/index.js | 168 +++++++++++++++--- 4 files changed, 211 insertions(+), 33 deletions(-) diff --git a/Sources/Rendering/Core/RenderWindow/index.d.ts b/Sources/Rendering/Core/RenderWindow/index.d.ts index 08831072237..aa916ccf460 100755 --- a/Sources/Rendering/Core/RenderWindow/index.d.ts +++ b/Sources/Rendering/Core/RenderWindow/index.d.ts @@ -9,6 +9,7 @@ export interface IRenderWindowInitialValues { interactor?: any, neverRendered?: boolean, numberOfLayers?: number + childRenderWindows?: vtkRenderWindow[], } interface IStatistics { @@ -42,6 +43,12 @@ export interface vtkRenderWindow extends vtkObject { */ addRenderer(renderer: vtkRenderer): void; + /** + * Add a child render window + * @param {vtkRenderWindow} renderWindow The vtkRenderWindow instance. + */ + addRenderWindow(renderWindow: vtkRenderWindow): void; + /** * Add renderer * @param view @@ -87,6 +94,16 @@ export interface vtkRenderWindow extends vtkObject { */ getRenderersByReference(): vtkRenderer[]; + /** + * + */ + getChildRenderWindows(): vtkRenderWindow[]; + + /** + * + */ + getChildRenderWindowsByReference(): vtkRenderWindow[]; + /** * */ diff --git a/Sources/Rendering/Core/RenderWindow/index.js b/Sources/Rendering/Core/RenderWindow/index.js index b6a6e87eb3b..f47fc7c0c36 100644 --- a/Sources/Rendering/Core/RenderWindow/index.js +++ b/Sources/Rendering/Core/RenderWindow/index.js @@ -143,6 +143,15 @@ function vtkRenderWindow(publicAPI, model) { ) .filter((i) => !!i); }; + + publicAPI.addRenderWindow = (child) => { + if (model.childRenderWindows.includes(child)) { + return false; + } + model.childRenderWindows.push(child); + publicAPI.modified(); + return true; + }; } // ---------------------------------------------------------------------------- @@ -156,6 +165,7 @@ const DEFAULT_VALUES = { interactor: null, neverRendered: true, numberOfLayers: 1, + childRenderWindows: [], }; // ---------------------------------------------------------------------------- @@ -173,7 +183,7 @@ export function extend(publicAPI, model, initialValues = {}) { 'defaultViewAPI', ]); macro.get(publicAPI, model, ['neverRendered']); - macro.getArray(publicAPI, model, ['renderers']); + macro.getArray(publicAPI, model, ['renderers', 'childRenderWindows']); macro.moveToProtected(publicAPI, model, ['views']); macro.event(publicAPI, model, 'completion'); diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.d.ts b/Sources/Rendering/OpenGL/RenderWindow/index.d.ts index c63b8c812bb..4c4f7859616 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.d.ts +++ b/Sources/Rendering/OpenGL/RenderWindow/index.d.ts @@ -18,6 +18,7 @@ export interface IOpenGLRenderWindowInitialValues { shaderCache?: null; initialized?: boolean; context?: WebGLRenderingContext | WebGL2RenderingContext; + context2D?: CanvasRenderingContext2D; canvas?: HTMLCanvasElement; cursorVisibility?: boolean; cursor?: string; @@ -40,13 +41,6 @@ export interface ICaptureOptions { scale?: number } -export interface I3DContextOptions { - preserveDrawingBuffer?: boolean; - depth?: boolean; - alpha?: boolean; - powerPreference?: string; -} - type vtkOpenGLRenderWindowBase = vtkObject & Omit; + + /** + * + * @param {CanvasRenderingContext2DSettings} options + */ + get2DContext(options: CanvasRenderingContext2DSettings): Nullable; + + /** + * Copy the content of the root parent, if there is one, to the canvas + */ + copyParentContent(): void; + + /** + * Resize this render window using the size of its children + * The new size of the renderwindow is the size of the bounding box + * containing all the child render windows + */ + resizeFromChildRenderWindows(): void; + + /** + * Returns the last ancestor of type vtkOpenGLRenderWindow if there is one + * If there is no parent vtkOpenGLRenderWindow, returns undefined + */ + getRootOpenGLRenderWindow(): vtkOpenGLRenderWindow | undefined; + + /** + * The context 2D is created during initialization instead of the WebGL context + * when there is a parent render window + */ + getContext2D(): CanvasRenderingContext2D | undefined; + + /** + * */ - get3DContext(options: I3DContextOptions): Nullable; + setContext2D(context2D: CanvasRenderingContext2D | undefined): boolean; /** * diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.js b/Sources/Rendering/OpenGL/RenderWindow/index.js index 5d8f77ad8e0..5aa633cfa8b 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.js +++ b/Sources/Rendering/OpenGL/RenderWindow/index.js @@ -22,6 +22,36 @@ const SCREENSHOT_PLACEHOLDER = { height: '100%', }; +const rootParentMethodsToProxy = [ + 'activateTexture', + 'deactivateTexture', + 'disableCullFace', + 'enableCullFace', + 'get3DContext', + 'getActiveFramebuffer', + 'getContext', + 'getDefaultTextureByteSize', + 'getDefaultTextureInternalFormat', + 'getDefaultToWebgl2', + 'getGLInformations', + 'getGraphicsMemoryInfo', + 'getGraphicsResourceForObject', + 'getHardwareMaximumLineWidth', + 'getPixelData', + 'getShaderCache', + 'getTextureUnitForTexture', + 'getTextureUnitManager', + 'getWebgl2', + 'makeCurrent', + 'releaseGraphicsResources', + 'releaseGraphicsResourcesForObject', + 'restoreContext', + 'setActiveFramebuffer', + 'setContext', + 'setDefaultToWebgl2', + 'setGraphicsResourceForObject', +]; + function checkRenderTargetSupport(gl, format, type) { // create temporary frame buffer and texture const framebuffer = gl.createFramebuffer(); @@ -86,7 +116,14 @@ function vtkOpenGLRenderWindow(publicAPI, model) { // Set our className model.classHierarchy.push('vtkOpenGLRenderWindow'); - const cachingContextHandler = createContextProxyHandler(); + // Only create a cachingContextHandler if needed + let cachingContextHandler; + function getCachingContextHandler() { + if (!cachingContextHandler) { + cachingContextHandler = createContextProxyHandler(); + } + return cachingContextHandler; + } publicAPI.getViewNodeFactory = () => model.myFactory; @@ -143,31 +180,53 @@ function vtkOpenGLRenderWindow(publicAPI, model) { publicAPI.prepareNodes(); publicAPI.addMissingNodes(model.renderable.getRenderersByReference()); + publicAPI.addMissingNodes( + model.renderable.getChildRenderWindowsByReference() + ); publicAPI.removeUnusedNodes(); publicAPI.initialize(); + model.children.forEach((child) => { - child.setOpenGLRenderWindow(publicAPI); + // Children can be openGl renderer or openGl render windows + // Only openGl renderers have a method setOpenGLRenderWindow + child.setOpenGLRenderWindow?.(publicAPI); }); } }; publicAPI.initialize = () => { if (!model.initialized) { - model.context = publicAPI.get3DContext(); - model.textureUnitManager = vtkOpenGLTextureUnitManager.newInstance(); - model.textureUnitManager.setContext(model.context); - model.shaderCache.setContext(model.context); - // initialize blending for transparency - const gl = model.context; - gl.blendFuncSeparate( - gl.SRC_ALPHA, - gl.ONE_MINUS_SRC_ALPHA, - gl.ONE, - gl.ONE_MINUS_SRC_ALPHA + // Set root parent if there is one + // Some methods of the root parent are proxied (see rootParentMethodsToProxy) + model.rootOpenGLRenderWindow = publicAPI.getLastAncestorOfType( + 'vtkOpenGLRenderWindow' ); - gl.depthFunc(gl.LEQUAL); - gl.enable(gl.BLEND); + + if (model.rootOpenGLRenderWindow) { + // Initialize a 2D context that will copy the content of the root parent + model.context2D = publicAPI.get2DContext(); + } else { + // Initialize a 3D context that may be used by child render windows + model.context = publicAPI.get3DContext(); + publicAPI.resizeFromChildRenderWindows(); + if (model.context) { + createGLContext(); + } + model.textureUnitManager = vtkOpenGLTextureUnitManager.newInstance(); + model.textureUnitManager.setContext(model.context); + model.shaderCache.setContext(model.context); + // initialize blending for transparency + const gl = model.context; + gl.blendFuncSeparate( + gl.SRC_ALPHA, + gl.ONE_MINUS_SRC_ALPHA, + gl.ONE, + gl.ONE_MINUS_SRC_ALPHA + ); + gl.depthFunc(gl.LEQUAL); + gl.enable(gl.BLEND); + } model.initialized = true; } }; @@ -264,9 +323,12 @@ function vtkOpenGLRenderWindow(publicAPI, model) { model.canvas.getContext('experimental-webgl', options); } - return new Proxy(result, cachingContextHandler); + return new Proxy(result, getCachingContextHandler()); }; + publicAPI.get2DContext = (options = {}) => + model.canvas.getContext('2d', options); + publicAPI.restoreContext = () => { const rp = vtkRenderPass.newInstance(); rp.setCurrentOperation('Release'); @@ -1015,6 +1077,47 @@ function vtkOpenGLRenderWindow(publicAPI, model) { if (model.notifyStartCaptureImage) { getCanvasDataURL(); } + publicAPI.copyParentContent(); + const childrenRW = model.renderable.getChildRenderWindowsByReference(); + for (let i = 0; i < childrenRW.length; ++i) { + publicAPI.getViewNodeFor(childrenRW[i])?.traverseAllPasses(); + } + }; + + publicAPI.copyParentContent = () => { + const rootParent = model.rootOpenGLRenderWindow; + if (!rootParent || !model.context2D) { + return; + } + const parentCanvas = rootParent.getCanvas(); + const selfCanvas = model.canvas; + model.context2D.drawImage( + parentCanvas, + 0, + parentCanvas.height - selfCanvas.height, // source y axis is inverted + selfCanvas.width, + selfCanvas.height, + 0, + 0, + selfCanvas.width, + selfCanvas.height + ); + }; + + publicAPI.resizeFromChildRenderWindows = () => { + // Adapt the size of the parent render window to the child render windows + const childrenRW = model.renderable.getChildRenderWindowsByReference(); + if (childrenRW.length > 0) { + const maxSize = [0, 0]; + for (let i = 0; i < childrenRW.length; ++i) { + const childSize = publicAPI.getViewNodeFor(childrenRW[i])?.getSize(); + if (childSize) { + maxSize[0] = childSize[0] > maxSize[0] ? childSize[0] : maxSize[0]; + maxSize[1] = childSize[1] > maxSize[1] ? childSize[1] : maxSize[1]; + } + } + publicAPI.setSize(...maxSize); + } }; publicAPI.disableCullFace = () => { @@ -1076,10 +1179,14 @@ function vtkOpenGLRenderWindow(publicAPI, model) { } publicAPI.delete = macro.chain( + () => { + if (model.context) { + deleteGLContext(); + } + }, clearEvents, publicAPI.delete, - publicAPI.setViewStream, - deleteGLContext + publicAPI.setViewStream ); // Do not trigger modified for performance reasons @@ -1148,6 +1255,18 @@ function vtkOpenGLRenderWindow(publicAPI, model) { glRen?.releaseGraphicsResources(); }); }; + + // Proxy some methods if needed + const publicAPIBeforeProxy = { ...publicAPI }; + rootParentMethodsToProxy.forEach((methodName) => { + publicAPI[methodName] = (...args) => { + if (model.rootOpenGLRenderWindow) { + // Proxy only methods when the render window has a parent + return model.rootOpenGLRenderWindow[methodName](...args); + } + return publicAPIBeforeProxy[methodName](...args); + }; + }); } // ---------------------------------------------------------------------------- @@ -1159,6 +1278,7 @@ const DEFAULT_VALUES = { shaderCache: null, initialized: false, context: null, + context2D: null, canvas: null, cursorVisibility: true, cursor: 'pointer', @@ -1184,9 +1304,10 @@ export function extend(publicAPI, model, initialValues = {}) { vtkRenderWindowViewNode.extend(publicAPI, model, initialValues); // Create internal instances - model.canvas = document.createElement('canvas'); - model.canvas.style.width = '100%'; - createGLContext(); + if (!model.canvas) { + model.canvas = document.createElement('canvas'); + model.canvas.style.width = '100%'; + } if (!model.selector) { model.selector = vtkOpenGLHardwareSelector.newInstance(); @@ -1215,8 +1336,6 @@ export function extend(publicAPI, model, initialValues = {}) { // setup default forward pass rendering model.renderPasses[0] = vtkForwardPass.newInstance(); - macro.event(publicAPI, model, 'imageReady'); - // Build VTK API macro.get(publicAPI, model, [ 'shaderCache', @@ -1224,11 +1343,13 @@ export function extend(publicAPI, model, initialValues = {}) { 'webgl2', 'useBackgroundImage', 'activeFramebuffer', + 'rootOpenGLRenderWindow', ]); macro.setGet(publicAPI, model, [ 'initialized', 'context', + 'context2D', 'canvas', 'renderPasses', 'notifyStartCaptureImage', @@ -1238,6 +1359,7 @@ export function extend(publicAPI, model, initialValues = {}) { ]); macro.setGetArray(publicAPI, model, ['size'], 2); + macro.event(publicAPI, model, 'imageReady'); macro.event(publicAPI, model, 'windowResizeEvent'); // Object methods From 106a12a93532c053af4f4fd5d87cefb33a10a184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Mon, 8 Apr 2024 19:43:33 +0200 Subject: [PATCH 08/10] docs(RenderWindow): Add an example for using a parent render window --- Examples/Rendering/ManyRenderWindows/index.js | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 Examples/Rendering/ManyRenderWindows/index.js diff --git a/Examples/Rendering/ManyRenderWindows/index.js b/Examples/Rendering/ManyRenderWindows/index.js new file mode 100644 index 00000000000..46d2fbf1d56 --- /dev/null +++ b/Examples/Rendering/ManyRenderWindows/index.js @@ -0,0 +1,166 @@ +import '@kitware/vtk.js/favicon'; + +// Load the rendering pieces we want to use (for both WebGL and WebGPU) +import '@kitware/vtk.js/Rendering/Misc/RenderingAPIs'; +import '@kitware/vtk.js/Rendering/Profiles/Volume'; + +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import vtkHttpDataSetReader from '@kitware/vtk.js/IO/Core/HttpDataSetReader'; +import vtkInteractorStyleTrackballCamera from '@kitware/vtk.js/Interaction/Style/InteractorStyleTrackballCamera'; +import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; +import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; +import vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow'; +import vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWindowInteractor'; +import vtkVolume from '@kitware/vtk.js/Rendering/Core/Volume'; +import vtkVolumeMapper from '@kitware/vtk.js/Rendering/Core/VolumeMapper'; + +// Force the loading of HttpDataAccessHelper to support gzip decompression +import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; + +// ---------------------------------------------------------------------------- +// Background colors +// ---------------------------------------------------------------------------- + +async function createActor() { + 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); + + return actor; +} + +const mainRenderWindow = vtkRenderWindow.newInstance(); +const mainRenderWindowView = mainRenderWindow.newAPISpecificView(); +mainRenderWindow.addView(mainRenderWindowView); + +const rootContainer = document.createElement('div'); +rootContainer.style.display = 'flex'; +rootContainer.style['align-items'] = 'center'; +rootContainer.style['justify-content'] = 'space-between'; +rootContainer.style['flex-wrap'] = 'wrap'; +document.body.appendChild(rootContainer); + +function applyStyle(element) { + const width = Math.floor(200 + Math.random() * 200); + const height = Math.floor(200 + Math.random() * 200); + element.style.width = `${width}px`; + element.style.height = `${height}px`; + element.style.margin = '20px'; + element.style.border = 'solid 2px #333'; + return element; +} + +function addRenderWindow() { + // Create a child renderwindow + const renderWindow = vtkRenderWindow.newInstance(); + mainRenderWindow.addRenderWindow(renderWindow); + + // Create the corresponding view + const renderWindowView = mainRenderWindowView.addMissingNode(renderWindow); + + // Create a container for the view + const container = applyStyle(document.createElement('div')); + rootContainer.appendChild(container); + renderWindowView.setContainer(container); + + // Resize the view to the size of the container + // Ideally, we would use a resize observer to resize the view and + // call resizeFromChildRenderWindows on the main view when the container is resized + const containerBounds = container.getBoundingClientRect(); + const pixRatio = window.devicePixelRatio; + const dimensions = [ + containerBounds.width * pixRatio, + containerBounds.height * pixRatio, + ]; + renderWindowView.setSize(...dimensions); + const canvas = renderWindowView.getCanvas(); + canvas.style.width = `${container.clientWidth}px`; + canvas.style.height = `${container.clientHeight}px`; + + // Add an interactor to the view + const interactor = vtkRenderWindowInteractor.newInstance(); + interactor.setView(renderWindowView); + interactor.initialize(); + interactor.bindEvents(canvas); + interactor.setInteractorStyle( + vtkInteractorStyleTrackballCamera.newInstance() + ); + + return renderWindow; +} + +// ---------------------------------------------------------------------------- +// Fill up page +// ---------------------------------------------------------------------------- + +const childRenderWindows = []; +createActor().then((actor) => { + // Main view has to be initialized before the first "render" from a child render window + // We initialize before creating the child render windows because the interactor initialization calls "render" on them + mainRenderWindowView.initialize(); + + for (let i = 0; i < 64; i++) { + const childRenderWindow = addRenderWindow(); + + // Create the corresponding renderer + const background = [ + 0.5 * Math.random() + 0.25, + 0.5 * Math.random() + 0.25, + 0.5 * Math.random() + 0.25, + ]; + const renderer = vtkRenderer.newInstance({ background }); + childRenderWindow.addRenderer(renderer); + + // Add the actor and reset camera + renderer.addActor(actor); + const camera = renderer.getActiveCamera(); + camera.yaw(90); + camera.roll(90); + camera.azimuth(Math.random() * 360); + renderer.resetCamera(); + + childRenderWindows.push(childRenderWindow); + } + + mainRenderWindowView.resizeFromChildRenderWindows(); + mainRenderWindow.render(); +}); + +// ---------------------------------------------------------------------------- +// Globals +// ---------------------------------------------------------------------------- + +global.rw = mainRenderWindow; +global.glrw = mainRenderWindowView; +global.childRw = childRenderWindows; From bb619f69c3ba54b5b4daad9dae8a448c80265014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Tue, 9 Apr 2024 14:14:32 +0200 Subject: [PATCH 09/10] fix(ViewNode): Register child in renderableChildMap when calling addMissingChildren --- Sources/Rendering/SceneGraph/ViewNode/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/Rendering/SceneGraph/ViewNode/index.js b/Sources/Rendering/SceneGraph/ViewNode/index.js index c1ac0198c5b..1ac64ab03b6 100644 --- a/Sources/Rendering/SceneGraph/ViewNode/index.js +++ b/Sources/Rendering/SceneGraph/ViewNode/index.js @@ -139,6 +139,10 @@ function vtkViewNode(publicAPI, model) { if (cindex === -1) { child.setParent(publicAPI); model.children.push(child); + const childRenderable = child.getRenderable(); + if (childRenderable) { + model._renderableChildMap.set(childRenderable, child); + } } child.setVisited(true); } From b3ac42729e18f79b7444bd118b49593c9c1afe6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Bruy=C3=A8re?= Date: Tue, 9 Apr 2024 14:22:12 +0200 Subject: [PATCH 10/10] refactor(RenderWindow): Rename rootParentMethodsToProxy --- Sources/Rendering/OpenGL/RenderWindow/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.js b/Sources/Rendering/OpenGL/RenderWindow/index.js index 5aa633cfa8b..d2b41eacd10 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.js +++ b/Sources/Rendering/OpenGL/RenderWindow/index.js @@ -22,7 +22,7 @@ const SCREENSHOT_PLACEHOLDER = { height: '100%', }; -const rootParentMethodsToProxy = [ +const parentMethodsToProxy = [ 'activateTexture', 'deactivateTexture', 'disableCullFace', @@ -198,7 +198,7 @@ function vtkOpenGLRenderWindow(publicAPI, model) { publicAPI.initialize = () => { if (!model.initialized) { // Set root parent if there is one - // Some methods of the root parent are proxied (see rootParentMethodsToProxy) + // Some methods of the root parent are proxied (see parentMethodsToProxy) model.rootOpenGLRenderWindow = publicAPI.getLastAncestorOfType( 'vtkOpenGLRenderWindow' ); @@ -1258,7 +1258,7 @@ function vtkOpenGLRenderWindow(publicAPI, model) { // Proxy some methods if needed const publicAPIBeforeProxy = { ...publicAPI }; - rootParentMethodsToProxy.forEach((methodName) => { + parentMethodsToProxy.forEach((methodName) => { publicAPI[methodName] = (...args) => { if (model.rootOpenGLRenderWindow) { // Proxy only methods when the render window has a parent