From 45e2a0d2bc649a4ac232b64c3572ccc8ad715673 Mon Sep 17 00:00:00 2001 From: Matias Lavik Date: Sat, 24 Aug 2024 23:11:32 +0200 Subject: [PATCH] Support for secondary volume (PET/CT or segmentation) --- .../Editor/VolumeRendererEditorFunctions.cs | 43 +++++++++++++++++++ .../VolumeObject/VolumeRenderedObject.cs | 39 ++++++++++++++--- .../DirectVolumeRenderingShader.shader | 29 +++++++++++++ 3 files changed, 104 insertions(+), 7 deletions(-) diff --git a/Assets/Editor/VolumeRendererEditorFunctions.cs b/Assets/Editor/VolumeRendererEditorFunctions.cs index 90396a3f..f9001009 100644 --- a/Assets/Editor/VolumeRendererEditorFunctions.cs +++ b/Assets/Editor/VolumeRendererEditorFunctions.cs @@ -36,6 +36,12 @@ private static void ShowDICOMImporter() DicomImportAsync(true); } + [MenuItem("Volume Rendering/Load dataset/Load PET-CT DICOM")] + private static void ShowPETCTDICOMImporter() + { + PETCTDicomImportAsync(); + } + [MenuItem("Assets/Volume Rendering/Import dataset/Import DICOM")] private static void ImportDICOMAsset() { @@ -79,6 +85,43 @@ private static async void DicomImportAsync(bool spawnInScene) } } + private static async void PETCTDicomImportAsync() + { + string dirCT = EditorUtility.OpenFolderPanel("Select a CT DICOM folder to load", "", ""); + string dirPET = EditorUtility.OpenFolderPanel("Select a PET DICOM folder to load", "", ""); + if (Directory.Exists(dirCT) && Directory.Exists(dirPET)) + { + Debug.Log("Async dataset load. Hold on."); + using (ProgressHandler progressHandler = new ProgressHandler(new EditorProgressView())) + { + progressHandler.StartStage(0.35f, "Importing CT dataset"); + Task importTaskCT = DicomImportDirectoryAsync(dirCT, progressHandler); + await importTaskCT; + progressHandler.EndStage(); + Debug.Assert(importTaskCT.Result.Length > 0); + progressHandler.StartStage(0.35f, "Importing PET dataset"); + Task importTaskPET = DicomImportDirectoryAsync(dirPET, progressHandler); + await importTaskPET; + progressHandler.EndStage(); + Debug.Assert(importTaskPET.Result.Length > 0); + progressHandler.StartStage(0.3f, "Spawning dataset"); + VolumeDataset datasetCT = importTaskCT.Result[0]; + VolumeDataset datasetPET = importTaskPET.Result[0]; + VolumeRenderedObject objCT = await VolumeObjectFactory.CreateObjectAsync(datasetCT); + VolumeRenderedObject objPET = await VolumeObjectFactory.CreateObjectAsync(datasetPET); + objPET.transferFunction.colourControlPoints = new List() { new TFColourControlPoint(0.0f, Color.red), new TFColourControlPoint(1.0f, Color.red) }; + objPET.transferFunction.GenerateTexture(); + progressHandler.EndStage(); + objCT.SetSecondaryVolume(objPET); + objPET.gameObject.SetActive(false); + } + } + else + { + Debug.LogError("Directory doesn't exist"); + } + } + private static async Task DicomImportDirectoryAsync(string dir, ProgressHandler progressHandler) { Debug.Log("Async dataset load. Hold on."); diff --git a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs index 707f0b48..d7c6b4ba 100644 --- a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs +++ b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs @@ -34,6 +34,9 @@ public class VolumeRenderedObject : MonoBehaviour [SerializeField, HideInInspector] private LightSource lightSource; + [SerializeField, HideInInspector] + private VolumeRenderedObject secondaryVolume; + // Minimum and maximum gradient threshold for lighting contribution. Values below min will be unlit, and between min and max will be partly shaded. [SerializeField, HideInInspector] private Vector2 gradientLightingThreshold = new Vector2(0.02f, 0.15f); @@ -82,6 +85,12 @@ public SlicingPlane CreateSlicingPlane() return slicingPlaneComp; } + public void SetSecondaryVolume(VolumeRenderedObject volumeObject) + { + this.secondaryVolume = volumeObject; + UpdateMaterialProperties(); + } + public void SetRenderMode(RenderMode mode) { Task task = SetRenderModeAsync(mode); @@ -317,11 +326,10 @@ private async Task UpdateMaterialPropertiesAsync(IProgressHandler progressHandle try { bool useGradientTexture = tfRenderMode == TFRenderMode.TF2D || renderMode == RenderMode.IsosurfaceRendering || lightingEnabled; - Texture3D gradientTexture = useGradientTexture ? await dataset.GetGradientTextureAsync(progressHandler) : null; Texture3D dataTexture = await dataset.GetDataTextureAsync(progressHandler); - meshRenderer.sharedMaterial.SetTexture("_DataTex", dataTexture); - meshRenderer.sharedMaterial.SetTexture("_GradientTex", gradientTexture); - UpdateMatInternal(); + Texture3D gradientTexture = useGradientTexture ? await dataset.GetGradientTextureAsync(progressHandler) : null; + Texture3D secondaryDataTexture = await secondaryVolume?.dataset?.GetDataTextureAsync(progressHandler); + UpdateMatInternal(dataTexture, gradientTexture, secondaryDataTexture); } finally { @@ -329,11 +337,28 @@ private async Task UpdateMaterialPropertiesAsync(IProgressHandler progressHandle } } - private void UpdateMatInternal() + private void UpdateMatInternal(Texture3D dataTexture, Texture3D gradientTexture, Texture3D secondaryDataTexture) { - if (meshRenderer.sharedMaterial.GetTexture("_DataTex") == null) + if (dataTexture != null) { - meshRenderer.sharedMaterial.SetTexture("_DataTex", dataset.GetDataTexture()); + meshRenderer.sharedMaterial.SetTexture("_DataTex", dataTexture); + } + + if (gradientTexture != null) + { + meshRenderer.sharedMaterial.SetTexture("_GradientTex", gradientTexture); + } + + if (secondaryDataTexture != null) + { + Texture2D secondaryTF = secondaryVolume.transferFunction.GetTexture(); + meshRenderer.sharedMaterial.SetTexture("_SecondaryDataTex", secondaryDataTexture); + meshRenderer.sharedMaterial.SetTexture("_SecondaryTFTex", secondaryTF); + meshRenderer.sharedMaterial.EnableKeyword("SECONDARY_VOLUME_ON"); + } + else + { + meshRenderer.sharedMaterial.DisableKeyword("SECONDARY_VOLUME_ON"); } if (meshRenderer.sharedMaterial.GetTexture("_NoiseTex") == null) diff --git a/Assets/Shaders/DirectVolumeRenderingShader.shader b/Assets/Shaders/DirectVolumeRenderingShader.shader index 37413038..cf99b886 100644 --- a/Assets/Shaders/DirectVolumeRenderingShader.shader +++ b/Assets/Shaders/DirectVolumeRenderingShader.shader @@ -13,6 +13,8 @@ _MinGradient("Gradient visibility threshold", Range(0.0, 1.0)) = 0.0 _LightingGradientThresholdStart("Gradient threshold for lighting (end)", Range(0.0, 1.0)) = 0.0 _LightingGradientThresholdEnd("Gradient threshold for lighting (start)", Range(0.0, 1.0)) = 0.0 + _SecondaryDataTex ("Secondary Data Texture (Generated)", 3D) = "" {} + _SecondaryTFTex("Transfer Function Texture for secondary volume", 2D) = "" {} [HideInInspector] _ShadowVolumeTextureSize("Shadow volume dimensions", Vector) = (1, 1, 1) [HideInInspector] _TextureSize("Dataset dimensions", Vector) = (1, 1, 1) } @@ -37,6 +39,7 @@ #pragma multi_compile __ RAY_TERMINATE_ON #pragma multi_compile __ USE_MAIN_LIGHT #pragma multi_compile __ CUBIC_INTERPOLATION_ON + #pragma multi_compile __ SECONDARY_VOLUME_ON #pragma vertex vert #pragma fragment frag @@ -76,6 +79,8 @@ sampler2D _NoiseTex; sampler2D _TFTex; sampler3D _ShadowVolume; + sampler3D _SecondaryDataTex; + sampler2D _SecondaryTFTex; float _MinVal; float _MaxVal; @@ -191,6 +196,12 @@ return tex2Dlod(_TFTex, float4(density, gradientMagnitude, 0.0f, 0.0f)); } + // Gets the colour from a secondary 1D Transfer Function (x = density) + float4 getSecondaryTF1DColour(float density) + { + return tex2Dlod(_SecondaryTFTex, float4(density, 0.0f, 0.0f, 0.0f)); + } + // Gets the density at the specified position float getDensity(float3 pos) { @@ -201,6 +212,16 @@ #endif } + // Gets the density of the secondary volume at the specified position + float getSecondaryDensity(float3 pos) + { +#if CUBIC_INTERPOLATION_ON + return interpolateTricubicFast(_SecondaryDataTex, float3(pos.x, pos.y, pos.z), _TextureSize); +#else + return tex3Dlod(_SecondaryDataTex, float4(pos.x, pos.y, pos.z, 0.0f)); +#endif + } + // Gets the density at the specified position, without tricubic interpolation float getDensityNoTricubic(float3 pos) { @@ -323,6 +344,14 @@ continue; #endif +#if SECONDARY_VOLUME_ON + const float density2 = getSecondaryDensity(currPos); + float4 src2 = getSecondaryTF1DColour(density2); + //src.rgb = src.rgb * (1.0 - src2.a) + src2.rgb * src2.a; + src = src2.a > 0.0 ? src2 : src; + //src.a = src2.a > 0.0 ? src.a : 0.0; +#endif + // Calculate gradient (needed for lighting and 2D transfer functions) #if defined(TF2D_ON) || defined(LIGHTING_ON) float3 gradient = getGradient(currPos);