/*============================================================================= Project: SharpImage Module: siVolumeRenderer.cs Language: C# Author: Dan Mueller Date: $Date: 2008-01-09 16:23:10 +1000 (Wed, 09 Jan 2008) $ Revision: $Revision: 29 $ Copyright (c) Queensland University of Technology (QUT) 2007. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the above copyright notices for more information. =============================================================================*/ using System; using System.IO; using System.Text; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Design; using System.Drawing.Imaging; using System.Drawing.Drawing2D; using System.Windows.Forms; using System.ComponentModel; using Tao.OpenGl; using Tao.FreeGlut; using Tao.Platform.Windows; using SharpImage.Main; using SharpImage.Forms; namespace SharpImage.Rendering { public class siVolumeRenderer : siRenderer { #region Constants //===================================================================== protected static readonly Color DEFAULT_BACK_COLOR = Color.White; //public const int MAXIMUM_IMAGE_TAGS_AND_ATTRIBUTES = 10; private const long SHADER_WATCHER_TIME = 5; // In seconds //===================================================================== #endregion #region Instance Variables //===================================================================== private bool m_IsDisposed = false; private bool m_HasRenderedFirstTime = false; private string m_DefaultFragmentProgramPath = null; FileSystemWatcher m_ShaderWatcher = null; TimeSpan m_ShaderLastChanged = TimeSpan.Zero; bool m_IsAskingUserToReloadShader = false; private siTextureGeometryGenerator m_TexureGeometryGenerator = new siTextureGeometryGenerator(); private siSpatialObjectRenderingHelper m_SpatialObjectHelper = new siSpatialObjectRenderingHelper(); private long m_TickFrequency = 0; private long m_TickCurrent = 0; private long m_TickPrevious = 0; private long m_FrameCount = 0; private float m_FrameRate = 0; //===================================================================== #endregion #region Construction and Disposal //===================================================================== /// /// Default constructor. /// public siVolumeRenderer(IApplication parent) : base("Volume Renderer", parent, new siOpenGlRendererForm()) { // Setup member variables this.BackColor = DEFAULT_BACK_COLOR; // Setup Metadata this.m_Properties = new siVolumeProperties(this); this.Metadata["IsTranslating"] = false; this.Metadata["IsRotating"] = false; this.Metadata["Shader"] = null; this.Metadata["TransferFunction"] = null; this.Metadata["VertexProgram"] = null; this.Metadata["FragmentProgram"] = null; this.Metadata["ForcePowerOfTwoTextureSize"] = false; // Watch for a change in the sampling rate this.Form.ResizeBegin += new EventHandler(Form_ResizeBegin); this.Form.ResizeEnd += new EventHandler(Form_ResizeEnd); // Watch for changes in the shader source this.m_ShaderLastChanged = new TimeSpan(DateTime.Now.Ticks); this.m_ShaderWatcher = new FileSystemWatcher(); this.m_ShaderWatcher.Path = "C:/"; this.m_ShaderWatcher.Filter = "ThisStringWillNotMatchAnyFiles.xxx"; this.m_ShaderWatcher.IncludeSubdirectories = false; this.m_ShaderWatcher.NotifyFilter = NotifyFilters.LastWrite; this.m_ShaderWatcher.EnableRaisingEvents = true; this.m_ShaderWatcher.Changed += new FileSystemEventHandler(watcher_Changed); } /// /// Deconstructor. /// ~siVolumeRenderer() { // Call Dispose with false. Since we're in the // destructor call, the managed resources will be // disposed of anyways. this.Dispose(false); } /// /// Dispose of any resources. /// public override void Dispose() { // Dispose both managed and unmanaged resources this.Dispose(true); // Tell the GC that the Finalize process no longer needs // to be run for this object. GC.SuppressFinalize(this); } /// /// Dispose of the managed resources. /// /// protected virtual void Dispose(bool disposeManagedResources) { // Check that we are not already disposed if (this.m_IsDisposed) return; // Only get here if not already disposed if (disposeManagedResources) { this.Form.Dispose(); } // Dispose of the file watcher if (this.m_ShaderWatcher != null) { this.m_ShaderWatcher.Changed -= new FileSystemEventHandler(watcher_Changed); this.m_ShaderWatcher.Dispose(); this.m_ShaderWatcher = null; } // Dispose base base.Dispose(); // Set that this have been disposed this.m_IsDisposed = true; } //===================================================================== #endregion #region Properties //===================================================================== #region Properties //===================================================================== private siVolumeProperties m_Properties; /// /// Gets the properties object associated with this renderer. /// [Browsable(false)] public siVolumeProperties Properties { get { return this.m_Properties; } set { this.m_Properties = value; } } //===================================================================== #endregion #region Shader //===================================================================== /// /// Gets the vertex and fragment shader programs replacing the OpenGL /// fixed functionality. /// [Browsable(false)] public siOpenGlShader Shader { get { return this.Metadata["Shader"] as siOpenGlShader; } } //===================================================================== #endregion #region TransferFunction //===================================================================== /// /// Gets the transfer function associated with this volume renderer. /// [Browsable(false)] public siTransferFunction TransferFunction { get { return this.Metadata["TransferFunction"] as siTransferFunction; } } //===================================================================== #endregion #region PowerOfTwoTextureSize //===================================================================== /// /// Gets/sets if the texture data should be forced to be a power of two. /// The default is false (if supported by the hardware). /// [Browsable(false)] public bool ForcePowerOfTwoTextureSize { get { return (bool)this.Metadata["ForcePowerOfTwoTextureSize"]; } set { this.Metadata["ForcePowerOfTwoTextureSize"] = value; } } //===================================================================== #endregion #region Geometry //===================================================================== /// /// Gets/sets the blending type. /// [Browsable(false)] public siTextureGeometryGenerator GeometryGenerator { get { return this.m_TexureGeometryGenerator; } } //===================================================================== #endregion //===================================================================== #endregion #region Public Methods //===================================================================== /// /// Initialise the Renderer. /// public override void Initialise() { // Call the base base.Initialise(); // Init the OpenGL form and control this.Form.Initialise(this); //Add filename to form text this.TitleText = Path.GetFileName(this.Inputs[0].Name); // Setup trackball this.Properties.TrackBall.Reset(); // Get the tick frequency for fps calculations // NOTE: This value will be constant for the machine operation if (!Kernel.QueryPerformanceFrequency(out this.m_TickFrequency)) throw new ApplicationException("The machine tick frequency could not be queried using Kernel32.dll QueryPerformanceFrequency()."); // Compute the model size this.Properties.ComputeModelSize(); // Init any input images this.InitialiseInputImages(); // Init for texture rendering if (this.ContainsMetadata("ImageValue")) { // Do general OpenGL initialisation Gl.glEnable(Gl.GL_DEPTH_TEST); // Enable depth testing Gl.glDepthFunc(Gl.GL_LEQUAL); Gl.glEnable(Gl.GL_NORMALIZE); // Make Normals [0..1] Gl.glPixelStorei(Gl.GL_UNPACK_ALIGNMENT, 1); // Tightly pack pixels Gl.glPixelStorei(Gl.GL_PACK_ALIGNMENT, 1); // Init shader programs if (!this.ContainsMetadata("VertexProgram", "FragmentProgram")) throw new ApplicationException("The renderer must be tagged with vertex and fragment shader program file paths using the Metadata fields: VertexProgram and FragmentProgram."); string pathVertex = (string)this.Metadata["VertexProgram"]; string pathFragment = (string)this.Metadata["FragmentProgram"]; string shaderInfo = "Loading shader program: {0}, {1}"; this.ParentApplication.ScriptConsole.WriteLine(string.Format(shaderInfo, Path.GetFileName(pathVertex), Path.GetFileName(pathFragment))); siOpenGlShader shader = new siOpenGlShader(); shader.Initialise(pathVertex, pathFragment); this.Metadata["Shader"] = shader; // Force the starting sampling rate to be uploaded to the shader this.SamplingRateChanged(this.Properties.SamplingRate); // Load OpenGL 3D texture extensions this.TestOpenGlExtensionsSupported(); // Load the images as textures this.LoadImagesAsTextures(); // Init the transfer function if (this.ContainsMetadata("TransferFunction")) { this.TransferFunction.Modified += new siTransferFunction.siTransferFunctionModifiedHandler(TransferFunction_Modified); this.TransferFunction.EditStarted += new siTransferFunction.siTransferFunctionHandler(TransferFunction_EditStarted); this.TransferFunction.EditFinished += new siTransferFunction.siTransferFunctionHandler(TransferFunction_EditFinished); this.InitialiseTransferFunction(); this.LoadTransferFunctionAsTexture(false); } // Setup the shader texture units this.SetShaderTextureUnits(); // Setup the lighting environment this.Properties.LightColorAmbient = Color.FromArgb(255, 128, 128, 128); this.Properties.LightColorDiffuse = Color.FromArgb(255, 128, 128, 128); this.Properties.LightColorSpecular = Color.FromArgb(255, 64, 64, 64); this.Properties.LightColorSpecularExponent = 5.0f; this.Properties.NormalOffset = 0.004f; this.Properties.Factor1 = 0.0f; this.Properties.Factor2 = 0.0f; this.Properties.Factor3 = 0.0f; // Init the geometry generator itk.itkImageBase imageValue = this.GetMetadataAsImage("ImageValue"); this.m_TexureGeometryGenerator.Initialise(imageValue.Size, imageValue.Spacing, this.Properties.ModelSize); } // Refresh this.Repaint(); this.Form.ForceResize(); } /// /// Forces the Renderer to repaint. /// public override void Repaint() { base.Repaint(); if (this.Form != null) this.Form.Repaint(); } /// /// Close the Renderer and dispose of all resources. /// public override void Close() { base.Close(); this.Dispose(); } /// /// Get the image currently displayed by the screen. /// /// public override Bitmap GetScreenCapture() { int width = this.ViewportSize.Width; int height = this.ViewportSize.Height; // Adjust width and height to be a factor of 4 if (width % 4 != 0) width -= width % 4; if (height % 4 != 0) height -= height % 4; // Create and lock bitmap Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb); Rectangle rect = new Rectangle(0, 0, width, height); BitmapData bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); // "Tightly" pack pixels Gl.glPixelStorei(Gl.GL_UNPACK_ALIGNMENT, 1); Gl.glPixelStorei(Gl.GL_PACK_ALIGNMENT, 1); // Force a flush / swap buffers //this..Invalidate(); // Read buffer Gl.glReadPixels(0, 0, width, height, Gl.GL_BGR, Gl.GL_UNSIGNED_BYTE, bitmapData.Scan0); // Unlock and flip bitmap.UnlockBits(bitmapData); bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY); //Return return bitmap; } /// /// Update the texture with the given input image. /// /// Either "Value", "Gradient", "Extra1", or "Extra2". /// public void UpdateInput(String name, itk.itkImageBase input) { // Check dimensions if (input.Dimension != 3) throw new ApplicationException("The input image '" + Path.GetFileName(input.Name) + "' is not a 3D image."); // Ensure size itk.itkImageBase validInput = null; if (this.ForcePowerOfTwoTextureSize || !this.IsNonPowerOfTwoTextureSizeSupported()) validInput = this.EnsureImageIsPowerOfTwoSize(input); else validInput = input; // Rescale the image (this will only occur if required) validInput = this.RescaleImage(validInput); // Copy metadata keys to validInput if (validInput != input) { foreach (KeyValuePair pair in input.Metadata) validInput.Metadata[pair.Key] = pair.Value; } // Check that current is not input itk.itkImageBase current = this.GetMetadataAsImage("Image" + name); if (validInput == current) return; // Copy metadata from current input to validInput foreach (KeyValuePair pair in current.Metadata) validInput.Metadata[pair.Key] = pair.Value; // Set the input this.Metadata["Image" + name] = validInput; // Upload the input as a texture this.LoadImageAsTexture(validInput, false); this.Repaint(); } //===================================================================== #endregion #region Rendering Methods //===================================================================== protected override void OnPaint(PaintEventArgs e) { // Give the base class a chance to paint base.OnPaint(e); // Only render if we are modified if (!this.IsModified) return; // Compute the fps this.m_FrameCount++; Kernel.QueryPerformanceCounterFast(out this.m_TickCurrent); long tickDifference = this.m_TickCurrent - this.m_TickPrevious; if (tickDifference > this.m_TickFrequency) { this.m_FrameRate = (float)(this.m_FrameCount * this.m_TickFrequency) / (float)tickDifference; this.m_TickPrevious = this.m_TickCurrent; this.m_FrameCount = 0; this.TitleText = Path.GetFileName(this.Inputs[0].Name) + " (" + this.m_FrameRate.ToString("00.0") + "fps)"; } // Do rendering this.RenderInitialise(); this.RenderBoxOutline(); this.RenderInputs(); this.RenderFinalise(); // Reset modified and first time flags this.m_HasRenderedFirstTime = true; this.IsModified = false; } /// /// Initialises for the DrawInput method. /// private void RenderInitialise() { // Force background to tbe drawn Gl.glMatrixMode(Gl.GL_MODELVIEW); Gl.glDrawBuffer(Gl.GL_BACK); Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT); float[] back = siColorHelper.To4fv(this.BackColor); Gl.glClearColor(back[0], back[1], back[2], back[3]); //Gl.glClearDepth(1.0f); Gl.glShadeModel(Gl.GL_SMOOTH); Gl.glHint(Gl.GL_PERSPECTIVE_CORRECTION_HINT, Gl.GL_NICEST); // Change what we are looking at Gl.glMatrixMode(Gl.GL_MODELVIEW); Gl.glLoadIdentity(); Glu.gluLookAt(this.Properties.EyeX, this.Properties.EyeY, this.Properties.EyeZ, this.Properties.CenterX, this.Properties.CenterY, this.Properties.CenterZ, this.Properties.UpX, this.Properties.UpY, this.Properties.UpZ); // Set Frustum Gl.glMatrixMode(Gl.GL_PROJECTION); Gl.glLoadIdentity(); Gl.glFrustum(this.Properties.FrustrumLeft, this.Properties.FrustrumRight, this.Properties.FrustrumBottom, this.Properties.FrustrumTop, this.Properties.FrustrumFront, this.Properties.FrustrumBack); // Translate Gl.glMatrixMode(Gl.GL_MODELVIEW); double[] transdata = this.Properties.TransformTranslate.Data; Gl.glTranslated(transdata[0], transdata[1], transdata[2]); // Rotate Gl.glMatrixMode(Gl.GL_MODELVIEW); Gl.glMultMatrixd(this.Properties.TrackBall.GetRotationMatrixAsArray()); } /// /// Finialises the drawing /// private void RenderFinalise() { // Flush Gl.glFlush(); } /// /// Render a box outlining the volume. /// private void RenderBoxOutline() { // Exit if we are not rendering the box if (!this.Properties.BoxOutlineEnabled) return; // Begin Gl.glMatrixMode(Gl.GL_MODELVIEW); Gl.glPushMatrix(); Gl.glDisable(Gl.GL_LIGHTING); //Gl.glDisable(Gl.GL_BLEND); Gl.glEnable(Gl.GL_BLEND); Gl.glBlendFunc(Gl.GL_SRC_ALPHA, Gl.GL_ONE_MINUS_SRC_ALPHA); //Gl.glDisable(Gl.GL_DEPTH_TEST); itk.itkArray size = this.Properties.ModelSize; Gl.glTranslated(-0.5 * size[0], -0.5 * size[1], -0.5 * size[2]); Gl.glBegin(Gl.GL_LINES); { Gl.glColor4fv(siColorHelper.To4fv(this.Properties.BoxOutlineColor)); // Back square Gl.glVertex3d(0, 0, 0); Gl.glVertex3d(0, size[1], 0); Gl.glVertex3d(0, size[1], 0); Gl.glVertex3d(size[0], size[1], 0); Gl.glVertex3d(size[0], size[1], 0); Gl.glVertex3d(size[0], 0, 0); Gl.glVertex3d(size[0], 0, 0); Gl.glVertex3d(0, 0, 0); // Front Square Gl.glVertex3d(0, 0, size[2]); Gl.glVertex3d(0, size[1], size[2]); Gl.glVertex3d(0, size[1], size[2]); Gl.glVertex3d(size[0], size[1], size[2]); Gl.glVertex3d(size[0], size[1], size[2]); Gl.glVertex3d(size[0], 0, size[2]); Gl.glVertex3d(size[0], 0, size[2]); Gl.glVertex3d(0, 0, size[2]); // Missing bits Gl.glVertex3d(0, 0, 0); Gl.glVertex3d(0, 0, size[2]); Gl.glVertex3d(0, size[1], 0); Gl.glVertex3d(0, size[1], size[2]); Gl.glVertex3d(size[0], size[1], 0); Gl.glVertex3d(size[0], size[1], size[2]); Gl.glVertex3d(size[0], 0, 0); Gl.glVertex3d(size[0], 0, size[2]); } Gl.glEnd(); Gl.glEnd(); Gl.glDisable(Gl.GL_BLEND); Gl.glEnable(Gl.GL_DEPTH_TEST); Gl.glPopMatrix(); } /// /// Renders the inputs /// private void RenderInputs() { // Dispatch rendering of SpatialObjects foreach (itk.itkDataObject input in this.Inputs) { if (m_SpatialObjectHelper.CanRender(input)) m_SpatialObjectHelper.Render(this, input); } // Render the texture interpolation geometry if (this.ContainsMetadata("ImageValue")) this.RenderTextureGeometry(); } //===================================================================== #endregion #region Transfer Function Methods //===================================================================== private void TransferFunction_Modified(siTransferFunction sender, bool partial) { if (!partial) { // Reload the transfer function image data to the GPU this.LoadTransferFunctionAsTexture(true); this.Repaint(); } } private void TransferFunction_EditStarted(siTransferFunction sender) { this.Properties.SamplingRate.MakeCurrentLowQuality(); } private void TransferFunction_EditFinished(siTransferFunction sender) { this.Properties.SamplingRate.MakeCurrentHighQuality(); } private void InitialiseTransferFunction() { if (this.ContainsMetadata("TransferFunction")) { // NOTE: We assume the transfer function is a power of two size. // Setup texture metadata int[] texnames = new int[1]; Gl.glGenTextures(1, texnames); this.TransferFunction.Image.Metadata["TextureType"] = "TransferFunction"; this.TransferFunction.Image.Metadata["TextureName"] = texnames[0]; this.TransferFunction.Image.Metadata["TextureUnit"] = Gl.GL_TEXTURE0_ARB; this.TransferFunction.Image.Metadata["TextureInterpolation"] = Gl.GL_NEAREST; } } /// /// Load the transfer function image data to the GPU. /// private void LoadTransferFunctionAsTexture(bool silent) { if (this.TransferFunction != null && this.TransferFunction.Image != null && !this.TransferFunction.Image.IsDisposed) { this.TransferFunction.PaintToImage(); //this.TransferFunction.Image.Write("C:/Temp/TransferFunction.mhd"); this.LoadImageAsTexture(this.TransferFunction.Image, silent); } } //===================================================================== #endregion #region Texture Initialisation Methods //===================================================================== /// /// Initialises the input images. /// This method looks at the input image tags, searching for: /// "Value": for a value image /// "Gradient": for a gradient image /// "Extra1": extra data /// "Extra2": extra data. /// This method then creates similar tags in the renderer, /// pointing to the associated input image. /// private void InitialiseInputImages() { foreach (itk.itkDataObject input in this.Inputs) { if (input is itk.itkImageBase) { // Cast to image itk.itkImageBase validInput = null; itk.itkImageBase inputAsImage = input as itk.itkImageBase; // Check dimensions if (inputAsImage.Dimension != 3) throw new ApplicationException("The input image '" + Path.GetFileName(input.Name) + "' is not a 3D image."); // Ensure size if (this.ForcePowerOfTwoTextureSize || !this.IsNonPowerOfTwoTextureSizeSupported()) validInput = this.EnsureImageIsPowerOfTwoSize(inputAsImage); else validInput = inputAsImage; // Rescale the image (this will only occur if required) validInput = this.RescaleImage(validInput); // Copy metadata keys to validInput if (validInput != inputAsImage) { foreach (KeyValuePair pair in input.Metadata) validInput.Metadata[pair.Key] = pair.Value; } // Set renderer metadata if (input.Metadata.ContainsKey("Value")) this.Metadata["ImageValue"] = validInput; else if (input.Metadata.ContainsKey("Gradient")) this.Metadata["ImageGradient"] = validInput; else if (input.Metadata.ContainsKey("Extra1")) this.Metadata["ImageExtra1"] = validInput; else if (input.Metadata.ContainsKey("Extra2")) this.Metadata["ImageExtra2"] = validInput; } } } /// /// Ensures each dimension of the given image is a power of two. /// If not, /// /// private itk.itkImageBase EnsureImageIsPowerOfTwoSize(itk.itkImageBase image) { // Determine if the image must be padded bool imageMustBePadded = false; itk.itkSize newSize = new itk.itkSize((int[])image.Size.Data.Clone()); for (int i = 0; i < image.Size.Dimension; i++) { newSize[i] = siMathHelper.RoundToNextHighestPowerOfTwo(image.Size[i]); if (image.Size[i] != newSize[i]) imageMustBePadded = true; } if (imageMustBePadded) { // Pad the image string info = "Padding {0}: From {1} to {2}"; this.ParentApplication.ScriptConsole.WriteLine(string.Format(info, Path.GetFileName(image.Name), image.Size, newSize)); itk.itkImage imagePadded = itk.itkImage.New(image); itk.itkConstantPadImageFilter filter = itk.itkConstantPadImageFilter.New(image, image); filter.SetInput(image); uint[] upper = new uint[image.Size.Dimension]; uint[] lower = new uint[image.Size.Dimension]; // We want all zeros for (int i = 0; i < image.Size.Dimension; i++) upper[i] = (uint)(newSize[i] - image.Size[i]); filter.PadUpperBound = upper; filter.PadLowerBound = lower; filter.Update(); filter.GetOutput(imagePadded); imagePadded.Name = image.Name; image.DisconnectPipeline(); imagePadded.DisconnectPipeline(); filter.Dispose(); return imagePadded; } else { return image; } } /// /// Ensures the given image is spread over the input pixel type. /// For example: for signed short, the image is rescaled from [-32768, 32767]. /// For example: for float, the image is rescaled from [0.0, 1.0]. /// /// /// private itk.itkImageBase RescaleImage(itk.itkImageBase image) { // Check if rescaling is required if (image.PixelType.IsChar) return image; // Get the min/max values double minValue = 0.0; double maxValue = 1.0; if (image.Metadata.ContainsKey("MinimumValueAsD") && image.Metadata.ContainsKey("MaximumValueAsD")) { // Get min/max values from the image minValue = (double)image.Metadata["MinimumValueAsD"]; maxValue = (double)image.Metadata["MaximumValueAsD"]; } else { // Compute the min/max values itk.itkMinimumMaximumImageCalculator calcMinMax = itk.itkMinimumMaximumImageCalculator.New(image); calcMinMax.SetImage(image); calcMinMax.Compute(); minValue = calcMinMax.Minimum.ValueAsD; maxValue = calcMinMax.Maximum.ValueAsD; image.DisconnectPipeline(); calcMinMax.Dispose(); } // Check if the rescale needs to be done switch (image.PixelType.TypeAsEnum) { case itk.itkPixelTypeEnum.SignedShort: if (minValue == Int16.MinValue && maxValue == Int16.MaxValue) { image.Metadata["RequiresScaleAndBias"] = true; return image; } break; case itk.itkPixelTypeEnum.UnsignedShort: if (minValue == UInt16.MinValue && maxValue == UInt16.MaxValue) return image; break; case itk.itkPixelTypeEnum.Float: case itk.itkPixelTypeEnum.Double: if (minValue == 0.0 && maxValue == 1.0) return image; break; default: throw new NotSupportedException("The given pixel type is not supported: " + image.PixelType.ToString()); } // NOTE: We only get here if the image needs to be rescaled // Setup for the rescaling bool requiresScaleAndBias = false; itk.itkImage imageRescaled = itk.itkImage.New(image); itk.itkRescaleIntensityImageFilter filter = itk.itkRescaleIntensityImageFilter.New(image, image); filter.SetInput(image); if (image.PixelType.IsReal) { filter.OutputMinimum = new itk.itkPixel(image.PixelType, 0.0); filter.OutputMaximum = new itk.itkPixel(image.PixelType, 1.0); } else if (!image.PixelType.IsChar) { filter.OutputMinimum = new itk.itkPixel(image.PixelType, image.PixelType.MinValue); filter.OutputMaximum = new itk.itkPixel(image.PixelType, image.PixelType.MaxValue); if (image.PixelType.IsSigned) requiresScaleAndBias = true; } // Tell the user string info = "Rescaling {0}: [{1}, {2}]"; this.ParentApplication.ScriptConsole.WriteLine(string.Format(info, Path.GetFileName(image.Name), filter.OutputMinimum, filter.OutputMaximum)); // Do the rescaling and return the result filter.UpdateLargestPossibleRegion(); filter.GetOutput(imageRescaled); imageRescaled.Name = image.Name; imageRescaled.Metadata.Add("RequiresScaleAndBias", requiresScaleAndBias); image.DisconnectPipeline(); imageRescaled.DisconnectPipeline(); filter.Dispose(); return imageRescaled; } /// /// Test if the required OpenGl extensions are available on the system. /// If they are not available, an exception is thrown. /// private void TestOpenGlExtensionsSupported() { // List the extensions to load string[] extensionsToTest = new string[] { "GL_EXT_texture3D", "GL_ARB_multitexture", "GL_EXT_blend_minmax" }; // Test the extensions bool result = true; foreach (string extensionToTest in extensionsToTest) result &= Gl.IsExtensionSupported(extensionToTest); // Format extensions string formattedExtensionsToTest = string.Empty; for (int i = 0; i < extensionsToTest.Length; i++) formattedExtensionsToTest += extensionsToTest[i] + ", "; // Show status info to user this.ParentApplication.ScriptConsole.WriteLine("Testing OpenGL extensions: " + formattedExtensionsToTest.TrimEnd(',', ' ')); // Throw exception if (!result) { throw new ApplicationException("The OpenGL extensions required for 3D textures are not available: " + formattedExtensionsToTest.Trim(' ', ',')); } } /// /// Loads the given input images as OpenGL 3D textures. /// private void LoadImagesAsTextures() { // TODO: Support bricks // NOTE: // TransferFunction (if provided) is always Texture0 // Value (which is required) is always Texture1 // Gradient (if provided) is always Texture2 // Extra1 (if provided) is always Texture3 // Extra2 (if provided) is always Texture4 // Setup textures (names, IDs, unit, interpolation) if (this.ContainsMetadata("ImageValue")) { int[] texnames = new int[1]; Gl.glGenTextures(1, texnames); this.GetMetadataAsImage("ImageValue").Metadata["TextureType"] = "Value"; this.GetMetadataAsImage("ImageValue").Metadata["TextureName"] = texnames[0]; this.GetMetadataAsImage("ImageValue").Metadata["TextureUnit"] = Gl.GL_TEXTURE1_ARB; } if (this.ContainsMetadata("ImageGradient")) { int[] texnames = new int[1]; Gl.glGenTextures(1, texnames); this.GetMetadataAsImage("ImageGradient").Metadata["TextureType"] = "Gradient"; this.GetMetadataAsImage("ImageGradient").Metadata["TextureName"] = texnames[0]; this.GetMetadataAsImage("ImageGradient").Metadata["TextureUnit"] = Gl.GL_TEXTURE2_ARB; } if (this.ContainsMetadata("ImageExtra1")) { int[] texnames = new int[1]; Gl.glGenTextures(1, texnames); this.GetMetadataAsImage("ImageExtra1").Metadata["TextureType"] = "Extra1"; this.GetMetadataAsImage("ImageExtra1").Metadata["TextureName"] = texnames[0]; this.GetMetadataAsImage("ImageExtra1").Metadata["TextureUnit"] = Gl.GL_TEXTURE3_ARB; } if (this.ContainsMetadata("ImageExtra2")) { int[] texnames = new int[1]; Gl.glGenTextures(1, texnames); this.GetMetadataAsImage("ImageExtra2").Metadata["TextureType"] = "Extra2"; this.GetMetadataAsImage("ImageExtra2").Metadata["TextureName"] = texnames[0]; this.GetMetadataAsImage("ImageExtra2").Metadata["TextureUnit"] = Gl.GL_TEXTURE4_ARB; } // Load the textures if (this.ContainsMetadata("ImageValue")) this.LoadImageAsTexture(this.GetMetadataAsImage("ImageValue")); if (this.ContainsMetadata("ImageGradient")) this.LoadImageAsTexture(this.GetMetadataAsImage("ImageGradient")); if (this.ContainsMetadata("ImageExtra1")) this.LoadImageAsTexture(this.GetMetadataAsImage("ImageExtra1")); if (this.ContainsMetadata("ImageExtra2")) this.LoadImageAsTexture(this.GetMetadataAsImage("ImageExtra2")); } /// /// Load the given image as an OpenGL 3D texture. /// Information is displayed in the console about the upload. /// /// The scalar or vector image to load. private void LoadImageAsTexture(itk.itkImageBase image) { this.LoadImageAsTexture(image, false); } /// /// Load the given image as an OpenGL 3D texture. /// /// The scalar or vector image to load. /// True and no info is displayed in the console, else False and info is printed. private void LoadImageAsTexture(itk.itkImageBase image, bool silent) { const bool IGNORE_CASE = true; // Get speed info long tickStart = 0; long tickEnd = 0; Kernel.QueryPerformanceCounterFast(out tickStart); Gl.glEnable(Gl.GL_TEXTURE_3D_EXT); // Enable { // Ensure image has interpolation if (!image.Metadata.ContainsKey("TextureInterpolation")) image.Metadata["TextureInterpolation"] = Gl.GL_LINEAR; // Bind this.BindTexture(image, false); // Compute required information // NOTE: We must swap the color components for the TransferFunction // because the System.Drawing namespace (which we assume was used // to create the TF) is actually BGR pixel type, not RGB. bool swapColorComponents = (String.Compare(image.Metadata["TextureType"] as String, "TransferFunction", IGNORE_CASE) == 0); int iFormat = this.ComputeTextureFormat(image.PixelType, swapColorComponents); int iInternalFormat = this.ComputeTextureInternalFormat(image.PixelType); int iDataType = this.ComputeTextureDataType(image.PixelType.TypeAsEnum); // Set the scale and bias if (image.Metadata.ContainsKey("RequiresScaleAndBias") && (bool)image.Metadata["RequiresScaleAndBias"]) { float scale = 0.5F; float bias = 0.5F; Gl.glPixelTransferf(Gl.GL_ALPHA_SCALE, scale); Gl.glPixelTransferf(Gl.GL_ALPHA_BIAS, bias); } // Upload the data Gl.glTexImage3DEXT( Gl.GL_TEXTURE_3D_EXT, // Target 0, // Level iInternalFormat, // Internal Format image.Size[0], // Width image.Size[1], // Height image.Size[2], // Depth 0, // Border iFormat, // Format iDataType, // Data Type image.Buffer // Data Pointer ); // Reset the scale and bias Gl.glPixelTransferf(Gl.GL_ALPHA_SCALE, 1.0F); Gl.glPixelTransferf(Gl.GL_ALPHA_BIAS, 0.0F); } Gl.glDisable(Gl.GL_TEXTURE_3D_EXT); //Disable // Compute time to load Kernel.QueryPerformanceCounterFast(out tickEnd); float timeToLoad = (float)(tickEnd - tickStart) / (float)this.m_TickFrequency; // Show information to user if (!silent) { string info = "{0}: {2} (loaded in {1} seconds)"; if (image.Name == null || image.Name.Length == 0) info = "{0}: (loaded in {1} seconds)"; string infoFormatted = string.Format(info, image.Metadata["TextureType"], timeToLoad, Path.GetFileName(image.Name)); this.ParentApplication.ScriptConsole.WriteLine(infoFormatted); } } /// /// Bind to the texture of the given image. /// /// /// Set to true to ensure the texture is made active. private void BindTexture(itk.itkImageBase image, bool makeActive) { Gl.glEnable(Gl.GL_TEXTURE_3D_EXT); // Enable { // Make active if (makeActive) Gl.glActiveTextureARB((int)image.Metadata["TextureUnit"]); // Bind Gl.glBindTexture(Gl.GL_TEXTURE_3D_EXT, (int)image.Metadata["TextureName"]); // Set minification and magnification parameters Gl.glTexParameteri(Gl.GL_TEXTURE_3D_EXT, Gl.GL_TEXTURE_MAG_FILTER, (int)image.Metadata["TextureInterpolation"]); Gl.glTexParameteri(Gl.GL_TEXTURE_3D_EXT, Gl.GL_TEXTURE_MIN_FILTER, (int)image.Metadata["TextureInterpolation"]); // Set general parameters if (String.Compare(image.Metadata["TextureType"] as String, "TransferFunction", true) == 0) { Gl.glTexParameteri(Gl.GL_TEXTURE_3D_EXT, Gl.GL_TEXTURE_WRAP_S, Gl.GL_CLAMP_TO_EDGE); Gl.glTexParameteri(Gl.GL_TEXTURE_3D_EXT, Gl.GL_TEXTURE_WRAP_T, Gl.GL_CLAMP_TO_EDGE); Gl.glTexParameteri(Gl.GL_TEXTURE_3D_EXT, Gl.GL_TEXTURE_WRAP_R, Gl.GL_CLAMP_TO_EDGE); } else { Gl.glTexParameteri(Gl.GL_TEXTURE_3D_EXT, Gl.GL_TEXTURE_WRAP_S, Gl.GL_CLAMP); Gl.glTexParameteri(Gl.GL_TEXTURE_3D_EXT, Gl.GL_TEXTURE_WRAP_T, Gl.GL_CLAMP); Gl.glTexParameteri(Gl.GL_TEXTURE_3D_EXT, Gl.GL_TEXTURE_WRAP_R, Gl.GL_CLAMP); } } Gl.glDisable(Gl.GL_TEXTURE_3D_EXT); // Disable } /// /// Unbind the texture for the given image. /// /// protected virtual void UnbindTexture(itk.itkImageBase image) { Gl.glEnable(Gl.GL_TEXTURE_3D_EXT); // Enable { // Make active Gl.glActiveTextureARB((int)image.Metadata["TextureUnit"]); // Unbind Gl.glBindTexture(Gl.GL_TEXTURE_3D_EXT, 0); } Gl.glDisable(Gl.GL_TEXTURE_3D_EXT); // Disable } /// /// Set the texture units of the input images and transfer /// function within the shader programs. /// private void SetShaderTextureUnits() { if (this.Shader != null) { if (this.ContainsMetadata("TransferFunction")) this.Shader.SetTextureUnit("sam3Tex0", 0); if (this.ContainsMetadata("ImageValue")) this.Shader.SetTextureUnit("sam3Tex1", 1); if (this.ContainsMetadata("ImageGradient")) this.Shader.SetTextureUnit("sam3Tex2", 2); if (this.ContainsMetadata("ImageExtra1")) this.Shader.SetTextureUnit("sam3Tex3", 3); if (this.ContainsMetadata("ImageExtra2")) this.Shader.SetTextureUnit("sam3Tex4", 4); } } /// /// Compute the texture internal format from the given pixel type. /// /// /// private int ComputeTextureInternalFormat(itk.itkPixelType type) { // Get the number of bytes per element uint numBytesPerComponents = type.PixelSize; numBytesPerComponents /= type.NumberOfComponentsPerPixel; // Get the internal format switch (type.NumberOfComponentsPerPixel) { case 1: if (numBytesPerComponents == 8) return Gl.GL_ALPHA8; else if (numBytesPerComponents == 16) return Gl.GL_ALPHA16; break; case 2: if (numBytesPerComponents == 8) return Gl.GL_LUMINANCE8_ALPHA8; else if (numBytesPerComponents == 16) return Gl.GL_LUMINANCE16_ALPHA16; break; case 3: if (numBytesPerComponents == 8) return Gl.GL_RGB8; else if (numBytesPerComponents == 16) return Gl.GL_RGB16; break; case 4: if (numBytesPerComponents == 8) return Gl.GL_RGBA8; else if (numBytesPerComponents == 16) return Gl.GL_RGBA16; break; } // If we get here the type is not supported throw new NotSupportedException("Could not compute the internal texture format: the given pixel type '" + type.TypeAsEnum.ToString() + " with " + type.NumberOfComponentsPerPixel.ToString() + " components is not supported."); } /// /// Compute the texture format from the given pixel type. /// /// The pixel type to compute the format. /// True and the RGB components should be swapped to BGR. /// private int ComputeTextureFormat(itk.itkPixelType type, bool swapColorComponents) { // Get the internal format switch (type.NumberOfComponentsPerPixel) { case 1: return Gl.GL_ALPHA; case 2: return Gl.GL_LUMINANCE_ALPHA; case 3: return (swapColorComponents == false) ? Gl.GL_RGB : Gl.GL_BGR; case 4: return (swapColorComponents == false) ? Gl.GL_RGBA : Gl.GL_BGRA; default: throw new NotSupportedException("Could not compute the internal texture format: " + type.NumberOfComponentsPerPixel.ToString() + " components per pixel is not supported."); } } /// /// Compute the texture data type from the given pixel type. /// /// /// private int ComputeTextureDataType(itk.itkPixelTypeEnum type) { switch (type) { case (itk.itkPixelTypeEnum.UnsignedChar): return Gl.GL_UNSIGNED_BYTE; case (itk.itkPixelTypeEnum.SignedChar): return Gl.GL_BYTE; case (itk.itkPixelTypeEnum.UnsignedShort): return Gl.GL_UNSIGNED_SHORT; case (itk.itkPixelTypeEnum.SignedShort): return Gl.GL_SHORT; case (itk.itkPixelTypeEnum.UnsignedLong): return Gl.GL_UNSIGNED_INT; case (itk.itkPixelTypeEnum.SignedLong): return Gl.GL_INT; case (itk.itkPixelTypeEnum.Float): return Gl.GL_FLOAT; case (itk.itkPixelTypeEnum.Double): return Gl.GL_DOUBLE; default: throw new NotSupportedException("The given pixel type '" + type.ToString() + " is not currently supported."); } } //===================================================================== #endregion #region Texture Rendering Methods //===================================================================== /// /// Sets shader. On failure, this method reverts to the first laoded shader /// which is assumed to be valid. /// /// A valid path to a fragment shader. public void SetShader(string pathFragment) { try { // Check value is not null or empty if (pathFragment == null || pathFragment.Length == 0) throw new ArgumentNullException("FragmentProgramPath", "The path can not be null or empty."); // Check the vertex program exists string pathVertex = Path.Combine(Path.GetDirectoryName(pathFragment), Path.GetFileNameWithoutExtension(pathFragment) + ".vert"); if (!File.Exists(pathVertex)) throw new FileNotFoundException("Could not find the vertex shader program file.", pathVertex); // Check if we should make this the default fragment shader if (this.m_DefaultFragmentProgramPath == null) this.m_DefaultFragmentProgramPath = pathFragment; // Force the shader to end this.Shader.End(); this.IsModified = false; // Set the fragment and vertex program paths this.Shader.FragmentProgramPath = pathFragment; this.Shader.VertexProgramPath = pathVertex; // Re-initialise the shader and repaint this.Shader.Initialise(); this.SetShaderTextureUnits(); this.SamplingRateChanged(this.Properties.SamplingRate); this.Properties.NormalOffset = this.Properties.NormalOffset; this.Properties.Factor1 = this.Properties.Factor1; this.Properties.Factor2 = this.Properties.Factor2; this.Properties.Factor3 = this.Properties.Factor3; // Watch the file for further changes this.m_ShaderWatcher.Path = Path.GetDirectoryName(pathFragment); this.m_ShaderWatcher.Filter = Path.GetFileName(pathFragment); } catch (Exception ex) { string text = "Error applying shader program (see details below).\r\n"; text += "Using default shader program: " + this.m_DefaultFragmentProgramPath + "\r\n"; text += ex.ToString(); string caption = "Error applying shader program"; MessageBox.Show(this.ParentApplication as IWin32Window, text, caption, MessageBoxButtons.OK, MessageBoxIcon.Error); // Use the old path // NOTE: If the old path also fails, the app will probably crash... this.SetShader(m_DefaultFragmentProgramPath); } } void watcher_Changed(object sender, FileSystemEventArgs e) { // Make thread safe if (this.Form != null && this.Form.InvokeRequired) { FileSystemEventHandler method = new FileSystemEventHandler(this.watcher_Changed); Object[] args = new Object[] { sender, e }; this.Form.Invoke(method, args); return; } // Check that some time has elapsed since the last change TimeSpan diff = new TimeSpan(DateTime.Now.Ticks).Subtract(this.m_ShaderLastChanged); if (!this.m_IsAskingUserToReloadShader && diff.Seconds > SHADER_WATCHER_TIME) { this.m_IsAskingUserToReloadShader = true; String text = "The shader '" + e.Name + "' has been changed. Do you want to reload the shader?"; String caption = "Reload shader?"; DialogResult result = MessageBox.Show(this.Form, text, caption, MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { this.Properties.FragmentProgramPath = e.FullPath; this.m_ShaderLastChanged = new TimeSpan(DateTime.Now.Ticks); } this.m_IsAskingUserToReloadShader = false; } } /// /// Renders the geometry which will interpolate the uploaded textures. /// This method assumes that the input images are tagged with /// "Value": for a value image /// "Gradient": for a gradient image /// "Extra1": extra data /// "Extra2": extra data. /// /// private void RenderTextureGeometry() { // Only render geometry if input images are available if (!this.ContainsMetadata("ImageValue")) return; // Bind textures this.BindTextures(); // Get the image texture units int texunitValue = this.ContainsMetadata("ImageValue") ? this.GetImageTextureUnit("ImageValue") : 0; int texunitGradient = this.ContainsMetadata("ImageGradient") ? this.GetImageTextureUnit("ImageGradient") : 0; int texunitExtra1 = this.ContainsMetadata("ImageExtra1") ? this.GetImageTextureUnit("ImageExtra1") : 0; int texunitExtra2 = this.ContainsMetadata("ImageExtra2") ? this.GetImageTextureUnit("ImageExtra2") : 0; // Enable Blending Gl.glEnable(Gl.GL_BLEND); switch (this.Properties.Blending) { case siVolumeBlendEnum.BackToFront: Gl.glBlendEquationEXT(Gl.GL_FUNC_ADD_EXT); Gl.glBlendFunc(Gl.GL_SRC_ALPHA, Gl.GL_ONE_MINUS_SRC_ALPHA); break; case siVolumeBlendEnum.MaximumProjection: Gl.glBlendEquationEXT(Gl.GL_MAX_EXT); //Gl.glBlendFunc(Gl.GL_ONE, Gl.GL_ONE); Gl.glBlendFunc(Gl.GL_SRC_ALPHA, Gl.GL_ONE_MINUS_SRC_ALPHA); break; case siVolumeBlendEnum.MinimumProjection: Gl.glBlendEquationEXT(Gl.GL_MIN_EXT); //Gl.glBlendFunc(Gl.GL_ONE, Gl.GL_ONE); Gl.glBlendFunc(Gl.GL_ZERO, Gl.GL_ZERO); break; } // Push the matrix Gl.glMatrixMode(Gl.GL_MODELVIEW); Gl.glPushMatrix(); // Enable and setup lighting Gl.glEnable(Gl.GL_LIGHTING); Gl.glEnable(Gl.GL_LIGHT0); Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_AMBIENT, siColorHelper.To4fv(this.Properties.LightColorAmbient)); Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_DIFFUSE, siColorHelper.To4fv(this.Properties.LightColorDiffuse)); Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_SPECULAR, siColorHelper.To4fv(this.Properties.LightColorSpecular)); Gl.glLightf(Gl.GL_LIGHT0, Gl.GL_SPOT_EXPONENT, this.Properties.LightColorSpecularExponent); // Translate into middle of bounding box itk.itkArray size = this.Properties.ModelSize; Gl.glTranslated(-0.5 * size[0], -0.5 * size[1], -0.5 * size[2]); // Begin the geometry generator this.m_TexureGeometryGenerator.Begin(this.Properties.TrackBall.RotationMatrix, this.Properties.SamplingRate.Current); Gl.glEnable(Gl.GL_TEXTURE_3D_EXT); // Enable { do { // Get the line loop points itk.itkPoint[] points = this.m_TexureGeometryGenerator.GetCurrentLineLoop(); if (points == null || points.Length == 0) continue; // No geometry // Render the wire frame if (this.Properties.WireFrame) { this.UnbindTextures(); Gl.glDisable(Gl.GL_LIGHTING); Gl.glEnable(Gl.GL_BLEND); Gl.glBlendFunc(Gl.GL_SRC_ALPHA, Gl.GL_ONE_MINUS_SRC_ALPHA); Gl.glDisable(Gl.GL_DEPTH_TEST); Gl.glBegin(Gl.GL_LINE_LOOP); // Render line loop { Gl.glColor3fv(siColorHelper.To3fv(this.Properties.BoxOutlineColor)); for (int i = 0; i < points.Length; i++) Gl.glVertex3dv(points[i].Data); } Gl.glEnd(); Gl.glEnable(Gl.GL_LIGHTING); this.BindTextures(); } // Turn shader on this.Shader.Begin(); // Render the texture interpolation geometry Gl.glBegin(Gl.GL_TRIANGLE_FAN); // Render triangle fan { for (int i = 0; i < points.Length; i++) { double[] pointOut = new double[3] { 0.0, 0.0, 0.0 }; double[] pointTex = new double[3] { 0.0, 0.0, 0.0 }; for (int j = 0; j < 3; j++) { pointOut[j] = points[i][j]; pointTex[j] = pointOut[j] / size[j]; } // Convert to the OpenGL texture coordinate system by // flipping along the z-axis pointTex[2] = 1.0 - pointTex[2]; // Output the texture coord if (this.ContainsMetadata("ImageValue")) Gl.glMultiTexCoord3dvARB(texunitValue, pointTex); if (this.ContainsMetadata("ImageGradient")) Gl.glMultiTexCoord3dvARB(texunitGradient, pointTex); if (this.ContainsMetadata("ImageExtra1")) Gl.glMultiTexCoord3dvARB(texunitExtra1, pointTex); if (this.ContainsMetadata("ImageExtra2")) Gl.glMultiTexCoord3dvARB(texunitExtra2, pointTex); // Output the vertex coord Gl.glVertex3dv(pointOut); } } Gl.glEnd(); // Turn shader off this.Shader.End(); } while (!this.m_TexureGeometryGenerator.End()); } Gl.glDisable(Gl.GL_TEXTURE_3D_EXT); //Disable // Pop Gl.glPopMatrix(); // Disable blending Gl.glBlendEquationEXT(Gl.GL_FUNC_ADD_EXT); Gl.glDisable(Gl.GL_BLEND); // Disable lighting Gl.glDisable(Gl.GL_LIGHT0); Gl.glDisable(Gl.GL_LIGHTING); // Unbind textures this.UnbindTextures(); // End the shader if (this.Shader != null) this.Shader.End(); } private void BindTextures() { // Bind textures if (this.ContainsMetadata("ImageValue")) this.BindTexture(this.GetMetadataAsImage("ImageValue"), true); if (this.ContainsMetadata("ImageGradient")) this.BindTexture(this.GetMetadataAsImage("ImageGradient"), true); if (this.ContainsMetadata("ImageExtra1")) this.BindTexture(this.GetMetadataAsImage("ImageExtra1"), true); if (this.ContainsMetadata("ImageExtra2")) this.BindTexture(this.GetMetadataAsImage("ImageExtra2"), true); if (this.ContainsMetadata("TransferFunction")) this.BindTexture(this.TransferFunction.Image, true); } private void UnbindTextures() { // Bind textures if (this.ContainsMetadata("ImageValue")) this.UnbindTexture(this.GetMetadataAsImage("ImageValue")); if (this.ContainsMetadata("ImageGradient")) this.UnbindTexture(this.GetMetadataAsImage("ImageGradient")); if (this.ContainsMetadata("ImageExtra1")) this.UnbindTexture(this.GetMetadataAsImage("ImageExtra1")); if (this.ContainsMetadata("ImageExtra2")) this.UnbindTexture(this.GetMetadataAsImage("ImageExtra2")); if (this.ContainsMetadata("TransferFunction") && this.TransferFunction.Image != null) this.UnbindTexture(this.TransferFunction.Image); } /// /// Get the texture unit for the image stored in this.Metadata[key]. /// No checking is done to ensure the metadata keys exists or are not null. /// /// The key of the image to get the texture unit for. /// private int GetImageTextureUnit(string key) { return ((int)this.GetMetadataAsImage(key).Metadata["TextureUnit"]); } /// /// Handle the siSamplingRate.Changed event. /// /// public void SamplingRateChanged(siSamplingRate rate) { if (this.ContainsMetadata("Shader")) { this.Shader.SetUniformFloat("fSamplingRate", rate.Current); this.RaiseModified(); this.Repaint(); } } private void Form_ResizeBegin(object sender, EventArgs e) { this.Properties.SamplingRate.MakeCurrentLowQuality(); } private void Form_ResizeEnd(object sender, EventArgs e) { this.Properties.SamplingRate.MakeCurrentHighQuality(); } //===================================================================== #endregion #region Event Handler Methods //===================================================================== protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); // Check we have rendered if (!this.m_HasRenderedFirstTime) return; // Change the sampling rate this.Properties.SamplingRate.MakeCurrentLowQuality(); // Save the last screen mouse down this.Metadata["LastScreenMouseDown"] = e; // Check if a change of Metadata is required if (e.Button == MouseButtons.Right) { this.Metadata["IsTranslating"] = true; this.Cursor = Cursors.Hand; } else if (e.Button == MouseButtons.Left) { this.Metadata["IsRotating"] = true; this.Cursor = Cursors.Arrow; int width = (int)this.Metadata["RenderingControlWidth"]; int height = (int)this.Metadata["RenderingControlHeight"]; this.Properties.TrackBall.StartRotation(e.X, e.Y, width, height); } } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); // Cancel translation if (e.Button == MouseButtons.Right) { this.Metadata["IsTranslating"] = false; this.Cursor = Cursors.Default; } else if (e.Button == MouseButtons.Left) { this.Metadata["IsRotating"] = false; this.Cursor = Cursors.Default; this.Properties.TrackBall.StopRotation(); } // Change the sampling rate this.Properties.SamplingRate.MakeCurrentHighQuality(); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); // Check we have rendered if (!this.m_HasRenderedFirstTime) return; // Save the old cursor value Cursor cursor = this.Cursor; // Check the Metadata of the mouse move if ((bool)this.Metadata["IsTranslating"]) { // We are translating the volume MouseEventArgs last = this.Metadata["LastScreenMouseMove"] as MouseEventArgs; cursor = Cursors.Hand; // Update transform this.Properties.TransformTranslate[0] += (double)(e.X - last.X) * 0.005; this.Properties.TransformTranslate[1] -= (double)(e.Y - last.Y) * 0.005; // Force a repaint this.Repaint(); } else if ((bool)this.Metadata["IsRotating"]) { // We are rotating the volume cursor = Cursors.Arrow; // Update transform int width = (int)this.Metadata["RenderingControlWidth"]; int height = (int)this.Metadata["RenderingControlHeight"]; this.Properties.TrackBall.TrackRotation(e.X, e.Y, width, height); // Force a repaint this.Repaint(); } // Set the form cursor if (this.Cursor != Cursors.WaitCursor) { if (this.ContainsMetadata("Cursor")) this.Cursor = (Cursor)this.Metadata["Cursor"]; else this.Cursor = cursor; } // Save the last screen mouse move this.Metadata["LastScreenMouseMove"] = e; } protected override void OnMouseWheel(MouseEventArgs e) { // Compute the wheel delta for GL double glDelta = (double)e.Delta / 2400.0; // Check if a key is down KeyEventArgs key = null; if (this.Metadata.ContainsKey("CurrentKey")) key = this.Metadata["CurrentKey"] as KeyEventArgs; // Change what we are doing based on key down if (key != null && key.Control) { // Control, change slice position this.m_TexureGeometryGenerator.GeometrySliceDepth += glDelta * 0.1; } else if (key != null && key.Shift) { // Control, change slice thickness this.m_TexureGeometryGenerator.GeometrySliceThickness += glDelta * 0.1; } else { // No key, zoom this.Properties.FrustrumLeft *= 1 + glDelta; this.Properties.FrustrumRight *= 1 + glDelta; this.Properties.FrustrumBottom *= 1 + glDelta; this.Properties.FrustrumTop *= 1 + glDelta; } this.Repaint(); } //===================================================================== #endregion #region General Helper Methods //===================================================================== /// /// Returns the given Metadata variable as an itkImage. /// Returns null if the Metadata variable name does not exist. /// /// /// private itk.itkImageBase GetMetadataAsImage(string key) { if (this.ContainsMetadata(key)) return this.Metadata[key] as itk.itkImageBase; else return null; } /// /// Computes if non power of two texture sizes are supported by the graphics H/W /// based on the presence of the GL_ARB_texture_non_power_of_two extension. /// /// private bool IsNonPowerOfTwoTextureSizeSupported() { return Gl.IsExtensionSupported("GL_ARB_texture_non_power_of_two"); } //===================================================================== #endregion } }