diff --git a/Assets/Editor/TransferFunctionEditorWindow.cs b/Assets/Editor/TransferFunctionEditorWindow.cs index 56b6a936..f602897c 100644 --- a/Assets/Editor/TransferFunctionEditorWindow.cs +++ b/Assets/Editor/TransferFunctionEditorWindow.cs @@ -5,7 +5,7 @@ namespace UnityVolumeRendering { public class TransferFunctionEditorWindow : EditorWindow { - private TransferFunction tf = null; + private TransferFunctionInstance tfInstance = null; private VolumeRenderedObject volRendObject = null; @@ -49,7 +49,8 @@ private void OnGUI() if (volRendObject == null) return; - tf = volRendObject.transferFunction; + tfInstance = volRendObject.transferFunctionInstance; + TransferFunction tf = tfInstance.transferFunction; Event currentEvent = new Event(Event.current); @@ -62,7 +63,7 @@ private void OnGUI() Rect outerRect = new Rect(0.0f, 0.0f, contentWidth, contentHeight); Rect tfEditorRect = new Rect(outerRect.x + 20.0f, outerRect.y + 20.0f, outerRect.width - 40.0f, outerRect.height - 50.0f); - tfEditor.SetVolumeObject(volRendObject); + tfEditor.SetTransferFunctionInstnace(tfInstance); tfEditor.DrawOnGUI(tfEditorRect); // Save TF @@ -76,7 +77,7 @@ private void OnGUI() // Load TF if(GUI.Button(new Rect(tfEditorRect.x + 75.0f, tfEditorRect.y + tfEditorRect.height + 20.0f, 70.0f, 30.0f), "Load")) { - string filepath = EditorUtility.OpenFilePanel("Save transfer function", "", "tf"); + string filepath = EditorUtility.OpenFilePanel("Load transfer function", "", "tf"); if(filepath != "") { TransferFunction newTF = TransferFunctionDatabase.LoadTransferFunction(filepath); @@ -92,6 +93,7 @@ private void OnGUI() if(GUI.Button(new Rect(tfEditorRect.x + 150.0f, tfEditorRect.y + tfEditorRect.height + 20.0f, 70.0f, 30.0f), "Clear")) { tf = ScriptableObject.CreateInstance(); + tf.relativeScale = true; tf.alphaControlPoints.Add(new TFAlphaControlPoint(0.2f, 0.0f)); tf.alphaControlPoints.Add(new TFAlphaControlPoint(0.8f, 1.0f)); tf.colourControlPoints.Add(new TFColourControlPoint(0.5f, new Color(0.469f, 0.354f, 0.223f, 1.0f))); diff --git a/Assets/Editor/TransferFunctionUpgraderWindow.cs b/Assets/Editor/TransferFunctionUpgraderWindow.cs new file mode 100644 index 00000000..97e7bd78 --- /dev/null +++ b/Assets/Editor/TransferFunctionUpgraderWindow.cs @@ -0,0 +1,145 @@ +using System.IO; +using UnityEditor; +using UnityEngine; + +namespace UnityVolumeRendering +{ + public class TransferFunctionUpgraderWindow : EditorWindow + { + private enum TFConversionState + { + ReadyToImport, + ReadyToConvert, + ReadyToSave + } + + private TransferFunction transferFunction = null; + private VolumeRenderedObject targetObject = null; + private string infoText = ""; + private string errorText = ""; + private string tfFilePath = ""; + + private TFConversionState state = TFConversionState.ReadyToImport; + + public static void ShowWindow() + { + TransferFunctionUpgraderWindow wnd = new TransferFunctionUpgraderWindow(); + wnd.Show(); + } + + private void OnGUI() + { + if (state == TFConversionState.ReadyToImport) + { + targetObject = null; + transferFunction = null; + } + + GUIStyle headerStyle = new GUIStyle(EditorStyles.label); + headerStyle.fontSize = 20; + + GUIStyle groupHeaderStyle = new GUIStyle(EditorStyles.label); + groupHeaderStyle.fontSize = 16; + groupHeaderStyle.fontStyle = FontStyle.Bold; + + GUIStyle errorStyle = new GUIStyle(EditorStyles.label); + errorStyle.normal.textColor = Color.red; + + GUIStyle infoStyle = new GUIStyle(EditorStyles.label); + infoStyle.wordWrap = true; + + EditorGUILayout.LabelField("Transfer function upgrader tool", headerStyle); + + EditorGUILayout.Space(); + + if (infoText != "") + { + EditorGUILayout.LabelField(infoText); + EditorGUILayout.Space(); + } + + if (errorText != "") + { + EditorGUILayout.LabelField(errorText, errorStyle); + EditorGUILayout.Space(); + } + + GUILayout.Label("Import", groupHeaderStyle); + if (GUILayout.Button("Load transfer function")) + { + tfFilePath = EditorUtility.OpenFilePanel("Load transfer function", "", "tf"); + transferFunction = TransferFunctionDatabase.LoadTransferFunction(tfFilePath); + if (transferFunction == null) + errorText = "Invalid transfer function"; + else + { + if (transferFunction.relativeScale) + { + infoText = "Old transfer function with relative scale detected. Please convert it."; + state = TFConversionState.ReadyToConvert; + } + else + { + infoText = "Transfer function already up-to-date. Nothing was done."; + } + } + } + EditorGUILayout.Space(); + + if (state == TFConversionState.ReadyToConvert || state == TFConversionState.ReadyToSave) + { + GUILayout.Label("Select target object (dataset)", groupHeaderStyle); + targetObject = EditorGUILayout.ObjectField("Target object", targetObject, typeof(VolumeRenderedObject), true) as VolumeRenderedObject; + if (targetObject != null && targetObject.dataset != null) + { + ConvertToAbsoluteTF(transferFunction, targetObject.dataset); + infoText = "The transfer function has been converted. Please save it."; + state = TFConversionState.ReadyToSave; + } + GUILayout.Label("Since the transfer function was using data values relative to a dataset," + + "we need a reference to this dataset in order to convert the values to Hounsfield scale, which is now the default.", infoStyle); + EditorGUILayout.Space(); + } + + if (state == TFConversionState.ReadyToSave) + { + GUILayout.Label("Save converted dataset", groupHeaderStyle); + if (targetObject == null || transferFunction == null) + { + state = TFConversionState.ReadyToImport; + return; + } + + if (GUILayout.Button("Save transfer function")) + { + string filepath = EditorUtility.SaveFilePanel("Save transfer function", Path.GetDirectoryName(tfFilePath), Path.GetFileName(tfFilePath), "tf"); + TransferFunctionDatabase.SaveTransferFunction(transferFunction, filepath); + infoText = "Transfer function saved. You can now import a new one."; + state = TFConversionState.ReadyToImport; + } + } + } + + private void ConvertToAbsoluteTF(TransferFunction transferFunction, VolumeDataset dataset) + { + if (transferFunction.relativeScale) + { + float minValue = dataset.GetMinDataValue(); + float maxValue = dataset.GetMaxDataValue(); + for (int i = 0; i < transferFunction.colourControlPoints.Count; i++) + { + TFColourControlPoint point = transferFunction.colourControlPoints[i]; + point.dataValue = Mathf.Lerp(minValue, maxValue, point.dataValue); + transferFunction.colourControlPoints[i] = point; + } + for (int i = 0; i < transferFunction.alphaControlPoints.Count; i++) + { + TFAlphaControlPoint point = transferFunction.alphaControlPoints[i]; + point.dataValue = Mathf.Lerp(minValue, maxValue, point.dataValue); + transferFunction.alphaControlPoints[i] = point; + } + transferFunction.relativeScale = false; + } + } + } +} diff --git a/Assets/Editor/TransferFunctionUpgraderWindow.cs.meta b/Assets/Editor/TransferFunctionUpgraderWindow.cs.meta new file mode 100644 index 00000000..dada3db6 --- /dev/null +++ b/Assets/Editor/TransferFunctionUpgraderWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d1db5a5c776b6664bbbe7a4996dd72b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/VolumeRendererEditorFunctions.cs b/Assets/Editor/VolumeRendererEditorFunctions.cs index 66168dbf..c51f8e31 100644 --- a/Assets/Editor/VolumeRendererEditorFunctions.cs +++ b/Assets/Editor/VolumeRendererEditorFunctions.cs @@ -372,7 +372,13 @@ private static void SpawnCutoutSphere() VolumeObjectFactory.SpawnCutoutSphere(objects[0]); } - [MenuItem("Volume Rendering/1D Transfer Function")] + [MenuItem("Volume Rendering/Transfer Function/Transfer function upgrader tool")] + private static void ShowTFUpgraderWindow() + { + TransferFunctionUpgraderWindow.ShowWindow(); + } + + [MenuItem("Volume Rendering/Transfer Function/1D Transfer Function editor")] private static void Show1DTFWindow() { VolumeRenderedObject volRendObj = SelectionHelper.GetSelectedVolumeObject(); @@ -387,7 +393,7 @@ private static void Show1DTFWindow() } } - [MenuItem("Volume Rendering/2D Transfer Function")] + [MenuItem("Volume Rendering/Transfer Function/2D Transfer Function editor")] private static void Show2DTFWindow() { TransferFunction2DEditorWindow.ShowWindow(); diff --git a/Assets/Scripts/GUI/Components/DistanceMeasureTool.cs b/Assets/Scripts/GUI/Components/DistanceMeasureTool.cs index 3241be01..51b112d2 100644 --- a/Assets/Scripts/GUI/Components/DistanceMeasureTool.cs +++ b/Assets/Scripts/GUI/Components/DistanceMeasureTool.cs @@ -94,31 +94,5 @@ private void UpdateWindow(int windowID) GUILayout.EndVertical(); } - - private void OnLoadTransferFunction(RuntimeFileBrowser.DialogResult result) - { - if(!result.cancelled) - { - string extension = Path.GetExtension(result.path); - if(extension == ".tf") - { - TransferFunction tf = TransferFunctionDatabase.LoadTransferFunction(result.path); - if (tf != null) - { - targetObject.transferFunction = tf; - targetObject.SetTransferFunctionMode(TFRenderMode.TF1D); - } - } - if (extension == ".tf2d") - { - TransferFunction2D tf = TransferFunctionDatabase.LoadTransferFunction2D(result.path); - if (tf != null) - { - targetObject.transferFunction2D = tf; - targetObject.SetTransferFunctionMode(TFRenderMode.TF2D); - } - } - } - } } } diff --git a/Assets/Scripts/GUI/Components/EditVolumeGUI.cs b/Assets/Scripts/GUI/Components/EditVolumeGUI.cs index e290c940..4d63020b 100644 --- a/Assets/Scripts/GUI/Components/EditVolumeGUI.cs +++ b/Assets/Scripts/GUI/Components/EditVolumeGUI.cs @@ -116,7 +116,7 @@ private void OnLoadTransferFunction(RuntimeFileBrowser.DialogResult result) TransferFunction tf = TransferFunctionDatabase.LoadTransferFunction(result.path); if (tf != null) { - targetObject.transferFunction = tf; + targetObject.SetTransferFunction(tf); targetObject.SetTransferFunctionMode(TFRenderMode.TF1D); } } diff --git a/Assets/Scripts/GUI/Components/RuntimeTransferFunctionEditor.cs b/Assets/Scripts/GUI/Components/RuntimeTransferFunctionEditor.cs index 26c45730..439fbdb9 100644 --- a/Assets/Scripts/GUI/Components/RuntimeTransferFunctionEditor.cs +++ b/Assets/Scripts/GUI/Components/RuntimeTransferFunctionEditor.cs @@ -5,7 +5,7 @@ namespace UnityVolumeRendering public class RuntimeTransferFunctionEditor : MonoBehaviour { private static RuntimeTransferFunctionEditor instance = null; - private TransferFunction tf = null; + private TransferFunctionInstance tfInstance = null; private VolumeRenderedObject volRendObject = null; private int windowID; @@ -60,7 +60,8 @@ private void UpdateWindow(int windowID) if (volRendObject == null) return; - tf = volRendObject.transferFunction; + tfInstance = volRendObject.transferFunctionInstance; + TransferFunction tf = tfInstance.transferFunction; float contentWidth = Mathf.Min(WINDOW_WIDTH, (WINDOW_HEIGHT - 100.0f) * 2.0f); float contentHeight = contentWidth * 0.5f; @@ -68,7 +69,7 @@ private void UpdateWindow(int windowID) Rect outerRect = new Rect(0.0f, 0.0f, contentWidth, contentHeight); Rect tfEditorRect = new Rect(outerRect.x + 20.0f, outerRect.y + 20.0f, outerRect.width - 40.0f, outerRect.height - 50.0f); - tfEditor.SetVolumeObject(volRendObject); + tfEditor.SetTransferFunctionInstnace(tfInstance); tfEditor.DrawOnGUI(tfEditorRect); // Save TF @@ -136,6 +137,7 @@ private void UpdateWindow(int windowID) /// Threshold for maximum distance. Points further away than this won't get picked. private int PickColourControlPoint(float position, float maxDistance = 0.03f) { + TransferFunction tf = tfInstance.transferFunction; int nearestPointIndex = -1; float nearestDist = 1000.0f; for (int i = 0; i < tf.colourControlPoints.Count; i++) @@ -157,6 +159,7 @@ private int PickColourControlPoint(float position, float maxDistance = 0.03f) /// Threshold for maximum distance. Points further away than this won't get picked. private int PickAlphaControlPoint(Vector2 position, float maxDistance = 0.05f) { + TransferFunction tf = tfInstance.transferFunction; int nearestPointIndex = -1; float nearestDist = 1000.0f; for (int i = 0; i < tf.alphaControlPoints.Count; i++) diff --git a/Assets/Scripts/GUI/IMGUI/TransferFunctionEditor.cs b/Assets/Scripts/GUI/IMGUI/TransferFunctionEditor.cs index f5f677c9..148f9e0d 100644 --- a/Assets/Scripts/GUI/IMGUI/TransferFunctionEditor.cs +++ b/Assets/Scripts/GUI/IMGUI/TransferFunctionEditor.cs @@ -8,7 +8,7 @@ public class TransferFunctionEditor private int movingAlphaPointIndex = -1; private int selectedColPointIndex = -1; - private VolumeRenderedObject volRendObject = null; + private TransferFunctionInstance transferFunctionInstance = null; private Texture2D histTex = null; private Material tfGUIMat = null; @@ -19,25 +19,30 @@ public class TransferFunctionEditor private const float COLOUR_PALETTE_HEIGHT = 20.0f; private const float COLOUR_POINT_WIDTH = 10.0f; + private float dataRangeMin = 0.0f; + private float dataRangeMax = 1.0f; + public void Initialise() { tfGUIMat = Resources.Load("TransferFunctionGUIMat"); tfPaletteGUIMat = Resources.Load("TransferFunctionPaletteGUIMat"); } - public void SetVolumeObject(VolumeRenderedObject volRendObject) + public void SetTransferFunctionInstnace(TransferFunctionInstance tfInstance) { - this.volRendObject = volRendObject; + this.transferFunctionInstance = tfInstance; } public void DrawOnGUI(Rect rect) { GUI.skin.button.alignment = TextAnchor.MiddleCenter; - if (volRendObject == null) + if (transferFunctionInstance == null) return; - TransferFunction tf = volRendObject.transferFunction; + TransferFunction tf = transferFunctionInstance.transferFunction; + dataRangeMin = tf.relativeScale ? 0.0f : transferFunctionInstance.dataset.GetMinDataValue(); + dataRangeMax = tf.relativeScale ? 1.0f : transferFunctionInstance.dataset.GetMaxDataValue(); Event currentEvent = Event.current; @@ -55,25 +60,25 @@ public void DrawOnGUI(Rect rect) Rect paletteInteractionRect = new Rect(paletteRect.x - 10.0f, paletteRect.y, paletteRect.width + 30.0f, paletteRect.height); // TODO: Don't do this every frame - tf.GenerateTexture(); + transferFunctionInstance.GenerateTexture(); // Create histogram texture if(histTex == null) { if(SystemInfo.supportsComputeShaders) - histTex = HistogramTextureGenerator.GenerateHistogramTextureOnGPU(volRendObject.dataset); + histTex = HistogramTextureGenerator.GenerateHistogramTextureOnGPU(transferFunctionInstance.dataset); else - histTex = HistogramTextureGenerator.GenerateHistogramTexture(volRendObject.dataset); + histTex = HistogramTextureGenerator.GenerateHistogramTexture(transferFunctionInstance.dataset); } // Draw histogram - tfGUIMat.SetTexture("_TFTex", tf.GetTexture()); + tfGUIMat.SetTexture("_TFTex", transferFunctionInstance.GetTexture()); tfGUIMat.SetTexture("_HistTex", histTex); - Graphics.DrawTexture(histRect, tf.GetTexture(), tfGUIMat); + Graphics.DrawTexture(histRect, transferFunctionInstance.GetTexture(), tfGUIMat); // Draw colour palette - Texture2D tfTexture = tf.GetTexture(); - tfPaletteGUIMat.SetTexture("_TFTex", tf.GetTexture()); + Texture2D tfTexture = transferFunctionInstance.GetTexture(); + tfPaletteGUIMat.SetTexture("_TFTex", transferFunctionInstance.GetTexture()); Graphics.DrawTexture(new Rect(paletteRect.x, paletteRect.y, paletteRect.width, paletteRect.height), tfTexture, tfPaletteGUIMat); // Release selected colour/alpha points if mouse leaves window @@ -135,6 +140,7 @@ public void DrawOnGUI(Rect rect) { TFAlphaControlPoint alphaPoint = tf.alphaControlPoints[movingAlphaPointIndex]; alphaPoint.dataValue = Mathf.Clamp((currentEvent.mousePosition.x - histRect.x) / histRect.width, 0.0f, 1.0f); + alphaPoint.dataValue = Rel2AbsTFValue(alphaPoint.dataValue); alphaPoint.alphaValue = Mathf.Clamp(1.0f - (currentEvent.mousePosition.y - histRect.y) / histRect.height, 0.0f, 1.0f); tf.alphaControlPoints[movingAlphaPointIndex] = alphaPoint; } @@ -144,6 +150,7 @@ public void DrawOnGUI(Rect rect) { TFColourControlPoint colPoint = tf.colourControlPoints[movingColPointIndex]; colPoint.dataValue = Mathf.Clamp((currentEvent.mousePosition.x - paletteRect.x - COLOUR_POINT_WIDTH / 2.0f) / paletteRect.width, 0.0f, 1.0f); + colPoint.dataValue = Rel2AbsTFValue(colPoint.dataValue); tf.colourControlPoints[movingColPointIndex] = colPoint; } @@ -151,7 +158,8 @@ public void DrawOnGUI(Rect rect) for (int iCol = 0; iCol < tf.colourControlPoints.Count; iCol++) { TFColourControlPoint colPoint = tf.colourControlPoints[iCol]; - Rect ctrlBox = new Rect(histRect.x + histRect.width * colPoint.dataValue, histRect.y + histRect.height + 20, COLOUR_POINT_WIDTH, COLOUR_PALETTE_HEIGHT); + float tDataValue = Abs2RelTFValue(colPoint.dataValue); + Rect ctrlBox = new Rect(histRect.x + histRect.width * tDataValue, histRect.y + histRect.height + 20, COLOUR_POINT_WIDTH, COLOUR_PALETTE_HEIGHT); GUI.color = Color.red; GUI.skin.box.fontSize = 6; GUI.Box(ctrlBox, "*"); @@ -162,7 +170,8 @@ public void DrawOnGUI(Rect rect) { const int pointSize = 10; TFAlphaControlPoint alphaPoint = tf.alphaControlPoints[iAlpha]; - Rect ctrlBox = new Rect(histRect.x + histRect.width * alphaPoint.dataValue - pointSize / 2, histRect.y + (1.0f - alphaPoint.alphaValue) * histRect.height - pointSize / 2, pointSize, pointSize); + float tDataValue = Abs2RelTFValue(alphaPoint.dataValue); + Rect ctrlBox = new Rect(histRect.x + histRect.width * tDataValue - pointSize / 2, histRect.y + (1.0f - alphaPoint.alphaValue) * histRect.height - pointSize / 2, pointSize, pointSize); GUI.color = Color.red; GUI.skin.box.fontSize = 6; GUI.Box(ctrlBox, "*"); @@ -211,7 +220,7 @@ public void ClearSelection() public Color? GetSelectedColour() { if (selectedColPointIndex != -1) - return volRendObject.transferFunction.colourControlPoints[selectedColPointIndex].colourValue; + return transferFunctionInstance.transferFunction.colourControlPoints[selectedColPointIndex].colourValue; else return null; } @@ -220,9 +229,9 @@ public void SetSelectedColour(Color colour) { if (selectedColPointIndex != -1) { - TFColourControlPoint colPoint = volRendObject.transferFunction.colourControlPoints[selectedColPointIndex]; + TFColourControlPoint colPoint = transferFunctionInstance.transferFunction.colourControlPoints[selectedColPointIndex]; colPoint.colourValue = colour; - volRendObject.transferFunction.colourControlPoints[selectedColPointIndex] = colPoint; + transferFunctionInstance.transferFunction.colourControlPoints[selectedColPointIndex] = colPoint; } } @@ -230,7 +239,7 @@ public void RemoveSelectedColour() { if (selectedColPointIndex != -1) { - volRendObject.transferFunction.colourControlPoints.RemoveAt(selectedColPointIndex); + transferFunctionInstance.transferFunction.colourControlPoints.RemoveAt(selectedColPointIndex); selectedColPointIndex = -1; } } @@ -241,12 +250,13 @@ public void RemoveSelectedColour() /// Threshold for maximum distance. Points further away than this won't get picked. private int PickColourControlPoint(float position, float maxDistance = 0.03f) { - TransferFunction tf = volRendObject.transferFunction; + TransferFunction tf = transferFunctionInstance.transferFunction; int nearestPointIndex = -1; float nearestDist = 1000.0f; for (int i = 0; i < tf.colourControlPoints.Count; i++) { TFColourControlPoint ctrlPoint = tf.colourControlPoints[i]; + ctrlPoint.dataValue = Abs2RelTFValue(ctrlPoint.dataValue); float dist = Mathf.Abs(ctrlPoint.dataValue - position); if (dist < maxDistance && dist < nearestDist) { @@ -263,12 +273,13 @@ private int PickColourControlPoint(float position, float maxDistance = 0.03f) /// Threshold for maximum distance. Points further away than this won't get picked. private int PickAlphaControlPoint(Vector2 position, float maxDistance = 0.05f) { - TransferFunction tf = volRendObject.transferFunction; + TransferFunction tf = transferFunctionInstance.transferFunction; int nearestPointIndex = -1; float nearestDist = 1000.0f; for (int i = 0; i < tf.alphaControlPoints.Count; i++) { TFAlphaControlPoint ctrlPoint = tf.alphaControlPoints[i]; + ctrlPoint.dataValue = Abs2RelTFValue(ctrlPoint.dataValue); Vector2 ctrlPos = new Vector2(ctrlPoint.dataValue, ctrlPoint.alphaValue); float dist = (ctrlPos - position).magnitude; if (dist < maxDistance && dist < nearestDist) @@ -279,5 +290,15 @@ private int PickAlphaControlPoint(Vector2 position, float maxDistance = 0.05f) } return nearestPointIndex; } + + private float Abs2RelTFValue(float value) + { + return Mathf.InverseLerp(dataRangeMin, dataRangeMax, value); + } + + private float Rel2AbsTFValue(float value) + { + return Mathf.Lerp(dataRangeMin, dataRangeMax, value); + } } } diff --git a/Assets/Scripts/TransferFunction/TransferFunction.cs b/Assets/Scripts/TransferFunction/TransferFunction.cs index 7435959f..e12bf279 100644 --- a/Assets/Scripts/TransferFunction/TransferFunction.cs +++ b/Assets/Scripts/TransferFunction/TransferFunction.cs @@ -12,11 +12,7 @@ public class TransferFunction : ScriptableObject [SerializeField] public List alphaControlPoints = new List(); - private Texture2D texture = null; - private Color[] tfCols; - - private const int TEXTURE_WIDTH = 512; - private const int TEXTURE_HEIGHT = 2; + public bool relativeScale = false; public void AddControlPoint(TFColourControlPoint ctrlPoint) { @@ -27,88 +23,5 @@ public void AddControlPoint(TFAlphaControlPoint ctrlPoint) { alphaControlPoints.Add(ctrlPoint); } - - public Texture2D GetTexture() - { - if (texture == null) - GenerateTexture(); - - return texture; - } - - public void GenerateTexture() - { - if (texture == null) - CreateTexture(); - - List cols = new List(colourControlPoints); - List alphas = new List(alphaControlPoints); - - // Sort lists of control points - cols.Sort((a, b) => (a.dataValue.CompareTo(b.dataValue))); - alphas.Sort((a, b) => (a.dataValue.CompareTo(b.dataValue))); - - // Add colour points at beginning and end - if (cols.Count == 0 || cols[cols.Count - 1].dataValue < 1.0f) - cols.Add(new TFColourControlPoint(1.0f, Color.white)); - if (cols[0].dataValue > 0.0f) - cols.Insert(0, new TFColourControlPoint(0.0f, Color.white)); - - // Add alpha points at beginning and end - if (alphas.Count == 0 || alphas[alphas.Count - 1].dataValue < 1.0f) - alphas.Add(new TFAlphaControlPoint(1.0f, 1.0f)); - if (alphas[0].dataValue > 0.0f) - alphas.Insert(0, new TFAlphaControlPoint(0.0f, 0.0f)); - - int numColours = cols.Count; - int numAlphas = alphas.Count; - int iCurrColour = 0; - int iCurrAlpha = 0; - - for (int iX = 0; iX < TEXTURE_WIDTH; iX++) - { - float t = iX / (float)(TEXTURE_WIDTH - 1); - while (iCurrColour < numColours - 2 && cols[iCurrColour + 1].dataValue < t) - iCurrColour++; - while (iCurrAlpha < numAlphas - 2 && alphas[iCurrAlpha + 1].dataValue < t) - iCurrAlpha++; - - TFColourControlPoint leftCol = cols[iCurrColour]; - TFColourControlPoint rightCol = cols[iCurrColour + 1]; - TFAlphaControlPoint leftAlpha = alphas[iCurrAlpha]; - TFAlphaControlPoint rightAlpha = alphas[iCurrAlpha + 1]; - - float tCol = (Mathf.Clamp(t, leftCol.dataValue, rightCol.dataValue) - leftCol.dataValue) / (rightCol.dataValue - leftCol.dataValue); - float tAlpha = (Mathf.Clamp(t, leftAlpha.dataValue, rightAlpha.dataValue) - leftAlpha.dataValue) / (rightAlpha.dataValue - leftAlpha.dataValue); - - tCol = Mathf.SmoothStep(0.0f, 1.0f, tCol); - tAlpha = Mathf.SmoothStep(0.0f, 1.0f, tAlpha); - - Color pixCol = rightCol.colourValue * tCol + leftCol.colourValue * (1.0f - tCol); - pixCol.a = rightAlpha.alphaValue * tAlpha + leftAlpha.alphaValue * (1.0f - tAlpha); - - for (int iY = 0; iY < TEXTURE_HEIGHT; iY++) - { - tfCols[iX + iY * TEXTURE_WIDTH] = QualitySettings.activeColorSpace == ColorSpace.Linear ? pixCol.linear : pixCol; - } - } - - texture.wrapMode = TextureWrapMode.Clamp; - texture.SetPixels(tfCols); - texture.Apply(); - } - - public Color GetColour(float x) - { - int index = Mathf.RoundToInt(x * TEXTURE_WIDTH); - return tfCols[index]; - } - - private void CreateTexture() - { - TextureFormat texformat = SystemInfo.SupportsTextureFormat(TextureFormat.RGBAHalf) ? TextureFormat.RGBAHalf : TextureFormat.RGBAFloat; - texture = new Texture2D(TEXTURE_WIDTH, TEXTURE_HEIGHT, texformat, false); - tfCols = new Color[TEXTURE_WIDTH * TEXTURE_HEIGHT]; - } } } diff --git a/Assets/Scripts/TransferFunction/TransferFunctionDatabase.cs b/Assets/Scripts/TransferFunction/TransferFunctionDatabase.cs index c044ea80..2231580b 100644 --- a/Assets/Scripts/TransferFunction/TransferFunctionDatabase.cs +++ b/Assets/Scripts/TransferFunction/TransferFunctionDatabase.cs @@ -12,8 +12,9 @@ private struct TF1DSerialisationData public int version; public List colourPoints; public List alphaPoints; + public bool relativeScale; - public const int VERSION_ID = 1; + public const int VERSION_ID = 2; } [System.Serializable] @@ -39,7 +40,6 @@ public static TransferFunction CreateTransferFunction() tf.AddControlPoint(new TFAlphaControlPoint(0.4f, 0.546f)); tf.AddControlPoint(new TFAlphaControlPoint(0.547f, 0.5266f)); - tf.GenerateTexture(); return tf; } @@ -59,12 +59,10 @@ public static TransferFunction LoadTransferFunction(string filepath) } string jsonstring = File.ReadAllText(filepath); TF1DSerialisationData data = JsonUtility.FromJson(jsonstring); - Debug.Log(jsonstring); - Debug.Log(data.colourPoints.ToString()); - Debug.Log(data.alphaPoints.ToString()); TransferFunction tf = ScriptableObject.CreateInstance(); tf.colourControlPoints = data.colourPoints; tf.alphaControlPoints = data.alphaPoints; + tf.relativeScale = data.version < 2 ? true : data.relativeScale; return tf; } @@ -88,6 +86,7 @@ public static void SaveTransferFunction(TransferFunction tf, string filepath) data.version = TF1DSerialisationData.VERSION_ID; data.colourPoints = new List(tf.colourControlPoints); data.alphaPoints = new List(tf.alphaControlPoints); + data.relativeScale = tf.relativeScale; string jsonstring = JsonUtility.ToJson(data); File.WriteAllText(filepath, jsonstring); } diff --git a/Assets/Scripts/TransferFunction/TransferFunctionInstance.cs b/Assets/Scripts/TransferFunction/TransferFunctionInstance.cs new file mode 100644 index 00000000..a19d5437 --- /dev/null +++ b/Assets/Scripts/TransferFunction/TransferFunctionInstance.cs @@ -0,0 +1,161 @@ +using UnityEngine; +using System.Collections.Generic; +using System; + +namespace UnityVolumeRendering +{ + [Serializable] + public class TransferFunctionInstance : ScriptableObject + { + [SerializeField] + public TransferFunction transferFunction; + + public VolumeDataset dataset; + + private Texture2D texture = null; + private Color[] tfCols; + + private const int TEXTURE_WIDTH = 512; + private const int TEXTURE_HEIGHT = 2; + + public void Initialise(TransferFunction tf, VolumeDataset dataset) + { + this.transferFunction = tf; + this.dataset = dataset; + EnsureAbsoluteScale(); + } + + public void AddControlPoint(TFColourControlPoint ctrlPoint) + { + transferFunction.AddControlPoint(ctrlPoint); + } + + public void AddControlPoint(TFAlphaControlPoint ctrlPoint) + { + transferFunction.AddControlPoint(ctrlPoint); + } + + public Texture2D GetTexture() + { + if (texture == null) + GenerateTexture(); + + return texture; + } + + public void EnsureAbsoluteScale() + { + if (transferFunction.relativeScale) + { + float minValue = dataset.GetMinDataValue(); + float maxValue = dataset.GetMaxDataValue(); + for (int i = 0; i < transferFunction.colourControlPoints.Count; i++) + { + TFColourControlPoint point = transferFunction.colourControlPoints[i]; + point.dataValue = Mathf.Lerp(minValue, maxValue, point.dataValue); + transferFunction.colourControlPoints[i] = point; + } + for (int i = 0; i < transferFunction.alphaControlPoints.Count; i++) + { + TFAlphaControlPoint point = transferFunction.alphaControlPoints[i]; + point.dataValue = Mathf.Lerp(minValue, maxValue, point.dataValue); + transferFunction.alphaControlPoints[i] = point; + } + transferFunction.relativeScale = false; + } + } + + public void GenerateTexture() + { + if (texture == null) + CreateTexture(); + + List cols = new List(transferFunction.colourControlPoints); + List alphas = new List(transferFunction.alphaControlPoints); + + if (!transferFunction.relativeScale) + { + float minValue = dataset.GetMinDataValue(); + float maxValue = dataset.GetMaxDataValue(); + for (int i = 0; i < cols.Count; i++) + { + TFColourControlPoint point = cols[i]; + point.dataValue = (Mathf.Clamp(point.dataValue, minValue, maxValue) - minValue) / (maxValue - minValue); + cols[i] = point; + } + for (int i = 0; i < alphas.Count; i++) + { + TFAlphaControlPoint point = alphas[i]; + point.dataValue = (Mathf.Clamp(point.dataValue, minValue, maxValue) - minValue) / (maxValue - minValue); + alphas[i] = point; + } + } + + // Sort lists of control points + cols.Sort((a, b) => (a.dataValue.CompareTo(b.dataValue))); + alphas.Sort((a, b) => (a.dataValue.CompareTo(b.dataValue))); + + // Add colour points at beginning and end + if (cols.Count == 0 || cols[cols.Count - 1].dataValue < 1.0f) + cols.Add(new TFColourControlPoint(1.0f, Color.white)); + if (cols[0].dataValue > 0.0f) + cols.Insert(0, new TFColourControlPoint(0.0f, Color.white)); + + // Add alpha points at beginning and end + if (alphas.Count == 0 || alphas[alphas.Count - 1].dataValue < 1.0f) + alphas.Add(new TFAlphaControlPoint(1.0f, 1.0f)); + if (alphas[0].dataValue > 0.0f) + alphas.Insert(0, new TFAlphaControlPoint(0.0f, 0.0f)); + + int numColours = cols.Count; + int numAlphas = alphas.Count; + int iCurrColour = 0; + int iCurrAlpha = 0; + + for (int iX = 0; iX < TEXTURE_WIDTH; iX++) + { + float t = iX / (float)(TEXTURE_WIDTH - 1); + while (iCurrColour < numColours - 2 && cols[iCurrColour + 1].dataValue < t) + iCurrColour++; + while (iCurrAlpha < numAlphas - 2 && alphas[iCurrAlpha + 1].dataValue < t) + iCurrAlpha++; + + TFColourControlPoint leftCol = cols[iCurrColour]; + TFColourControlPoint rightCol = cols[iCurrColour + 1]; + TFAlphaControlPoint leftAlpha = alphas[iCurrAlpha]; + TFAlphaControlPoint rightAlpha = alphas[iCurrAlpha + 1]; + + float tCol = (Mathf.Clamp(t, leftCol.dataValue, rightCol.dataValue) - leftCol.dataValue) / (rightCol.dataValue - leftCol.dataValue); + float tAlpha = (Mathf.Clamp(t, leftAlpha.dataValue, rightAlpha.dataValue) - leftAlpha.dataValue) / (rightAlpha.dataValue - leftAlpha.dataValue); + + tCol = Mathf.SmoothStep(0.0f, 1.0f, tCol); + tAlpha = Mathf.SmoothStep(0.0f, 1.0f, tAlpha); + + Color pixCol = rightCol.colourValue * tCol + leftCol.colourValue * (1.0f - tCol); + pixCol.a = rightAlpha.alphaValue * tAlpha + leftAlpha.alphaValue * (1.0f - tAlpha); + + for (int iY = 0; iY < TEXTURE_HEIGHT; iY++) + { + tfCols[iX + iY * TEXTURE_WIDTH] = QualitySettings.activeColorSpace == ColorSpace.Linear ? pixCol.linear : pixCol; + } + } + + texture.wrapMode = TextureWrapMode.Clamp; + texture.SetPixels(tfCols); + texture.Apply(); + } + + public Color GetColour(float x) + { + int index = Mathf.RoundToInt(x * TEXTURE_WIDTH); + return tfCols[index]; + } + + private void CreateTexture() + { + TextureFormat texformat = SystemInfo.SupportsTextureFormat(TextureFormat.RGBAHalf) ? TextureFormat.RGBAHalf : TextureFormat.RGBAFloat; + texture = new Texture2D(TEXTURE_WIDTH, TEXTURE_HEIGHT, texformat, false); + tfCols = new Color[TEXTURE_WIDTH * TEXTURE_HEIGHT]; + } + } +} diff --git a/Assets/Scripts/TransferFunction/TransferFunctionInstance.cs.meta b/Assets/Scripts/TransferFunction/TransferFunctionInstance.cs.meta new file mode 100644 index 00000000..bf57122a --- /dev/null +++ b/Assets/Scripts/TransferFunction/TransferFunctionInstance.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63c18ac5e6fdf4d4d855bea3eeae360d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/VolumeObject/VolumeObjectFactory.cs b/Assets/Scripts/VolumeObject/VolumeObjectFactory.cs index 24598528..e5788acc 100644 --- a/Assets/Scripts/VolumeObject/VolumeObjectFactory.cs +++ b/Assets/Scripts/VolumeObject/VolumeObjectFactory.cs @@ -49,24 +49,12 @@ private static void CreateObjectInternal(VolumeDataset dataset, GameObject meshC volObj.meshRenderer = meshRenderer; volObj.dataset = dataset; - const int noiseDimX = 512; - const int noiseDimY = 512; - Texture2D noiseTexture = NoiseTextureGenerator.GenerateNoiseTexture(noiseDimX, noiseDimY); - TransferFunction tf = TransferFunctionDatabase.CreateTransferFunction(); - Texture2D tfTexture = tf.GetTexture(); - volObj.transferFunction = tf; - TransferFunction2D tf2D = TransferFunctionDatabase.CreateTransferFunction2D(); - volObj.transferFunction2D = tf2D; - meshRenderer.sharedMaterial.SetTexture("_GradientTex", null); - meshRenderer.sharedMaterial.SetTexture("_NoiseTex", noiseTexture); - meshRenderer.sharedMaterial.SetTexture("_TFTex", tfTexture); + volObj.Initialise(dataset, tf); - meshRenderer.sharedMaterial.EnableKeyword("MODE_DVR"); - meshRenderer.sharedMaterial.DisableKeyword("MODE_MIP"); - meshRenderer.sharedMaterial.DisableKeyword("MODE_SURF"); + volObj.transferFunction2D = tf2D; meshContainer.transform.localScale = dataset.scale; meshContainer.transform.localRotation = dataset.rotation; diff --git a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs index 52f5cff2..35450b2f 100644 --- a/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs +++ b/Assets/Scripts/VolumeObject/VolumeRenderedObject.cs @@ -8,7 +8,7 @@ namespace UnityVolumeRendering public class VolumeRenderedObject : MonoBehaviour { [SerializeField, HideInInspector] - public TransferFunction transferFunction; + public TransferFunctionInstance transferFunctionInstance; [SerializeField, HideInInspector] public TransferFunction2D transferFunction2D; @@ -58,6 +58,28 @@ public class VolumeRenderedObject : MonoBehaviour private SemaphoreSlim updateMatLock = new SemaphoreSlim(1, 1); + public void Initialise(VolumeDataset dataset, TransferFunction tf) + { + this.dataset = dataset; + this.transferFunctionInstance = ScriptableObject.CreateInstance(); + this.transferFunctionInstance.transferFunction = tf; + this.transferFunctionInstance.dataset = dataset; + + Texture2D tfTexture = transferFunctionInstance.GetTexture(); + + const int noiseDimX = 512; + const int noiseDimY = 512; + Texture2D noiseTexture = NoiseTextureGenerator.GenerateNoiseTexture(noiseDimX, noiseDimY); + + meshRenderer.sharedMaterial.SetTexture("_GradientTex", null); + meshRenderer.sharedMaterial.SetTexture("_NoiseTex", noiseTexture); + meshRenderer.sharedMaterial.SetTexture("_TFTex", tfTexture); + + meshRenderer.sharedMaterial.EnableKeyword("MODE_DVR"); + meshRenderer.sharedMaterial.DisableKeyword("MODE_MIP"); + meshRenderer.sharedMaterial.DisableKeyword("MODE_SURF"); + } + public SlicingPlane CreateSlicingPlane() { GameObject sliceRenderingPlane = GameObject.Instantiate(Resources.Load("SlicingPlane")); @@ -69,7 +91,7 @@ public SlicingPlane CreateSlicingPlane() sliceMeshRend.material = new Material(sliceMeshRend.sharedMaterial); Material sliceMat = sliceRenderingPlane.GetComponent().sharedMaterial; sliceMat.SetTexture("_DataTex", dataset.GetDataTexture()); - sliceMat.SetTexture("_TFTex", transferFunction.GetTexture()); + sliceMat.SetTexture("_TFTex", transferFunctionInstance.GetTexture()); sliceMat.SetMatrix("_parentInverseMat", transform.worldToLocalMatrix); sliceMat.SetMatrix("_planeMat", Matrix4x4.TRS(sliceRenderingPlane.transform.position, sliceRenderingPlane.transform.rotation, transform.lossyScale)); // TODO: allow changing scale @@ -105,8 +127,8 @@ public async Task SetTransferFunctionModeAsync(TFRenderMode mode, IProgressHandl progressHandler.StartStage(0.3f, "Generating transfer function texture"); tfRenderMode = mode; - if (tfRenderMode == TFRenderMode.TF1D && transferFunction != null) - transferFunction.GenerateTexture(); + if (tfRenderMode == TFRenderMode.TF1D && transferFunctionInstance != null) + transferFunctionInstance.GenerateTexture(); else if (transferFunction2D != null) transferFunction2D.GenerateTexture(); progressHandler.EndStage(); @@ -261,7 +283,9 @@ public void SetCubicInterpolationEnabled(bool enable) public void SetTransferFunction(TransferFunction tf) { - this.transferFunction = tf; + this.transferFunctionInstance.transferFunction = tf; + this.transferFunctionInstance.EnsureAbsoluteScale(); + this.transferFunctionInstance.GenerateTexture(); UpdateMaterialProperties(); } @@ -272,12 +296,12 @@ public async Task SetTransferFunctionAsync(TransferFunction tf, IProgressHandler meshRenderer.sharedMaterial = new Material(Shader.Find("VolumeRendering/DirectVolumeRenderingShader")); meshRenderer.sharedMaterial.SetTexture("_DataTex", dataset.GetDataTexture()); } - if (transferFunction == null) + if (transferFunctionInstance == null) { - transferFunction = TransferFunctionDatabase.CreateTransferFunction(); + transferFunctionInstance = ScriptableObject.CreateInstance(); } - this.transferFunction = tf; + this.transferFunctionInstance.transferFunction = tf; await UpdateMaterialPropertiesAsync(progressHandler); } @@ -312,7 +336,7 @@ private void UpdateMatInternal() } else { - meshRenderer.sharedMaterial.SetTexture("_TFTex", transferFunction.GetTexture()); + meshRenderer.sharedMaterial.SetTexture("_TFTex", transferFunctionInstance.GetTexture()); meshRenderer.sharedMaterial.DisableKeyword("TF2D_ON"); } diff --git a/Assets/Scripts/VolumeQueries/VolumeRaycaster.cs b/Assets/Scripts/VolumeQueries/VolumeRaycaster.cs index 9b0bb2c7..1cc6d97d 100644 --- a/Assets/Scripts/VolumeQueries/VolumeRaycaster.cs +++ b/Assets/Scripts/VolumeQueries/VolumeRaycaster.cs @@ -86,7 +86,7 @@ private bool RaycastObject(Ray worldSpaceRay, VolumeRenderedObject volumeObject, // Normalise to 0.0-1.0 (TF uses normalised scale) float normalisedValue = Mathf.InverseLerp(dataset.GetMinDataValue(), dataset.GetMaxDataValue(), value); // Check if value is within visibility window, and TF gives us a visible colour. - if (value >= minValue && value <= maxValue && volumeObject.transferFunction.GetColour(normalisedValue).a > 0.0f) + if (value >= minValue && value <= maxValue && volumeObject.transferFunctionInstance.GetColour(normalisedValue).a > 0.0f) { hit.point = volumeObject.transform.TransformPoint(position); hit.distance = (worldSpaceRay.origin - volumeObject.transform.TransformPoint(position)).magnitude;