Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental option for SSAO for volume rendering #23

Merged
merged 2 commits into from Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 3 additions & 8 deletions ColorizeVolume/ColorizeVolume.py
Expand Up @@ -3,6 +3,7 @@
from typing import Annotated, Optional

import vtk
import vtkAddon

import slicer
from slicer.ScriptedLoadableModule import *
Expand Down Expand Up @@ -96,7 +97,7 @@ def setup(self) -> None:
"""
ScriptedLoadableModuleWidget.setup(self)

if not hasattr(vtk.vtkImageMedian3D, 'SetIgnoreBackground') and not hasattr(slicer, 'vtkImageLabelDilate3D'):
if not hasattr(vtkAddon, 'vtkImageLabelDilate3D'):
slicer.util.errorDisplay("This module requires a more recent version of Slicer. Please download and install latest Slicer Preview Release.")
return

Expand Down Expand Up @@ -372,13 +373,7 @@ def process(self,
# Dilate labelmap to avoid edge artifacts
slicer.util.showStatusMessage(f"Dilating segments...")
slicer.app.processEvents()
if hasattr(slicer, 'vtkImageLabelDilate3D'):
print("Use slicer.vtkImageLabelDilate3D")
slicer.app.processEvents()
dilate = slicer.vtkImageLabelDilate3D()
else:
dilate = vtk.vtkImageMedian3D()
dilate.SetIgnoreBackground(True) # ignoring background turns makes median filter dilate label values
dilate = vtkAddon.vtkImageLabelDilate3D()
dilate.SetInputData(labelmapVolumeNode.GetImageData())
dilationKernelSize = int(colorBleedThicknessVoxel + 0.5) * 2 + 1
dilate.SetKernelSize(dilationKernelSize, dilationKernelSize, dilationKernelSize)
Expand Down
128 changes: 111 additions & 17 deletions Lights/Lights.py
Expand Up @@ -107,6 +107,8 @@ def setup(self):

self.ui.ssaoCheckBox.connect('toggled(bool)', lambda value: self.logic.setUseSSAO(value))
self.ui.ssaoSizeScaleSliderWidget.connect('valueChanged(double)', lambda value: self.logic.setSSAOSizeScaleLog(value))
self.ui.ssaoVolumeRenderingCheckBox.connect('toggled(bool)', lambda value: self.logic.setUseSSAOForVolumeRendering(value))
self.ui.ssaoVolumeRenderingOpacityThresholdPercentSliderWidget.connect('valueChanged(double)', lambda value: self.logic.setSSAOVolumeRenderingOpacity(value*0.01))

self.ui.imageNone.connect('clicked(bool)', lambda: self.logic.setImageBasedLighting(None))

Expand Down Expand Up @@ -287,8 +289,16 @@ def __init__(self):
self.lightKit = vtk.vtkLightKit()
self.lightKit.MaintainLuminanceOn()
self.lightkitObserverTag = self.lightKit.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onLightkitModified)

self.ssaoEnabled = False
self.ssaoSizeScaleLog = 0.0
self.ssaoVolumeRenderingEnabled = False
self.ssaoVolumeRenderingOpacityThreshold = 0.25

self.ssaoPass = vtk.vtkSSAOPass()
renderPass = vtk.vtkRenderStepsPass()
self.ssaoPass.SetDelegatePass(renderPass)

self.managedViewNodes = []
self.imageBasedLightingImageFile = None

Expand Down Expand Up @@ -317,18 +327,16 @@ def addManagedView(self, viewNode):
renderer = renderWindow.GetRenderers().GetFirstRenderer()
renderer.RemoveAllLights()
self.lightKit.AddLightsToRenderer(renderer)

renderer.SSAOBlurOn() # reduce noise in SSAO mode
self.setUseSSAO(self.ssaoEnabled)
self.setSSAOSizeScaleLog(self.ssaoSizeScaleLog)
self.setImageBasedLighting(self.imageBasedLightingImageFile)
self.setUseSSAOSingleView(viewNode, self.ssaoEnabled)
self.setSSAOSizeScaleLogSingleView(viewNode, self.ssaoSizeScaleLog)
renderWindow.Render()

def removeManagedView(self, viewNode):
if viewNode not in self.managedViewNodes:
return
self.managedViewNodes.remove(viewNode)
renderWindow = self.renderWindowFromViewNode(viewNode)
self.setUseSSAOSingleView(viewNode, False)
renderer = renderWindow.GetRenderers().GetFirstRenderer()
# Make a copy of current lightkit
currentLightKit = vtk.vtkLightKit()
Expand All @@ -343,25 +351,111 @@ def removeManagedView(self, viewNode):
def setUseSSAO(self, enable):
self.ssaoEnabled = enable
for viewNode in self.managedViewNodes:
renderWindow = self.renderWindowFromViewNode(viewNode)
renderer = renderWindow.GetRenderers().GetFirstRenderer()
self.setUseSSAOSingleView(viewNode, self.ssaoEnabled)

def setUseSSAOSingleView(self, viewNode, enable):
renderWindow = self.renderWindowFromViewNode(viewNode)
renderer = renderWindow.GetRenderers().GetFirstRenderer()
if self.setUseSSAOForVolumeRendering:
if self.ssaoEnabled:
renderer.SetPass(self.ssaoPass)
else:
renderer.SetPass(None)
else:
renderer.SetUseSSAO(self.ssaoEnabled)
renderWindow.Render()
renderWindow.Render()

def setUseSSAOForVolumeRendering(self, enable):
self.ssaoVolumeRenderingEnabled = enable
vrDisplayNodes = slicer.util.getNodesByClass('vtkMRMLGPURayCastVolumeRenderingDisplayNode')
for vrDisplayNode in vrDisplayNodes:
self.setUseSSAOForVolumeRenderingDisplayNode(vrDisplayNode, self.ssaoVolumeRenderingEnabled, self.ssaoVolumeRenderingOpacityThreshold)

def setSSAOVolumeRenderingOpacity(self, opacityThreshold):
self.ssaoVolumeRenderingOpacityThreshold = opacityThreshold
vrDisplayNodes = slicer.util.getNodesByClass('vtkMRMLGPURayCastVolumeRenderingDisplayNode')
for vrDisplayNode in vrDisplayNodes:
self.setUseSSAOForVolumeRenderingDisplayNode(vrDisplayNode, self.ssaoVolumeRenderingEnabled, self.ssaoVolumeRenderingOpacityThreshold)

def setSSAOSizeScaleLog(self, scaleLog):
self.ssaoSizeScaleLog = scaleLog
# ScaleLog = 0.0 corresponds to 100mm scene size
sceneSize = 100.0 * pow(10, self.ssaoSizeScaleLog)
def setUseSSAOForVolumeRenderingDisplayNode(self, vrDisplayNode, enabled, opacityThreshold=0.25):

shaderPropertyNode = vrDisplayNode.GetOrCreateShaderPropertyNode(slicer.mrmlScene)
shaderProperty = shaderPropertyNode.GetShaderProperty()

shaderProperty.ClearAllFragmentShaderReplacements()

if not enabled:
return

shaderProperty.AddFragmentShaderReplacement("//VTK::ComputeLighting::Dec", True,
"""
vec3 g_dataNormal;
//VTK::ComputeLighting::Dec
""", False)
shaderProperty.AddFragmentShaderReplacement("//VTK::RenderToImage::Dec", True,
"""
vec3 l_opaqueFragNormal;
vec3 l_opaqueFragPos;
bool l_updateDepth;
""", False)
shaderProperty.AddFragmentShaderReplacement("//VTK::RenderToImage::Init", True,
"""
l_opaqueFragPos = vec3(-1.0);
l_updateDepth = true;
""", False)
shaderProperty.AddFragmentShaderReplacement("//VTK::RenderToImage::Impl", True,
"""
if(!g_skip && g_srcColor.a > """ + str(opacityThreshold) + """ && l_updateDepth)
{
l_opaqueFragPos = g_dataPos;
l_opaqueFragNormal = g_dataNormal;
l_updateDepth = false;
}
""", False)
shaderProperty.AddFragmentShaderReplacement("//VTK::RenderToImage::Exit", True,
"""
if (l_opaqueFragPos == vec3(-1.0))
{
gl_FragDepth = 1.0;
gl_FragData[1] = gl_FragData[1];
}
else
{
vec4 depthValue = in_projectionMatrix * in_modelViewMatrix *
in_volumeMatrix[0] * in_textureDatasetMatrix[0] *
vec4(l_opaqueFragPos, 1.0);
depthValue /= depthValue.w;
gl_FragDepth = 0.5 * (gl_DepthRange.far - gl_DepthRange.near) * depthValue.z + 0.5 * (gl_DepthRange.far + gl_DepthRange.near);
gl_FragData[1] = in_modelViewMatrix * in_volumeMatrix[0] * in_textureDatasetMatrix[0] * vec4(l_opaqueFragPos, 1.0);
gl_FragData[2] = vec4(normalize(l_opaqueFragNormal), 0.0);
}
""", False)


def setSSAOSizeScaleLog(self, sizeScaleLog):
self.ssaoSizeScaleLog = sizeScaleLog
for viewNode in self.managedViewNodes:
self.setSSAOSizeScaleLogSingleView(viewNode, self.ssaoSizeScaleLog)

def setSSAOSizeScaleLogSingleView(self, viewNode, sizeScaleLog):
# SizeScaleLog = 0.0 corresponds to 100mm scene size
sceneSize = 100.0 * pow(10, sizeScaleLog)
# Bias and radius are from example in https://blog.kitware.com/ssao/.
# These values have been tested on different kind of meshes and found to work well.
for viewNode in self.managedViewNodes:
renderWindow = self.renderWindowFromViewNode(viewNode)
renderWindow = self.renderWindowFromViewNode(viewNode)
if self.setUseSSAOForVolumeRendering:
self.ssaoPass.SetBias(0.001 * sceneSize) # how much distance difference will be made visible
self.ssaoPass.SetRadius(0.1 * sceneSize) # determines the spread of shadows cast by ambient occlusion
self.ssaoPass.SetBlur(True) # reduce noise
self.ssaoPass.SetKernelSize(320) # larger kernel size reduces noise pattern in the darkened region
else:
renderer = renderWindow.GetRenderers().GetFirstRenderer()
renderer.SetSSAOBias(0.001 * sceneSize); # how much distance difference will be made visible
renderer.SetSSAORadius(0.1 * sceneSize); # determines the spread of shadows cast by ambient occlusion
renderer.SetSSAOBias(0.001 * sceneSize) # how much distance difference will be made visible
renderer.SetSSAORadius(0.1 * sceneSize) # determines the spread of shadows cast by ambient occlusion
renderer.SetSSAOBlur(True) # reduce noise
renderer.SetSSAOKernelSize(320) # larger kernel size reduces noise pattern in the darkened region
renderWindow.Render()

renderWindow.Render()

def setImageBasedLighting(self, imageFilePath):
self.imageBasedLightingImageFile = imageFilePath
Expand Down
91 changes: 74 additions & 17 deletions Lights/Resources/UI/Lights.ui
Expand Up @@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>352</width>
<height>494</height>
<height>499</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2">
Expand Down Expand Up @@ -62,6 +62,63 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Shadows for volume rendering (experimental)</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="1" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>Opacity threshold:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="ctkSliderWidget" name="ssaoVolumeRenderingOpacityThresholdPercentSliderWidget">
<property name="toolTip">
<string>Size scale used for determining size of features to be emphasized or suppressed. The scale is logairthmic, default (0.0) corresponds to object size of about 100mm.</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="singleStep">
<double>1.000000000000000</double>
</property>
<property name="pageStep">
<double>10.000000000000000</double>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>100.000000000000000</double>
</property>
<property name="value">
<double>25.000000000000000</double>
</property>
<property name="prefix">
<string/>
</property>
<property name="suffix">
<string>%</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Enable:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="ctkCheckBox" name="ssaoVolumeRenderingCheckBox"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
Expand Down Expand Up @@ -621,6 +678,22 @@
</layout>
</widget>
<customwidgets>
<customwidget>
<class>qMRMLCheckableNodeComboBox</class>
<extends>qMRMLNodeComboBox</extends>
<header>qMRMLCheckableNodeComboBox.h</header>
</customwidget>
<customwidget>
<class>qMRMLCollapsibleButton</class>
<extends>ctkCollapsibleButton</extends>
<header>qMRMLCollapsibleButton.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>qMRMLNodeComboBox</class>
<extends>QWidget</extends>
<header>qMRMLNodeComboBox.h</header>
</customwidget>
<customwidget>
<class>ctkCheckBox</class>
<extends>QCheckBox</extends>
Expand All @@ -643,22 +716,6 @@
<extends>QWidget</extends>
<header>ctkSliderWidget.h</header>
</customwidget>
<customwidget>
<class>qMRMLCheckableNodeComboBox</class>
<extends>qMRMLNodeComboBox</extends>
<header>qMRMLCheckableNodeComboBox.h</header>
</customwidget>
<customwidget>
<class>qMRMLCollapsibleButton</class>
<extends>ctkCollapsibleButton</extends>
<header>qMRMLCollapsibleButton.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>qMRMLNodeComboBox</class>
<extends>QWidget</extends>
<header>qMRMLNodeComboBox.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
Expand Down