2This example shows how to create a glow around a model node using the SlicerLayerDisplayableManager module.
4It goes over the following concepts:
5 - Creates a unique display pipeline to set attach a VTK glow pass to a renderer
6 - Creates one glow display pipeline per 3D view for each created model nodes in the scene
7 - Register the pipeline creation mechanism
8 - Handles hover interaction: on mouse move, each pipeline reports whether the cursor is within its model's
9 bounding box and its distance to the model center. The closest matching pipeline gains focus and sets an
10 "IsSelected" attribute on its data node; losing focus (cursor moved to another model or left the window)
11 resets the attribute. The observed attribute change updates the glow actor visibility, so only the
15 This example is implemented as a scripted module and can be added as such to Slicer.
16 Once added, loading new model nodes will set their glow pass automatically.
25 vtkMRMLAbstractViewNode,
26 vtkMRMLInteractionEventData,
30 vtkMRMLScriptedModuleNode,
34from slicer.ScriptedLoadableModule
import ScriptedLoadableModule, ScriptedLoadableModuleWidget
48 vtkTransformPolyDataFilter,
51from LayerDMLib
import ScriptedPipelineSceneConnector, vtkMRMLLayerDMScriptedPipeline
56 ScriptedLoadableModule.__init__(self, parent)
57 self.parent.title =
"Model Glow Pipeline Example"
58 self.parent.categories = [
"Examples"]
59 self.parent.dependencies = []
60 self.parent.contributors = []
61 self.parent.helpText =
""
62 self.parent.acknowledgementText =
""
67 slicer.app.connect(
"startupCompleted()", _sceneConnector.connect)
71 """In this example, the module's widget will allow us to create random sphere model nodes in the scene."""
75 In the setup method, we create a widget with only two buttons:
76 - A "create sphere" button to create a random sphere in the scene
77 - A "Reset 3D views" button to reset the 3D view on the created spheres
82 _sceneConnector.connect()
83 ScriptedLoadableModuleWidget.setup(self)
86 layout = qt.QVBoxLayout(widget)
88 createSphereButton = qt.QPushButton(
"Create sphere")
90 layout.addWidget(createSphereButton)
92 reset3DView = qt.QPushButton(
"Reset 3D views")
93 reset3DView.clicked.connect(slicer.util.resetThreeDViews)
94 layout.addWidget(reset3DView)
97 self.layout.addWidget(widget)
101 Called when the module is about to be unloaded and by slicer.util.reloadScriptedModule before the widget is
102 rebuilt (module Reload button).
104 Here, we disconnect the pipelines from the scene (unregister the pipeline creator from the factory
105 singleton and remove the scene observers). Without this, the previously registered creator and
106 observers would keep running the pre-reload code.
108 _sceneConnector.disconnect()
109 ScriptedLoadableModuleWidget.cleanup(self)
114 Here we create the sphere models at random.
116 Note: Attaching the glow data to the model could be done in this type of methods if we wanted more control
117 on the creation logic.
120 sphereSource = vtkSphereSource()
121 sphereSource.SetCenter(random.uniform(0, 10),
122 random.uniform(0, 10),
123 random.uniform(0, 10))
124 sphereSource.SetRadius(random.uniform(0.1, 1.0))
125 sphereSource.Update()
128 modelNode = slicer.mrmlScene.AddNewNodeByClass(
"vtkMRMLModelNode")
129 modelNode.SetAndObservePolyData(sphereSource.GetOutput())
130 modelNode.CreateDefaultDisplayNodes()
133 displayNode = modelNode.GetDisplayNode()
134 modelNode.SetAndObserveDisplayNodeID(displayNode.GetID())
135 displayNode.SetColor(random.random(), random.random(), random.random())
140 This class is a convenience abstract class for the glow pass pipelines.
141 It provides the following static methods:
142 - _CreatePipelineNode: Creates a new scripted node with a PipelineType string property containing the class type
143 This value is used to check if the pipeline should be created when a node is added to the scene.
144 - IsPipelineNode: Checks if a node is a scripted node and it contains the right PipelineType string property
145 - TryCreatePipeline: Creates new pipeline instances for views matching 3D views and PipelineType nodes.
151 Creates a new scripted node with a PipelineType string property containing the class type.
152 This value is used to check if the pipeline should be created when a node is added to the scene.
154 In Python, inheritance of vtkMRMLDisplayNode is not possible to create new display node types.
155 To bypass this limitation and create display data that will be used in the pipelines, we use
156 vtkMRMLScriptedModuleNode instead.
158 vtkMRMLScriptedModuleNode can contain any pairs of string keys and string values.
159 We use this mechanism to store the type of pipeline we would like to create for the given node.
161 The actual choice of attribute is arbitrary in this example and the creation logic can be adapted in actual
164 Warning: Pipeline creation in the views is triggered by adding the node to the scene.
165 vtkMRMLScriptedModuleNode properties should be initialized prior to adding the nodes to the scene.
174 Returns True if the input vktMRMLNode is a scripted node and has the pipeline type attribute matching the
175 current pipeline class.
177 return isinstance(node, vtkMRMLScriptedModuleNode)
and node.GetAttribute(
"PipelineType") == cls.
_GetClassName()
181 cls, viewNode: vtkMRMLAbstractViewNode, node: vtkMRMLNode,
182 ) -> vtkMRMLLayerDMScriptedPipeline |
None:
184 Since we are creating pipelines for 3D views only, we check here if the view node is a ThreedView node and
185 if the node matches the current pipeline type (i.e. was created using the _CreatePipelineNode method).
188 if not cls.
IsPipelineNode(node)
or not isinstance(viewNode, vtkMRMLViewNode):
196 Convenience method to get the name of the current class.
197 This method will return the actual class name for inheriting classes.
204 The GlowDMPassPipeline is responsible for setting a glow render pass to the input renderer.
205 Since the pass only need to be set once, the following are used in this class:
206 - The data node is set to be a singleton data. This will make the data node persist in between scene clear.
207 - Setting and removing the render pass is done on renderer added / removed API calls.
208 - We set the pipeline to its own renderer (GetRenderOrder != 0) and we make it easy for other classes to know
209 the pipeline's render order by adding a convenience static method.
211 Note: This logic doesn't need to be split but is an example of possible implementation decoupling.
216 At creation, we call the super class initialization and create the VTK passes.
217 The VTK passes are then set during the API on renderer added / removed calls.
226 Triggered when the pipeline is displayed on a new renderer.
228 When the renderer is added, we attach our glow pass.
229 Since we don't control the actual renderer used by the pipeline, this should be used systematically.
230 See also: self.GetRenderer()
239 Triggered when the pipeline is removed from its previous renderer.
241 When the renderer is removed, we remove our glow pass and release the graphics resources it holds
242 (framebuffers, textures). Without the release, the GPU resources would leak when the pipeline is
243 deleted (for instance when it is recreated after a module reload).
244 Since we don't control the actual renderer used by the pipeline, this should be used systematically.
245 See also: self.GetRenderer()
250 renderer.SetPass(
None)
251 if renderer.GetRenderWindow():
252 self.
_glowPass.ReleaseGraphicsResources(renderer.GetRenderWindow())
256 Arbitrary render order number where the pipeline wants to be displayed.
257 Return 0 to be at the default order (main 3D Slicer pipelines)
258 Return larger values to be rendered on top of pipelines with lower render orders.
260 :return: default = 0. Here we use a helper static method to return the Glow pass render order and allow
261 pipelines that want to be rendered in this renderer to use it as well.
267 """Convenience static method to return the glow pass render order and be used by other classes."""
273 Convenience static method to add a singleton render glow pass data node to the scene.
274 The scene is passed in argument to avoid using the singleton slicer.mrmlScene which is not available in
281 node.SetSingletonOn()
282 node.SetSingletonTag(
"RenderGlowPassPipeline")
288 Convenience static method to check if the singleton glow pass data node is present in the scene.
289 The scene is passed in argument to avoid using the singleton slicer.mrmlScene which is not available in
292 return scene.GetSingletonNode(
"RenderGlowPassPipeline",
"vtkMRMLScriptedModuleNode")
is not None
297 The ModelGlowDMPipeline is responsible for the actual creation of actors (and mappers) that will be rendered
298 in the glow pass renderer.
300 In this pipeline, we do three things:
301 - Configure the rendering pipeline
302 - connect the pipeline reactivity to the scene observers
303 - connect the interaction events to make our model glow when the interaction is within the model's bounding box
305 Creation of the pipeline will be handled by our _Pipeline.TryCreatePipeline base methods and connected to the
306 factory in the registerPipeline method that we connected to the application load event.
311 In the pipeline creation, we create the different VTK objects.
312 Here, the mapper properties are static, but they could be set in the data node and be reactive.
316 colors = vtkNamedColors()
320 self.
_glowActor.GetProperty().SetColor(colors.GetColor3d(
"Magenta"))
336 Triggered when the pipeline is displayed on a new renderer.
337 default behavior: does nothing.
339 Here, we add our actor to the input renderer.
340 If the pipeline renderer has changed, the pipeline's UpdateDisplay method will be triggered and in turn its
341 UpdateFromMRML method will be triggered.
343 Since we don't control the actual renderer used by the pipeline, this should be used systematically.
344 See also: self.GetRenderer()
347 if renderer
is None or renderer.HasViewProp(self.
_glowActor):
353 Triggered when the pipeline is removed from its previous renderer.
354 default behavior: does nothing.
356 Here, we add our actor to the input renderer.
357 If the pipeline renderer has changed, the pipeline's UpdateDisplay method will be triggered and in turn its
358 UpdateFromMRML method will be triggered.
360 Since we don't control the actual renderer used by the pipeline, this should be used systematically.
361 See also: self.GetRenderer()
364 if renderer
is None or not renderer.HasViewProp(self.
_glowActor):
370 Arbitrary render order number where the pipeline wants to be displayed.
371 Here, we use the glow pass's render order to be in the same renderer (although we don't know which it will be)
373 return GlowDMPassPipeline.GetGlowPassRenderOrder()
377 Triggered by self.UpdateDisplay() calls:
378 - Called automatically at pipeline creation / add to the render window
379 - Called automatically when switching renderer
380 Override to update the representation of the pipeline in the different views.
382 See also: self.RequestRender()
383 default behavior: does nothing.
385 Here, we update the mapper connection to our modelNode PolyData and update the actor visibility to follow
386 the model's visibility.
387 Finally we ask for a rendering refresh.
395 Observer update callback.
396 Triggered when any object & events observed using UpdateObservation is triggered.
398 :param obj: vtkObject instance which triggered the callback
399 :param eventId: Event id which triggered the callback
400 :param callData: Optional observer call data. Use self.CastCallData(callData, vtkType) to convert to Python
402 Here, we want to update our model transform node observer if it has changed and trigger the pipeline's display
403 when either the view has changed (observed by default), the data node has changed (observed by default), or
404 our modelNode / transform nodes have changed (manually observed).
414 Set the display node for the pipeline has changed (initialization).
415 default behavior: Stored and display node is observed for vtkCommand::ModifiedEvent.
416 See also: self.UpdateObservation(prevObj, newObj, eventIds)
417 See also: self.OnUpdate(obj, eventId, callData)
419 :param node: The new instance of display node for the pipeline
426 Should return true + distance2 to interaction if the pipeline can process the input event data.
427 :param eventData: The MRML event needing to be processed
428 :return: (bool, distance2) default = False, float_max
430 Here, we check if the event interaction is within the glow pass actor bounds and return the distance to it.
432 To avoid blocking camera interaction, we will also process mouse move events.
433 Of course we could also check for left click to go further with this widget.
435 Note: This is not an efficient way to check for interactions. A better way would be to delegate to the model
436 display node for picking events.
439 isMouseMoveEvent = eventData.GetType() == vtkCommand.MouseMoveEvent
442 return False, sys.float_info.max
444 pos = eventData.GetWorldPosition()
445 glowActorBounds = self.
_polyData.GetBounds()
447 glowActorBounds[0] < pos[0] < glowActorBounds[1]
448 and glowActorBounds[2] < pos[1] < glowActorBounds[3]
449 and glowActorBounds[4] < pos[2] < glowActorBounds[5]
451 distance2 = vtkMath.Distance2BetweenPoints(pos, self.
_polyData.GetCenter())
452 return isInBounds, distance2
456 Triggered when the pipeline can process the interaction and is at the top of the priority list.
457 default behavior: does nothing and returns false.
459 :param eventData: The MRML event needing to be processed
460 :return: True if event was processed. False otherwise (default = false)
462 Here, the pipeline is the closest to the interaction. We modify our display property which will trigger the
465 if not self.GetDisplayNode():
468 self.GetDisplayNode().SetAttribute(
"IsSelected", str(1))
471 def LoseFocus(self, eventData: vtkMRMLInteractionEventData |
None) ->
None:
473 Triggered when the pipeline had focus (processed an interaction) and loses the focus (other pipeline
474 handled the new interaction or window leave event).
475 default behavior: does nothing.
476 :param eventData: Optional event data which triggered the lose focus
478 Here, the pipeline lost the previous interaction.
479 We make sure to restore the selection state.
482 if not self.GetDisplayNode():
484 self.GetDisplayNode().SetAttribute(
"IsSelected", str(0))
488 Convenience method to update the mapper connection.
489 Note: We apply the polydata transform manually here, but we could configure a transform pipeline and use
490 the model's polydata connection instead for a cleaner VTK implementation.
497 """Convenience method to update the transformed displayed polydata based on the current transform node."""
499 if transformNode
is None:
501 transformFilter = vtkTransformPolyDataFilter()
502 transform = vtkGeneralTransform()
503 transformNode.GetTransformToWorld(transform)
504 transformFilter.SetTransform(transform)
505 transformFilter.SetInputData(polyData)
506 transformFilter.Update()
507 return transformFilter.GetOutput()
510 """Convenience method to update the actor based on the model's visibility and the selection."""
512 selectedAttribute = self.GetDisplayNode().GetAttribute(
"IsSelected")
if self.GetDisplayNode()
else None
513 isSelected = bool(int(selectedAttribute))
if selectedAttribute
else False
519 Convenience static method to create and add a new glow data node pointing to a model node and add it to the
522 Note: Here, we could add our new data node as a reference node for the modelNode instead of keeping each
523 separate. This would allow to iterate over the modelNode's references and find it instead of having
524 to iterate on the scene.
533 node.SetAttribute(
"ModelNodeID", modelNode.GetID())
534 node.SetAttribute(
"IsSelected", str(0))
535 return scene.AddNode(node)
540 Convenience static method to remove a glow node set on a given modelNode.
541 See also: CreateGlowNode
543 Note: This logic can be simplified if we attach our pipeline to the model node directly.
544 We would then iterate over node references to check if we have our pipeline node.
546 for node
in slicer.util.getNodesByClass(
"vtkMRMLScriptedModuleNode", scene):
548 scene.RemoveNode(node)
551 """Convenience method to check if the pipeline's model node is visible."""
553 if modelNode
is None:
555 return bool(modelNode.GetDisplayVisibility())
559 Convenience method to get the model node associated with the pipeline's data node.
561 Here, we use the following APIs:
562 - GetScene: This will return the scene on which the pipeline is attached
563 - GetDisplayNode: This will return the pipeline's data node instance
565 return self.GetScene().GetNodeByID(self.
_GetModelNodeID(self.GetDisplayNode()))
569 Convenience method to update the model and the model's transform node observers.
571 Here, we use the UpdateObservation method:
572 - UpdateObservation(vtkObject* prevObj, vtkObject* obj, const std::vector<unsigned long>& events) -> bool
573 - UpdateObservation(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent) -> bool
575 This method should be used to add an observer on VTK object.
576 By default, the object's modified event will be observed.
577 The method also supports lists of events.
579 On modify event, the class's self.OnUpdate method will be called.
581 Warning: prevObj is not mutated by this call. To update the pointer, a manual set is required after update.
583 See also: self.OnUpdate
594 Convenience method to update the model's transform node observer.
596 Note: Here we explicitly observe the transform modified event as modifying the transform doesn't trigger
599 See also: self._ObserveModelNode
605 self.UpdateObservation(self.
_modelTransform, transformNode, vtkMRMLTransformNode.TransformModifiedEvent)
610 """Convenience method to get the model node ID attached to the input MRML node."""
612 return node.GetAttribute(
"ModelNodeID")
618 Connects this module's pipelines to the scene.
620 For pipelines to be created in our views, the pipeline factory needs to know about our pipeline classes.
621 When a node is added to the scene, the LayerDM orchestration queries the vtkMRMLLayerDMPipelineFactory
622 singleton to check if a pipeline can be created for the view node / node pair. The
623 ScriptedPipelineSceneConnector base class registers a creator which delegates to our pipeline classes'
624 TryCreatePipeline methods, and keeps track of what was connected so that the connection can be undone
625 (module unload) or replaced (module reload).
627 We also attach two scene observers to add our glow data node to model nodes when they are added and
628 garbage collect them on removal, and we create the glow pass data node so the glow pass pipeline is
631 Note: Scene lifecycle are managed by the LayerDM library. If the view is newly created, its pipeline
632 manager will iterate over all the nodes in the scene to check if pipelines need to be created.
633 Similarly, when loading a scene, clearing a scene, the pipelines will be handled accordingly.
637 return [GlowDMPassPipeline, ModelGlowDMPipeline]
647 Create our glow pass data node so that the glow pass pipeline is created.
648 Note: We use self.getScene() instead of the slicer.mrmlScene singleton so that the connector can be
649 used with an explicit scene (for instance in trame-slicer where the singleton is not available).
651 GlowDMPassPipeline.EnsureGlowPass(self.getScene())
653 @calldata_type(VTK_OBJECT)
655 """Attach a glow data node to model nodes when they are added to the scene."""
656 if isinstance(node, vtkMRMLModelNode):
657 ModelGlowDMPipeline.CreateGlowNode(node, self.getScene())
659 @calldata_type(VTK_OBJECT)
661 """Garbage collect the glow data node when its model node is removed from the scene."""
662 if isinstance(node, vtkMRMLModelNode):
663 ModelGlowDMPipeline.RemoveGlowNode(node, self.getScene())
GetGlowPassRenderOrder(cls)
bool SceneHasGlowPass(cls, vtkMRMLScene scene)
None OnRendererRemoved(self, vtkRenderer renderer)
EnsureGlowPass(cls, vtkMRMLScene scene)
None OnRendererAdded(self, vtkRenderer renderer)
vtkMRMLModelNode|None _GetModelNode(self)
None OnRendererRemoved(self, vtkRenderer renderer)
vtkMRMLModelNode|None _modelNode
_UpdateActorVisibility(self)
_UpdateMapperConnection(self)
_GetModelNodeID(cls, node)
SetDisplayNode(self, node)
_TransformPolyData(self, polyData)
None OnRendererAdded(self, vtkRenderer renderer)
RemoveGlowNode(cls, vtkMRMLModelNode modelNode, vtkMRMLScene scene)
OnUpdate(self, obj, eventId, callData)
_ObserveModelTransformNode(self)
tuple[bool, float] CanProcessInteractionEvent(self, vtkMRMLInteractionEventData eventData)
None LoseFocus(self, vtkMRMLInteractionEventData|None eventData)
bool _IsModelVisible(self)
CreateGlowNode(cls, vtkMRMLModelNode modelNode, vtkMRMLScene scene)
bool ProcessInteractionEvent(self, vtkMRMLInteractionEventData eventData)
list getSceneObservers(self)
IsPipelineNode(cls, node)
vtkMRMLScriptedModuleNode _CreatePipelineNode(cls)
vtkMRMLLayerDMScriptedPipeline|None TryCreatePipeline(cls, vtkMRMLAbstractViewNode viewNode, vtkMRMLNode node)