Click here to Skip to main content
Click here to Skip to main content

D3dScenePresenter - How to present and manipulate a 3D scene using MDX

, 15 Feb 2013
Rate this:
Please Sign up or sign in to vote.
This article shows how we can present a 3D scene and, perform common operations (zoom, rotate, move, zoom to specific region, adjust the camera to view the whole of the scene, and pick a 3D shape on a specific region on the rendered surface) on it, using Managed DirectX.

Sample Image

Table of contents

Introduction

When developing 3D scenes for our applications, we sometimes want to enable some manipulations on the scenes. We may want to move or rotate our camera relative to the current view. We may want to perform zooming on our scene. We may want to show the full scene or, a specific region of it. We may also want to pick a shape in a specific point on the rendered surface.

This article shows how we can implement those operations and, how we can create a UI control that enables performing them.

Background

In this article I discuss on picking (find the graphic that behind a specific surface's point) and, on how we can perform common camera transformations for the whole of the available cameras of the MDX (Managed DirectX) framework.

This article assumes an understanding of the C# language and a basic knowledge on DirectX concepts.

You can read more about Direct3D on the Direct3D Graphics MSDN topic.

How it works

The scene

Create the scene elements

The main discussion of this article is about operations that can be done on a scene. But, we cannot perform any scene's operation without a scene to perform that operation on. This section presents the components of our scene.

The discussion on the DirectX's scene's elements (matrices, lights, etc..), is beyond the scope of this article. So, we just present the parts of the scene's classes, without discussing deeply on how they actually works. But, if you are familiar with the concepts, it is easily understandable. You can read more on those concepts on the Direct3D Tutorials MSDN topic.

As I wrote, this section just presents the code for our scene's components. So, if you want to get directly to the main discussion of this article, you can skip this section and, jump to the Camera transformations section or to the Picking section.

For our scene, we create classes for implementing the available cameras:

  • An enum for indicating the camera's coordinate system:
    public enum CameraCoordinateSystem
    {
        RightHanded,
        LeftHanded
    }
  • Base class for cameras implementation:
    • Camera's coordinate system:
      public abstract class D3dCamera
      {
          protected D3dCamera()
          {
              CoordinateSystem = CameraCoordinateSystem.RightHanded;
          }
      
          #region CoordinateSystem
          private CameraCoordinateSystem _coordinateSystem;
          public CameraCoordinateSystem CoordinateSystem
          {
              get { return _coordinateSystem; }
              set
              {
                  lock (this)
                  {
                      _coordinateSystem = value;
                      _isViewMatrixValid = false;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      }
    • View matrix:
      • Properties for holding the camera's position:
        #region PositionX
        private float _positionX;
        public float PositionX
        {
            get { return _positionX; }
            set
            {
                lock (this)
                {
                    _positionX = value;
                    _isViewMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region PositionY
        private float _positionY;
        public float PositionY
        {
            get { return _positionY; }
            set
            {
                lock (this)
                {
                    _positionY = value;
                    _isViewMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region PositionZ
        private float _positionZ;
        public float PositionZ
        {
            get { return _positionZ; }
            set
            {
                lock (this)
                {
                    _positionZ = value;
                    _isViewMatrixValid = false;
                }
            }
        }
        #endregion
      • Properties for holding the camera target's position:
        #region TargetX
        private float _targetX;
        public float TargetX
        {
            get { return _targetX; }
            set
            {
                lock (this)
                {
                    _targetX = value;
                    _isViewMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region TargetY
        private float _targetY;
        public float TargetY
        {
            get { return _targetY; }
            set
            {
                lock (this)
                {
                    _targetY = value;
                    _isViewMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region TargetZ
        private float _targetZ;
        public float TargetZ
        {
            get { return _targetZ; }
            set
            {
                lock (this)
                {
                    _targetZ = value;
                    _isViewMatrixValid = false;
                }
            }
        }
        #endregion
      • Properties for holding the camera's up vector:
        #region UpX
        private float _upX;
        public float UpX
        {
            get { return _upX; }
            set
            {
                lock (this)
                {
                    _upX = value;
                    _isViewMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region UpY
        private float _upY;
        public float UpY
        {
            get { return _upY; }
            set
            {
                lock (this)
                {
                    _upY = value;
                    _isViewMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region UpZ
        private float _upZ;
        public float UpZ
        {
            get { return _upZ; }
            set
            {
                lock (this)
                {
                    _upZ = value;
                    _isViewMatrixValid = false;
                }
            }
        }
        #endregion
      • Property for holding the view matrix:
        #region ViewMatrix
        private Matrix _viewMatrix;
        public Matrix ViewMatrix
        {
            get
            {
                lock (this)
                {
                    if (!_isViewMatrixValid)
                    {
                        _viewMatrix = GetViewMatrix();
                        _isViewMatrixValid = true;
                    }
                }
        
                return _viewMatrix;
            }
        }
        
        protected bool _isViewMatrixValid;
        #endregion
        
        public virtual Matrix GetViewMatrix()
        {
            Matrix res;
        
            if (CameraCoordinateSystem.LeftHanded == CoordinateSystem)
            {
                res = Matrix.LookAtLH(new Vector3(PositionX, PositionY, PositionZ),
                                        new Vector3(TargetX, TargetY, TargetZ),
                                        new Vector3(UpX, UpY, UpZ));
            }
            else
            {
                // It's a right-handed coordinate system.
        
                res = Matrix.LookAtRH(new Vector3(PositionX, PositionY, PositionZ),
                                        new Vector3(TargetX, TargetY, TargetZ),
                                        new Vector3(UpX, UpY, UpZ));
            }
        
            return res;
        }
    • Projection matrix:
      • Properties for holding the near and far planes' distances:
        #region ZNearPlane
        private float _zNearPlane;
        public float ZNearPlane
        {
            get { return _zNearPlane; }
            set
            {
                lock (this)
                {
                    _zNearPlane = value;
                    _isProjectionMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region ZFarPlane
        private float _zFarPlane;
        public float ZFarPlane
        {
            get { return _zFarPlane; }
            set
            {
                lock (this)
                {
                    _zFarPlane = value;
                    _isProjectionMatrixValid = false;
                }
            }
        }
        #endregion
      • Property for holding the projection matrix:
        #region ProjectionMatrix
        private Matrix _projectionMatrix;
        public Matrix ProjectionMatrix
        {
            get
            {
                lock (this)
                {
                    if (!_isProjectionMatrixValid)
                    {
                        _projectionMatrix = GetProjectionMatrix();
                        _isProjectionMatrixValid = true;
                    }
                }
        
                return _projectionMatrix;
            }
        }
        
        protected bool _isProjectionMatrixValid;
        #endregion
        
        public abstract Matrix GetProjectionMatrix();
    • Apply the camera's settings:
      public virtual void Render(Device d3dDevice)
      {
          if (null == d3dDevice)
          {
              return;
          }
      
          CurrentViewMatrix = ViewMatrix;
          CurrentProjectionMatrix = ProjectionMatrix;
      
          lock (d3dDevice)
          {
              d3dDevice.Transform.View = CurrentViewMatrix;
              d3dDevice.Transform.Projection = CurrentProjectionMatrix;
          }
      }
      
      public Matrix CurrentViewMatrix { get; protected set; }
      public Matrix CurrentProjectionMatrix { get; protected set; }
  • Orthographic cameras:
    • Base class for orthographic cameras implementation:
      public abstract class D3dOrthoCameraBase : D3dCamera
      {
      }
    • Orthographic camera:
      public class D3dOrthoCamera : D3dOrthoCameraBase
      {
          public D3dOrthoCamera()
          {
              Width = 1000;
              Height = 1000;
          }
      
          #region D3dOrthoCameraBase implementation
          public override Matrix GetProjectionMatrix()
          {
              return CameraCoordinateSystem.LeftHanded == CoordinateSystem
                          ? Matrix.OrthoLH(Width, Height, ZNearPlane, ZFarPlane)
                          : Matrix.OrthoRH(Width, Height, ZNearPlane, ZFarPlane);
          }
          #endregion
      
          #region properties
      
          #region Width
          private float _width;
          public float Width
          {
              get { return _width; }
              set
              {
                  lock (this)
                  {
                      _width = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #region Height
          private float _height;
          public float Height
          {
              get { return _height; }
              set
              {
                  lock (this)
                  {
                      _height = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #endregion
      }
    • Orthographic off-center camera:
      public class D3dOrthoOffCenterCamera : D3dOrthoCameraBase
      {
          public D3dOrthoOffCenterCamera()
          {
              Left = -500;
              Right = 500;
              Bottom = -500;
              Top = 500;
          }
      
          #region D3dOrthoCameraBase implementation
          public override Matrix GetProjectionMatrix()
          {
              return CameraCoordinateSystem.LeftHanded == CoordinateSystem
                          ? Matrix.OrthoOffCenterLH(Left, Right, Bottom, Top, ZNearPlane, ZFarPlane)
                          : Matrix.OrthoOffCenterRH(Left, Right, Bottom, Top, ZNearPlane, ZFarPlane);
          }
          #endregion
      
          #region properties
      
          #region Left
          private float _left;
          public float Left
          {
              get { return _left; }
              set
              {
                  lock (this)
                  {
                      _left = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #region Right
          private float _right;
          public float Right
          {
              get { return _right; }
              set
              {
                  lock (this)
                  {
                      _right = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #region Bottom
          private float _bottom;
          public float Bottom
          {
              get { return _bottom; }
              set
              {
                  lock (this)
                  {
                      _bottom = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #region Top
          private float _top;
          public float Top
          {
              get { return _top; }
              set
              {
                  lock (this)
                  {
                      _top = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #endregion
      }
  • Perspective cameras:
    • Base class for perspective camera's implementation:
      public abstract class D3dPerspectiveCameraBase : D3dCamera
      {
      }
    • Perspective camera:
      public class D3dPerspectiveCamera : D3dPerspectiveCameraBase
      {
          public D3dPerspectiveCamera()
          {
              Width = 1;
              Height = 1;
          }
      
          #region D3dPerspectiveCameraBase implementation
          public override Matrix GetProjectionMatrix()
          {
              return CameraCoordinateSystem.LeftHanded == CoordinateSystem
                          ? Matrix.PerspectiveLH(Width, Height, ZNearPlane, ZFarPlane)
                          : Matrix.PerspectiveRH(Width, Height, ZNearPlane, ZFarPlane);
          }
          #endregion
      
          #region properties
      
          #region Width
          private float _width;
          public float Width
          {
              get { return _width; }
              set
              {
                  lock (this)
                  {
                      _width = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #region Height
          private float _height;
          public float Height
          {
              get { return _height; }
              set
              {
                  lock (this)
                  {
                      _height = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #endregion
      }
    • Perspective off-center camera:
      public class D3dPerspectiveOffCenterCamera : D3dPerspectiveCameraBase
      {
          public D3dPerspectiveOffCenterCamera()
          {
              Left = -0.5f;
              Right = 0.5f;
              Bottom = 0.5f;
              Top = -0.5f;
          }
      
          #region D3dPerspectiveCameraBase implementation
          public override Matrix GetProjectionMatrix()
          {
              return CameraCoordinateSystem.LeftHanded == CoordinateSystem
                          ? Matrix.PerspectiveOffCenterLH(Left, Right, Bottom, Top, ZNearPlane, ZFarPlane)
                          : Matrix.PerspectiveOffCenterRH(Left, Right, Bottom, Top, ZNearPlane, ZFarPlane);
          }
          #endregion
      
          #region properties
      
          #region Left
          private float _left;
          public float Left
          {
              get { return _left; }
              set
              {
                  lock (this)
                  {
                      _left = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #region Right
          private float _right;
          public float Right
          {
              get { return _right; }
              set
              {
                  lock (this)
                  {
                      _right = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #region Bottom
          private float _bottom;
          public float Bottom
          {
              get { return _bottom; }
              set
              {
                  lock (this)
                  {
                      _bottom = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #region Top
          private float _top;
          public float Top
          {
              get { return _top; }
              set
              {
                  lock (this)
                  {
                      _top = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #endregion
      }
    • Perspective FOV camera:
      public class D3dPerspectiveFovCamera : D3dPerspectiveCameraBase
      {
          public D3dPerspectiveFovCamera()
          {
              FieldOfViewY = (float)Math.PI / 4.0f;
              AspectRatio = 1;
          }
      
          #region D3dPerspectiveCameraBase implementation
          public override Matrix GetProjectionMatrix()
          {
              return CameraCoordinateSystem.LeftHanded == CoordinateSystem
                          ? Matrix.PerspectiveFovLH(FieldOfViewY, AspectRatio, ZNearPlane, ZFarPlane)
                          : Matrix.PerspectiveFovRH(FieldOfViewY, AspectRatio, ZNearPlane, ZFarPlane);
          }
          #endregion
      
          #region properties
      
          #region FieldOfViewY
          private float _fieldOfViewY;
          public float FieldOfViewY
          {
              get { return _fieldOfViewY; }
              set
              {
                  lock (this)
                  {
                      _fieldOfViewY = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #region AspectRatio
          private float _aspectRatio;
          public float AspectRatio
          {
              get { return _aspectRatio; }
              set
              {
                  lock (this)
                  {
                      _aspectRatio = value;
                      _isProjectionMatrixValid = false;
                  }
              }
          }
          #endregion
      
          #endregion
      }

Create classes for implementing the available lights:

  • Base class for lights implementation:
    public abstract class D3dLight
    {
        protected D3dLight()
        {
            Diffuse = Color.White;
    
            Enabled = true;
        }
    
        public virtual void Render(Device d3dDevice)
        {
            if (0 > Index)
            {
                return;
            }
    
            lock (d3dDevice)
            {
                SetSpecificLightValues(d3dDevice);
    
                d3dDevice.Lights[Index].Diffuse = Diffuse;
                d3dDevice.Lights[Index].Ambient = Ambient;
                d3dDevice.Lights[Index].Specular = Specular;
    
                d3dDevice.Lights[Index].Enabled = Enabled;
            }
        }
    
        #region properties
    
        public int Index { get; set; }
    
        public Color Diffuse { get; set; }
        public Color Ambient { get; set; }
        public Color Specular { get; set; }
    
        public bool Enabled { get; set; }
    
        #endregion
    
        protected abstract void SetSpecificLightValues(Device d3dDevice);
    }
  • Directional light:
    public class D3dDirectionalLight : D3dLight
    {
        public D3dDirectionalLight()
        {
            XDirection = 1;
            YDirection = -1;
            ZDirection = -1;
        }
    
        #region D3dLight implementation
        protected override void SetSpecificLightValues(Device d3dDevice)
        {
            d3dDevice.Lights[Index].Type = LightType.Directional;
    
            d3dDevice.Lights[Index].XDirection = XDirection;
            d3dDevice.Lights[Index].YDirection = YDirection;
            d3dDevice.Lights[Index].ZDirection = ZDirection;
        }
        #endregion
    
        #region properties
    
        public float XDirection { get; set; }
        public float YDirection { get; set; }
        public float ZDirection { get; set; }
    
        #endregion
    }
  • Point light:
    public class D3dPointLight : D3dLight
    {
        public D3dPointLight()
        {
            XPosition = YPosition = ZPosition = 0;
    
            Range = 1000;
    
            // Set attenuation to no attenuation.
            Attenuation0 = 1;
            Attenuation1 = 0;
            Attenuation2 = 0;
        }
    
        #region D3dLight implementation
        protected override void SetSpecificLightValues(Device d3dDevice)
        {
            d3dDevice.Lights[Index].Type = LightType.Point;
    
            d3dDevice.Lights[Index].XPosition = XPosition;
            d3dDevice.Lights[Index].YPosition = YPosition;
            d3dDevice.Lights[Index].ZPosition = ZPosition;
    
            d3dDevice.Lights[Index].Range = Range;
    
            d3dDevice.Lights[Index].Attenuation0 = Attenuation0;
            d3dDevice.Lights[Index].Attenuation1 = Attenuation1;
            d3dDevice.Lights[Index].Attenuation2 = Attenuation2;
        }
        #endregion
    
        #region properties
    
        public float XPosition { get; set; }
        public float YPosition { get; set; }
        public float ZPosition { get; set; }
    
        public float Range { get; set; }
    
        public float Attenuation0 { get; set; }
        public float Attenuation1 { get; set; }
        public float Attenuation2 { get; set; }
    
        #endregion
    }
  • Spot light:
    public class D3dSpotLight : D3dLight
    {
        public D3dSpotLight()
        {
            XPosition = YPosition = ZPosition = 0;
    
            XDirection = 1;
            YDirection = -1;
            ZDirection = -1;
    
            Range = 1000;
    
            // Set attenuation to no attenuation.
            Attenuation0 = 1;
            Attenuation1 = 0;
            Attenuation2 = 0;
    
            InnerConeAngle = (float)(Math.PI / 16);
            OuterConeAngle = (float)(Math.PI / 4);
            Falloff = 1;
        }
    
        #region D3dLight implementation
        protected override void SetSpecificLightValues(Device d3dDevice)
        {
            d3dDevice.Lights[Index].Type = LightType.Spot;
    
            d3dDevice.Lights[Index].XPosition = XPosition;
            d3dDevice.Lights[Index].YPosition = YPosition;
            d3dDevice.Lights[Index].ZPosition = ZPosition;
    
            d3dDevice.Lights[Index].XDirection = XDirection;
            d3dDevice.Lights[Index].YDirection = YDirection;
            d3dDevice.Lights[Index].ZDirection = ZDirection;
    
            d3dDevice.Lights[Index].Range = Range;
    
            d3dDevice.Lights[Index].Attenuation0 = Attenuation0;
            d3dDevice.Lights[Index].Attenuation1 = Attenuation1;
            d3dDevice.Lights[Index].Attenuation2 = Attenuation2;
    
            d3dDevice.Lights[Index].InnerConeAngle = InnerConeAngle;
            d3dDevice.Lights[Index].OuterConeAngle = OuterConeAngle;
            d3dDevice.Lights[Index].Falloff = Falloff;
        }
        #endregion
    
        #region properties
    
        public float XPosition { get; set; }
        public float YPosition { get; set; }
        public float ZPosition { get; set; }
    
        public float XDirection { get; set; }
        public float YDirection { get; set; }
        public float ZDirection { get; set; }
    
        public float Range { get; set; }
    
        public float Attenuation0 { get; set; }
        public float Attenuation1 { get; set; }
        public float Attenuation2 { get; set; }
    
        public float InnerConeAngle { get; set; }
        public float OuterConeAngle { get; set; }
        public float Falloff { get; set; }
    
        #endregion
    }

Create classes for implementing shapes:

  • Base class for shapes implementation:
    • Shape's material:
      public abstract class D3dShape
      {
          protected D3dShape()
          {
              DefaultMaterial = new Material
              {
                  DiffuseColor = new ColorValue(1.0f, 1.0f, 1.0f, 1.0f)
              };
      
              IsVisible = true;
          }
      
          protected static object _shapeLoaderLock = "Shape load lock";
      
          public Material DefaultMaterial { get; set; }
      
          public D3dShape Parent { get; set; }
      
          public bool IsVisible { get; set; }
      }
    • World matrix:
      • Properties for holding the shape's translation:
        #region TranslationX
        private float _translationX;
        public float TranslationX
        {
            get { return _translationX; }
            set
            {
                lock (this)
                {
                    _translationX = value;
                    _isOwnWorldMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region TranslationY
        private float _translationY;
        public float TranslationY
        {
            get { return _translationY; }
            set
            {
                lock (this)
                {
                    _translationY = value;
                    _isOwnWorldMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region TranslationZ
        private float _translationZ;
        public float TranslationZ
        {
            get { return _translationZ; }
            set
            {
                lock (this)
                {
                    _translationZ = value;
                    _isOwnWorldMatrixValid = false;
                }
            }
        }
        #endregion
      • Properties for holding the shape's scaling:
        #region ScalingX
        private float _scalingX;
        public float ScalingX
        {
            get { return _scalingX; }
            set
            {
                lock (this)
                {
                    _scalingX = value;
                    _isOwnWorldMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region ScalingY
        private float _scalingY;
        public float ScalingY
        {
            get { return _scalingY; }
            set
            {
                lock (this)
                {
                    _scalingY = value;
                    _isOwnWorldMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region ScalingZ
        private float _scalingZ;
        public float ScalingZ
        {
            get { return _scalingZ; }
            set
            {
                lock (this)
                {
                    _scalingZ = value;
                    _isOwnWorldMatrixValid = false;
                }
            }
        }
        #endregion
      • Properties for holding the shape's rotation:
        #region RotationX
        private float _rotationX;
        public float RotationX
        {
            get { return _rotationX; }
            set
            {
                lock (this)
                {
                    _rotationX = value;
                    _isOwnWorldMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region RotationY
        private float _rotationY;
        public float RotationY
        {
            get { return _rotationY; }
            set
            {
                lock (this)
                {
                    _rotationY = value;
                    _isOwnWorldMatrixValid = false;
                }
            }
        }
        #endregion
        
        #region RotationZ
        private float _rotationZ;
        public float RotationZ
        {
            get { return _rotationZ; }
            set
            {
                lock (this)
                {
                    _rotationZ = value;
                    _isOwnWorldMatrixValid = false;
                }
            }
        }
        #endregion
      • Property for holding the own world matrix:
        private Matrix _ownWorldMatrix;
        public Matrix OwnWorldMatrix 
        { 
            get
            {
                lock (this)
                {
                    if (!_isOwnWorldMatrixValid)
                    {
                        _ownWorldMatrix = CalculateOwnWorldMatrix();
                        _isOwnWorldMatrixValid = true;
                    }
                }
        
                return _ownWorldMatrix;
            }
        }
        
        protected virtual Matrix CalculateOwnWorldMatrix()
        {
            return Matrix.Scaling(new Vector3(ScalingX, ScalingY, ScalingZ))*
                    Matrix.RotationX(RotationX)*Matrix.RotationY(RotationY)*
                    Matrix.RotationZ(RotationZ)*
                    Matrix.Translation(new Vector3(TranslationX, TranslationY, TranslationZ));
        }
        
        // In order to calculate the own world matrix only when it is needed,
        // we hold a data-member for indicating if the own world matrix is valid.
        protected bool _isOwnWorldMatrixValid;
      • Method for getting the actual world matrix:
        public virtual Matrix GetActualWorldMatrix()
        {
            Matrix res = OwnWorldMatrix;
        
            if (null != Parent)
            {
                res *= Parent.GetActualWorldMatrix();
            }
        
            return res;
        }
        
        public Matrix CurrentWorldMatrix { get;  protected set; }
    • Abstract method for rendering the shape:
      public abstract void Render(Device d3dDevice);
  • Base class for single shapes implementation:
    public abstract class D3dSingleShape : D3dShape
    {
        #region D3dShape implementation
    
        public override void Render(Device d3dDevice)
        {
            if (null == d3dDevice)
            {
                return;
            }
    
            if (!IsVisible)
            {
                return;
            }
    
            InitDrawing();
    
            CurrentWorldMatrix = GetActualWorldMatrix();
    
            lock (d3dDevice)
            {
                d3dDevice.Material = DefaultMaterial;
                d3dDevice.Transform.World = CurrentWorldMatrix;
    
                Draw(d3dDevice);
            }
        }
        #endregion
    
        // Since we want to lock the DirectX's device as short time as we can,
        // we separate the rendering to 2 methods:
        // InitDrawing - The initialization code that has to be done before the rendering
        //               (and, doesn't need the DirectX's device).
        // Draw - The rendering code that uses the DirectX's device.
        protected abstract void InitDrawing();
        protected abstract void Draw(Device d3dDevice);
    }
  • Composed shape:
    public class D3dComposedShape : D3dShape
    {
        #region D3dShape implementation
    
        public override void Render(Device d3dDevice)
        {
            if (!IsVisible)
            {
                return;
            }
    
            CurrentWorldMatrix = GetActualWorldMatrix();
    
            Shapes.ForEach(s => RenderShape(s, d3dDevice));
        }
        #endregion
    
        protected virtual void RenderShape(D3dShape shape, Device d3dDevice)
        {
            shape.Parent = this;
            shape.RenderAlsoIfOutOfView = RenderAlsoIfOutOfView;
            shape.EnvironmentData = EnvironmentData;
            shape.Render(d3dDevice);
        }
    
        #region Shapes
        private List<D3dShape> _shapes;
        public List<D3dShape> Shapes
        {
            get { return _shapes ?? (_shapes = new List<D3dShape>()); }
        }
        #endregion
    }
  • A simple box:
    public class D3dBox : D3dSingleShape
    {
        private const int _verticesNumber = 36; // (6 sides ) * (2 triangles) * (3 vertices)
        private static CustomVertex.PositionNormal[] _boxVertices = null;
    
        #region D3dSingleShape implementation
    
        protected override void InitDrawing()
        {
            if (null != _boxVertices)
            {
                // The vertices are already initiated.
                return;
            }
    
            lock (_shapeLoaderLock)
            {
                if (null == _boxVertices)
                {
                    // Positions
                    Vector3 frontTopLeftPosition = new Vector3(-0.5f, 0.5f, 0.5f);
                    Vector3 frontTopRightPosition = new Vector3(0.5f, 0.5f, 0.5f);
                    Vector3 frontBottomLeftPosition = new Vector3(-0.5f, -0.5f, 0.5f);
                    Vector3 frontBottomRightPosition = new Vector3(0.5f, -0.5f, 0.5f);
                    Vector3 backTopLeftPosition = new Vector3(-0.5f, 0.5f, -0.5f);
                    Vector3 backTopRightPosition = new Vector3(0.5f, 0.5f, -0.5f);
                    Vector3 backBottomLeftPosition = new Vector3(-0.5f, -0.5f, -0.5f);
                    Vector3 backBottomRightPosition = new Vector3(0.5f, -0.5f, -0.5f);
    
                    // Normals
                    Vector3 frontNormal = new Vector3(0, 0, 1);
                    Vector3 backNormal = new Vector3(0, 0, -1);
                    Vector3 leftNormal = new Vector3(-1, 0, 0);
                    Vector3 rightNormal = new Vector3(1, 0, 0);
                    Vector3 upNormal = new Vector3(0, 1, 0);
                    Vector3 downNormal = new Vector3(0, -1, 0);
    
                    // Vertices
                    CustomVertex.PositionNormal[] vertices = new CustomVertex.PositionNormal[_verticesNumber];
    
                    // Front
                    SetPositionNormalRectangle(vertices, 0, frontTopRightPosition, frontTopLeftPosition,
                                                frontBottomLeftPosition, frontBottomRightPosition, frontNormal);
    
                    // Left
                    SetPositionNormalRectangle(vertices, 6, frontTopLeftPosition, backTopLeftPosition,
                                                backBottomLeftPosition,
                                                frontBottomLeftPosition, leftNormal);
    
                    // Up
                    SetPositionNormalRectangle(vertices, 12, frontTopLeftPosition, frontTopRightPosition,
                                                backTopRightPosition, backTopLeftPosition, upNormal);
    
                    // Right
                    SetPositionNormalRectangle(vertices, 18, backTopRightPosition, frontTopRightPosition,
                                                frontBottomRightPosition, backBottomRightPosition, rightNormal);
    
                    // Down
                    SetPositionNormalRectangle(vertices, 24, frontBottomRightPosition, frontBottomLeftPosition,
                                                backBottomLeftPosition, backBottomRightPosition, downNormal);
    
                    // Back
                    SetPositionNormalRectangle(vertices, 30, backTopLeftPosition, backTopRightPosition,
                                                backBottomRightPosition, backBottomLeftPosition, backNormal);
    
                    _boxVertices = vertices;
                }
            }
        }
    
        protected override void Draw(Device d3dDevice)
        {
            if (null == _boxVertices)
            {
                return;
            }
    
            d3dDevice.VertexFormat = CustomVertex.PositionNormal.Format;
            d3dDevice.DrawUserPrimitives(PrimitiveType.TriangleList, _verticesNumber / 3, _boxVertices);
        }
        #endregion
    
        private void SetPositionNormalTriangle(CustomVertex.PositionNormal[] verticesArray, int startIndex,
            Vector3 firstPosition, Vector3 secondPosition, Vector3 thirdPosition, Vector3 normal)
        {
            verticesArray[startIndex].Position = firstPosition;
            verticesArray[startIndex].Normal = normal;
            verticesArray[startIndex + 1].Position = secondPosition;
            verticesArray[startIndex + 1].Normal = normal;
            verticesArray[startIndex + 2].Position = thirdPosition;
            verticesArray[startIndex + 2].Normal = normal;
        }
    
        private void SetPositionNormalRectangle(CustomVertex.PositionNormal[] verticesArray, int startIndex,
            Vector3 firstPosition, Vector3 secondPosition, Vector3 thirdPosition, Vector3 fourthPosition, Vector3 normal)
        {
            SetPositionNormalTriangle(verticesArray, startIndex, firstPosition, secondPosition, thirdPosition, normal);
            SetPositionNormalTriangle(verticesArray, startIndex + 3, thirdPosition, fourthPosition, firstPosition,
                                        normal);
        }
    }

and, create a class for holding a full scene:

public class D3dScene
{
    public D3dScene()
    {
        ClearColor = Color.Black;
    }

    public virtual void Render(Device d3dDevice)
    {
        if (null == d3dDevice)
        {
            return;
        }

        InitDevice(d3dDevice);

        d3dDevice.BeginScene();

        // Render camera
        RenderCamera(d3dDevice);

        // Render lights
        RenderLights(d3dDevice);

        // Render shapes
        RenderShapes(d3dDevice);

        d3dDevice.EndScene();
    }

    protected virtual void InitDevice(Device d3dDevice)
    {
        d3dDevice.RenderState.ZBufferEnable = true;
        d3dDevice.RenderState.Lighting = Lights.Count > 0;
        d3dDevice.RenderState.CullMode = Camera.CoordinateSystem == CameraCoordinateSystem.RightHanded
                                                ? Cull.Clockwise
                                                : Cull.CounterClockwise;
        d3dDevice.RenderState.NormalizeNormals = true;
        d3dDevice.RenderState.DiffuseMaterialSource = ColorSource.Material;

        d3dDevice.Clear(ClearFlags.Target | ClearFlags.ZBuffer, ClearColor, 1.0f, 0);
    }

    protected virtual void RenderCamera(Device d3dDevice)
    {
        Camera.Render(d3dDevice);
    }

    protected virtual void RenderLights(Device d3dDevice)
    {
        // Disable old scene lights.
        int lightsCount = d3dDevice.Lights.Count;
        for (int oldLightInx = 0; oldLightInx < lightsCount; oldLightInx++)
        {
            d3dDevice.Lights[oldLightInx].Enabled = false;
        }

        // Render the enabled scene lights.
        int lightIndex = 0;
        foreach (D3dLight light in Lights)
        {
            if (null != light && light.Enabled)
            {
                light.Index = lightIndex;
                light.Render(d3dDevice);
                lightIndex++;
            }
        }
    }

    protected virtual void RenderShapes(Device d3dDevice)
    {
        Shapes.ForEach(s => RenderShape(s, d3dDevice));
    }

    protected virtual void RenderShape(D3dShape shape, Device d3dDevice)
    {
        if (null == shape)
        {
            return;
        }

        shape.Render(d3dDevice);
    }

    #region Properties

    #region Camera
    private D3dCamera _camera;
    public D3dCamera Camera
    {
        get { return _camera ?? (_camera = new D3dPerspectiveFovCamera()); }
        set { _camera = value; }
    }
    #endregion

    #region Lights
    private List<D3dLight> _lights;
    public List<D3dLight> Lights
    {
        get { return _lights ?? (_lights = new List<D3dLight>()); }
    }
    #endregion

    #region Shapes
    private List<D3dShape> _shapes;
    public List<D3dShape> Shapes
    {
        get { return _shapes ?? (_shapes = new List<D3dShape>()); }
    }
    #endregion

    public Color ClearColor { get; set; }

    #endregion
}

Render parallelly

For improving the rendering performance, we can render the shapes parallelly using TPL, as follows:

public class D3dScene
{
    ...

    protected virtual void RenderShapes(Device d3dDevice)
    {
        if (RenderShapesParallelly)
        {
            Parallel.ForEach(Shapes, s => RenderShape(s, d3dDevice));
        }
        else
        {
            Shapes.ForEach(s => RenderShape(s, d3dDevice));
        }
    }

    public bool RenderShapesParallelly { get; set; }

    ...
}

Ignore out-of-view shapes

Another thing that can improve the rendering performance is discarding the out-of-view shapes. That can be done by notifying the shape about the environment matrices:

public class ShapeEnvironmentData
{
    public ShapeEnvironmentData()
    {
        _viewMatrix = Matrix.Identity;
        _projectionMatrix = Matrix.Identity;

        ViewProjectionMatrix = Matrix.Identity;
    }

    #region ViewMatrix

    private Matrix _viewMatrix;
    public Matrix ViewMatrix
    {
        get { return _viewMatrix; }
        set
        {
            _viewMatrix = value;
            ViewProjectionMatrix = GetViewProjectionMatrix();
        }
    }

    #endregion

    #region ProjectionMatrix

    private Matrix _projectionMatrix;
    public Matrix ProjectionMatrix
    {
        get { return _projectionMatrix; }
        set
        {
            _projectionMatrix = value;
            ViewProjectionMatrix = GetViewProjectionMatrix();
        }
    }

    #endregion

    #region ViewProjectionMatrix

    public Matrix ViewProjectionMatrix { get; protected set; }

    protected Matrix GetViewProjectionMatrix()
    {
        return ViewMatrix * ProjectionMatrix;
    }

    #endregion
}

public abstract class D3dShape
{
    ...

    public bool RenderAlsoIfOutOfView { get; set; }

    public ShapeEnvironmentData EnvironmentData { get; set; }

    ...
}

public class D3dScene
{
    ...

    protected virtual void RenderShapes(Device d3dDevice)
    {
        ShapeEnvironmentData environmentData = GetShapeEnvironmentData();

        if (RenderShapesParallelly)
        {
            Parallel.ForEach(Shapes, s => RenderShape(s, environmentData, d3dDevice));
        }
        else
        {
            Shapes.ForEach(s => RenderShape(s, environmentData, d3dDevice));
        }
    }

    protected virtual ShapeEnvironmentData GetShapeEnvironmentData()
    {
        return new ShapeEnvironmentData
                    {
                        ViewMatrix = Camera.ViewMatrix,
                        ProjectionMatrix = Camera.ProjectionMatrix
                    };
    }

    protected virtual void RenderShape(D3dShape shape, ShapeEnvironmentData environmentData, Device d3dDevice)
    {
        if (null == shape)
        {
            return;
        }

        shape.RenderAlsoIfOutOfView = RenderShapesAlsoIfOutOfView;
        shape.EnvironmentData = environmentData;
        shape.Render(d3dDevice);
    }

    public bool RenderShapesAlsoIfOutOfView { get; set; }

    ...
}

and, checking if the shape is in the projection region, for each rendering, as follows:

// Define the BoundingBox as a struct, in order to use it on the stack (instead of the heap).
public struct BoundingBox
{
    public BoundingBox(Vector3 minimalValues, Vector3 maximalValues)
    {
        FrontTopLeftPosition = new Vector3(minimalValues.X, maximalValues.Y, maximalValues.Z);
        FrontTopRightPosition = new Vector3(maximalValues.X, maximalValues.Y, maximalValues.Z);
        FrontBottomLeftPosition = new Vector3(minimalValues.X, minimalValues.Y, maximalValues.Z);
        FrontBottomRightPosition = new Vector3(maximalValues.X, minimalValues.Y, maximalValues.Z);
        BackTopLeftPosition = new Vector3(minimalValues.X, maximalValues.Y, minimalValues.Z);
        BackTopRightPosition = new Vector3(maximalValues.X, maximalValues.Y, minimalValues.Z);
        BackBottomLeftPosition = new Vector3(minimalValues.X, minimalValues.Y, minimalValues.Z);
        BackBottomRightPosition = new Vector3(maximalValues.X, minimalValues.Y, minimalValues.Z);
    }

    // Hold the box's points' positions as different data-members (instead of an array),
    // for preventing using a memory allocation for the BoundingBox's operations.
    public Vector3 FrontTopLeftPosition;
    public Vector3 FrontTopRightPosition;
    public Vector3 FrontBottomLeftPosition;
    public Vector3 FrontBottomRightPosition;
    public Vector3 BackTopLeftPosition;
    public Vector3 BackTopRightPosition;
    public Vector3 BackBottomLeftPosition;
    public Vector3 BackBottomRightPosition;

    public void TransformCoordinates(Matrix transformationMatrix)
    {
        FrontTopLeftPosition.TransformCoordinate(transformationMatrix);
        FrontTopRightPosition.TransformCoordinate(transformationMatrix);
        FrontBottomLeftPosition.TransformCoordinate(transformationMatrix);
        FrontBottomRightPosition.TransformCoordinate(transformationMatrix);
        BackTopLeftPosition.TransformCoordinate(transformationMatrix);
        BackTopRightPosition.TransformCoordinate(transformationMatrix);
        BackBottomLeftPosition.TransformCoordinate(transformationMatrix);
        BackBottomRightPosition.TransformCoordinate(transformationMatrix);
    }

    public bool IsOutOfBoundingBox(Vector3 boxMinimalValues, Vector3 boxMaximalValues)
    {
        return IsOutOfBoundingBox(boxMinimalValues.X, boxMinimalValues.Y, boxMinimalValues.Z,
                                boxMaximalValues.X, boxMaximalValues.Y, boxMaximalValues.Z);
    }

    public bool IsOutOfBoundingBox(float boxMinimalX, float boxMinimalY, float boxMinimalZ,
        float boxMaximalX, float boxMaximalY, float boxMaximalZ)
    {
        float thisMinimalX = GetMinimalX();
        float thisMinimalY = GetMinimalY();
        float thisMinimalZ = GetMinimalZ();
        float thisMaximalX = GetMaximalX();
        float thisMaximalY = GetMaximalY();
        float thisMaximalZ = GetMaximalZ();

        bool isOutOfXView = thisMinimalX > boxMaximalX || thisMaximalX < boxMinimalX;
        bool isOutOfYView = thisMinimalY > boxMaximalY || thisMaximalY < boxMinimalY;
        bool isOutOfZView = thisMinimalZ > boxMaximalZ || thisMaximalZ < boxMinimalZ;

        return isOutOfXView || isOutOfYView || isOutOfZView;
    }

    public float GetMinimalX()
    {
        return Min(FrontTopLeftPosition.X, FrontTopRightPosition.X, FrontBottomLeftPosition.X,
                    FrontBottomRightPosition.X, BackTopLeftPosition.X, BackTopRightPosition.X,
                    BackBottomLeftPosition.X, BackBottomRightPosition.X);
    }

    public float GetMinimalY()
    {
        return Min(FrontTopLeftPosition.Y, FrontTopRightPosition.Y, FrontBottomLeftPosition.Y,
                    FrontBottomRightPosition.Y, BackTopLeftPosition.Y, BackTopRightPosition.Y,
                    BackBottomLeftPosition.Y, BackBottomRightPosition.Y);
    }

    public float GetMinimalZ()
    {
        return Min(FrontTopLeftPosition.Z, FrontTopRightPosition.Z, FrontBottomLeftPosition.Z,
                    FrontBottomRightPosition.Z, BackTopLeftPosition.Z, BackTopRightPosition.Z,
                    BackBottomLeftPosition.Z, BackBottomRightPosition.Z);
    }

    public float GetMaximalX()
    {
        return Max(FrontTopLeftPosition.X, FrontTopRightPosition.X, FrontBottomLeftPosition.X,
                    FrontBottomRightPosition.X, BackTopLeftPosition.X, BackTopRightPosition.X,
                    BackBottomLeftPosition.X, BackBottomRightPosition.X);
    }

    public float GetMaximalY()
    {
        return Max(FrontTopLeftPosition.Y, FrontTopRightPosition.Y, FrontBottomLeftPosition.Y,
                    FrontBottomRightPosition.Y, BackTopLeftPosition.Y, BackTopRightPosition.Y,
                    BackBottomLeftPosition.Y, BackBottomRightPosition.Y);
    }

    public float GetMaximalZ()
    {
        return Max(FrontTopLeftPosition.Z, FrontTopRightPosition.Z, FrontBottomLeftPosition.Z,
                    FrontBottomRightPosition.Z, BackTopLeftPosition.Z, BackTopRightPosition.Z,
                    BackBottomLeftPosition.Z, BackBottomRightPosition.Z);
    }

    private float Min(float a, float b, float c, float d, float e, float f, float g, float h)
    {
        float minAB = a < b ? a : b;
        float minCD = c < d ? c : d;
        float minEF = e < f ? e : f;
        float minGH = g < h ? g : h;

        float minABCD = minAB < minCD ? minAB : minCD;
        float minEFGH = minEF < minGH ? minEF : minGH;

        return minABCD < minEFGH ? minABCD : minEFGH;

    }

    private float Max(float a, float b, float c, float d, float e, float f, float g, float h)
    {
        float maxAB = a > b ? a : b;
        float maxCD = c > d ? c : d;
        float maxEF = e > f ? e : f;
        float maxGH = g > h ? g : h;

        float maxABCD = maxAB > maxCD ? maxAB : maxCD;
        float maxEFGH = maxEF > maxGH ? maxEF : maxGH;

        return maxABCD > maxEFGH ? maxABCD : maxEFGH;
    }
}

public abstract class D3dShape
{
    ...

    private static readonly BoundingBox _defaultBoundingBoxBeforeTransformation =
        new BoundingBox(new Vector3(-0.5f, -0.5f, -0.5f), new Vector3(0.5f, 0.5f, 0.5f));

    public virtual BoundingBox GetBoundingBoxBeforeTransformation()
    {
        return _defaultBoundingBoxBeforeTransformation;
    }

    public virtual bool IsBoundingBoxOutOfView(Matrix worldViewProjectionMatrix)
    {
        // Get the shape's bounding-box in the projection region.
        BoundingBox boundingBox = GetBoundingBoxBeforeTransformation();
        boundingBox.TransformCoordinates(worldViewProjectionMatrix);

        // Check if the shape's bounding-box is out of the projection region.
        return boundingBox.IsOutOfBoundingBox(-1f, -1f, 0f, 1f, 1f, 1f);
    }

    public virtual bool IsBoundingBoxOutOfView()
    {
        if (null == EnvironmentData)
        {
            return false;
        }

        return IsBoundingBoxOutOfView(CurrentWorldMatrix * EnvironmentData.ViewProjectionMatrix);
    }

    ...
}

public abstract class D3dSingleShape : D3dShape
{
    ...

    public override void Render(Device d3dDevice)
    {
        if (null == d3dDevice)
        {
            return;
        }

        if (!IsVisible)
        {
            return;
        }

        InitDrawing();

        CurrentWorldMatrix = GetActualWorldMatrix();

        if (!RenderAlsoIfOutOfView && IsBoundingBoxOutOfView())
        {
            return;
        }

        lock (d3dDevice)
        {
            d3dDevice.Material = DefaultMaterial;
            d3dDevice.Transform.World = CurrentWorldMatrix;

            Draw(d3dDevice);
        }
    }

    ...
}

Picking

Intersections test

One of the operations we may want to perform on our scene is, checking if a point on the rendered surface is contained in a specific shape. We can do that by, checking if any of the shapes triangles, intersects with a ray, that contains the whole of the 3D points that are rendered in the wanted surface's point.

For getting the triangles' intersections of a single shape, we can add a method for getting the shapes triangles:

public struct TrianglePointsPositions
{
    public Vector3 Position1;
    public Vector3 Position2;
    public Vector3 Position3;
}

public abstract class D3dSingleShape : D3dShape
{
    ...

    protected abstract IEnumerable<TrianglePointsPositions> GetTrianglesPointsPositions();
}

public class D3dBox : D3dSingleShape
{
    private const int _verticesNumber = 36; // (6 sides ) * (2 triangles) * (3 vertices)
    private static CustomVertex.PositionNormal[] _boxVertices = null;

    private const int _trianglesNumber = 12; // (6 sides ) * (2 triangles)
    private static TrianglePointsPositions[] _boxTriangles = null;

    ...

    protected override IEnumerable<TrianglePointsPositions> GetTrianglesPointsPositions()
    {
        if (null != _boxTriangles)
        {
            // The triangles are already initiated.
            return _boxTriangles;
        }

        if (null == _boxVertices)
        {
            return null;
        }

        lock (_shapeLoaderLock)
        {
            if (null == _boxTriangles)
            {
                TrianglePointsPositions[] triangles = new TrianglePointsPositions[_trianglesNumber];

                for (int triangleInx = 0; triangleInx < _trianglesNumber; triangleInx++)
                {
                    triangles[triangleInx] = new TrianglePointsPositions
                    {
                        Position1 = _boxVertices[triangleInx * 3].Position,
                        Position2 = _boxVertices[(triangleInx * 3) + 1].Position,
                        Position3 = _boxVertices[(triangleInx * 3) + 2].Position
                    };
                }

                _boxTriangles = triangles;
            }
        }

        return _boxTriangles;
    }
}

and, get the triangles' intersections according to a given ray:

public abstract class D3dShape
{
    ...

    public abstract void AddRayIntersections(Vector3 rayOrigin, Vector3 rayDirection,
                                                List<IntersectResult> targetIntersectionsList);

    public bool IsHitTestVisible { get; set; }

    ...
}

public abstract class D3dSingleShape : D3dShape
{
    ...

    public override void AddRayIntersections(Vector3 rayOrigin, Vector3 rayDirection,
                                                List<IntersectResult> targetIntersectionsList)
    {
        if (!IsHitTestVisible)
        {
            return;
        }

        // Transform the given ray according to the shape's world matrix.
        Matrix invertedWorldMatrix = Matrix.Invert(CurrentWorldMatrix);
        Vector3 transformedRayOrigin = Vector3.TransformCoordinate(rayOrigin, invertedWorldMatrix);
        Vector3 transformedRayDirection =
            Vector3.TransformNormal(rayDirection, invertedWorldMatrix);

        // Check the shape's intersections according to the transformed ray.
        AddTransformedRayIntersections(transformedRayOrigin, transformedRayDirection, targetIntersectionsList);
    }

    protected void AddTransformedRayIntersections(Vector3 transformedRayOrigin, Vector3 transformedRayDirection,
        List<IntersectResult> targetIntersectionsList)
    {
        if (null == targetIntersectionsList)
        {
            return;
        }

        IntersectResult shapeIntersection = null;

        IEnumerable<TrianglePointsPositions> triangles = GetTrianglesPointsPositions();
        if (null == triangles)
        {
            return;
        }

        int triangleInx = 0;
        foreach (TrianglePointsPositions triangle in triangles)
        {
            IntersectInformation currTriangleIntersection;

            Geometry.IntersectTri(triangle.Position1, triangle.Position2, triangle.Position3, transformedRayOrigin,
                                    transformedRayDirection, out currTriangleIntersection);

            if (currTriangleIntersection.Dist > 0.0f)
            {
                if (null == shapeIntersection)
                {
                    // There is an intersection. So, create an intersection result and, add it to the list.
                    shapeIntersection = new IntersectResult
                                            {
                                                Shape = this
                                            };

                    lock (targetIntersectionsList)
                    {
                        targetIntersectionsList.Add(shapeIntersection);
                    }
                }

                currTriangleIntersection.FaceIndex = triangleInx;
                shapeIntersection.TriangleIntersections.Add(currTriangleIntersection);
            }

            triangleInx++;
        }
    }

    ...
}

For getting the triangles' intersections of a composed shape, we can merge the triangles' intersections of its inner shapes:

public class D3dComposedShape : D3dShape
{
    ...

    public override void AddRayIntersections(Vector3 rayOrigin, 
        Vector3 rayDirection, List<IntersectResult> targetIntersectionsList)
    {
        if (!IsHitTestVisible)
        {
            return;
        }

        Parallel.ForEach(Shapes, shape => shape.AddRayIntersections(rayOrigin, 
                         rayDirection, targetIntersectionsList));
    }

    ...
}

Pick the appropriate shape

Now, after we have the intersections check, all what we have to do is, to get the appropriate ray and, to use it for the intersections checks:

public class D3dScene
{
    ...

    public D3dShape Pick(Device d3dDevice, float surfaceX, float surfaceY)
    {
        LastPickIntersections = GetPointIntersections(d3dDevice, surfaceX, surfaceY);

        // Get the closest shape.
        D3dShape res = GetLastPickClosestShape();

        return res;
    }

    public List<IntersectResult> GetPointIntersections(Device d3dDevice, float surfaceX, float surfaceY)
    {
        Vector3 nearPlanePoint = new Vector3(surfaceX, surfaceY, 0);
        Vector3 farPlanePoint = new Vector3(surfaceX, surfaceY, 1);

        nearPlanePoint.Unproject(d3dDevice.Viewport, d3dDevice.Transform.Projection, d3dDevice.Transform.View,
                                    Matrix.Identity);
        farPlanePoint.Unproject(d3dDevice.Viewport, d3dDevice.Transform.Projection, d3dDevice.Transform.View,
                        Matrix.Identity);

        Vector3 rayDirection = Vector3.Subtract(farPlanePoint, nearPlanePoint);

        List<IntersectResult> res = new List<IntersectResult>();

        Parallel.ForEach(Shapes, shape => shape.AddRayIntersections(nearPlanePoint, rayDirection, res));

        return res;
    }

    public D3dShape GetLastPickClosestShape()
    {
        return LastPickIntersections.OrderBy(ir => ir.TriangleIntersections.Min(ti => ti.Dist)).
            Select(ir => ir.Shape).FirstOrDefault();
    }

    #region LastPickIntersections
    private List<IntersectResult> _lastPickIntersections;
    public List<IntersectResult> LastPickIntersections
    {
        get { return _lastPickIntersections ?? (_lastPickIntersections = new List<IntersectResult>()); }
        protected set { _lastPickIntersections = value; }
    }
    #endregion

    ...
}

For supporting unpickable shapes, we can add a property that indicates if a shape is pickable:

public abstract class D3dShape
{
    ...

    public bool IsPickable { get; set; }

    ...
}

and, use it in the picking operation:

public class D3dScene
{
    ...

    public D3dShape Pick(Device d3dDevice, float surfaceX, float surfaceY)
    {
        LastPickIntersections = GetPointIntersections(d3dDevice, surfaceX, surfaceY);

        // Get the closest shape.
        D3dShape res = GetLastPickClosestShape();

        // Get the pickable parent of the shape.
        while (null != res && !res.IsPickable)
        {
            res = res.Parent;
        }

        return res;
    }

    ...
}

Camera transformations

Zoom

The first camera operation we are going to discuss on, is zooming. In that operation, we scale the camera's view according to a given scaling factor.

We can enable zooming in our cameras by, adding an abstract Zoom method to the base class:

public abstract class D3dCamera
{
    ...

    public abstract void Zoom(float scalingFactorX, float scalingFactorY);

    ...
}

and, implement it appropriately in the derived classes:

  • For zooming a D3dOrthoCamera and a D3dPerspectiveCamera, we can multiply the Width and the Height with the given parameters appropriately:
    public override void Zoom(float scalingFactorX, float scalingFactorY)
    {
        Width *= scalingFactorX;
        Height *= scalingFactorY;
    }
  • For zooming a D3dOrthoOffCenterCamera and a D3dPerspectiveOffCenterCamera, we can change the Left, the Right, the Bottom and, the Top, according to the given parameters:
    public override void Zoom(float scalingFactorX, float scalingFactorY)
    {
        float centerX = (Left + Right)/2;
        float centerY = (Bottom + Top)/2;
    
        float halfWidth = ((Right - Left)/2)*scalingFactorX;
        float halfHeight = ((Top - Bottom)/2)*scalingFactorY;
    
        Left = centerX - halfWidth;
        Right = centerX + halfWidth;
        Bottom = centerY - halfHeight;
        Top = centerY + halfHeight;
    }
  • For zooming a D3dPerspectiveFovCamera, we have to calculate the new FieldOfViewY and the new AspectRatio:
    • The FieldOfViewY can be calculated with the following equation:
    • fov zoom

      =>

      public override void Zoom(float scalingFactorX, float scalingFactorY)
      {
          double newFov = Math.Atan(Math.Tan(FieldOfViewY/2)*scalingFactorY)*2;
          FieldOfViewY = (float) newFov;
      }
    • The AspectRatio's scaling is the quotient of the X-scaling and the Y-scaling:
      public override void Zoom(float scalingFactorX, float scalingFactorY)
      {
          double newFov = Math.Atan(Math.Tan(FieldOfViewY/2)*scalingFactorY)*2;
          FieldOfViewY = (float) newFov;
      
          AspectRatio *= (scalingFactorX/scalingFactorY);
      }

Rotate

Relative axes

The second camera operation we are going to discuss on, is rotating. Since we want to rotate our camera according to the current view, we have to know the directions of the relative axes:

  • The relative Y axis direction is: the direction of the Up vector:
    public Vector3 GetRelativeYAxisDirection()
    {
        Vector3 relativeYAxisDirection = new Vector3(UpX, UpY, UpZ);
        relativeYAxisDirection.Normalize();
    
        return relativeYAxisDirection;
    }
  • The relative Z axis direction (from the camera to the scene) is: the difference between the target and the position:
    public Vector3 GetRelativeZAxisDirection()
    {
        Vector3 cameraPosition = new Vector3(PositionX, PositionY, PositionZ);
        Vector3 targetPosition = new Vector3(TargetX, TargetY, TargetZ);
        Vector3 relativeZAxisDirection = targetPosition - cameraPosition;
        relativeZAxisDirection.Normalize();
    
        return relativeZAxisDirection;
    }
  • The relative X axis direction is: the Y axis direction, rotated by 90 degrees, around the relative Z axis:
    public Vector3 GetRelativeXAxisDirection()
    {
        Vector3 relativeZAxisDirection = GetRelativeZAxisDirection();
        Vector3 relativeXAxisDirection = GetRelativeYAxisDirection();
        float rotationRadians = (CameraCoordinateSystem.RightHanded == CoordinateSystem)
                                    ? (float) Math.PI/2
                                    : (float) -Math.PI/2;
        relativeXAxisDirection.TransformNormal(Matrix.RotationAxis(relativeZAxisDirection, rotationRadians));
    
        return relativeXAxisDirection;
    }
Camera and target rotation

Now, after we have the relative axes, we can use them for our rotation.

The rotation can be done around the camera's target (rotation of the camera's position) or, around the camera's position (rotation of the camera's target). For indicating the rotation's center, we add the following enum:

public enum CameraTransformationCenterPosition
{
    CameraPosition,
    TargetPosition
}

For rotating the camera around the wanted rotation's center, we can:

  1. Translate the camera to put the rotation center in the (0,0,0) coordinate.
  2. Apply the wanted rotation on the other point (the camera's position or, the camera's target).
  3. Translate the camera back.

That can be done as follows:

public void Rotate(Matrix rotationTransformation, CameraTransformationCenterPosition centerPosition)
{
    Matrix translationBefore;
    Matrix translationAfter;
    Matrix transformation;

    if (CameraTransformationCenterPosition.TargetPosition == centerPosition)
    {
        translationBefore = Matrix.Translation(-TargetX, -TargetY, -TargetZ);
        translationAfter = Matrix.Translation(TargetX, TargetY, TargetZ);
        transformation = translationBefore*rotationTransformation*translationAfter;

        TransformPosition(transformation);
    }
    else
    {
        // The transform center is the camera's position.

        translationBefore = Matrix.Translation(-PositionX, -PositionY, -PositionZ);
        translationAfter = Matrix.Translation(PositionX, PositionY, PositionZ);
        transformation = translationBefore*rotationTransformation*translationAfter;

        TransformTarget(transformation);
    }

    TransformUp(rotationTransformation);
}

public void TransformPosition(Matrix transformation)
{
    Vector3 cameraPosition = new Vector3(PositionX, PositionY, PositionZ);
    cameraPosition.TransformCoordinate(transformation);
    PositionX = cameraPosition.X;
    PositionY = cameraPosition.Y;
    PositionZ = cameraPosition.Z;
}

public void TransformTarget(Matrix transformation)
{
    Vector3 targetPosition = new Vector3(TargetX, TargetY, TargetZ);
    targetPosition.TransformCoordinate(transformation);
    TargetX = targetPosition.X;
    TargetY = targetPosition.Y;
    TargetZ = targetPosition.Z;
}

public void TransformUp(Matrix transformation)
{
    Vector3 upDirection = new Vector3(UpX, UpY, UpZ);
    upDirection.TransformCoordinate(transformation);
    UpX = upDirection.X;
    UpY = upDirection.Y;
    UpZ = upDirection.Z;
}

Finally, after we have the implementation of the rotation, we can add methods for performing the rotation for each axis:

public enum CameraRotationDirection
{
    Clockwise,
    CounterClockwise
}

public void RelativeRotateX(float relativeArcLength,
                            CameraTransformationCenterPosition centerPosition,
                            CameraRotationDirection rotationDirection)
{
    const float pi = (float) Math.PI;

    float rotation = relativeArcLength*(pi*2);

    if ((CameraCoordinateSystem.LeftHanded == CoordinateSystem &&
            CameraRotationDirection.CounterClockwise == rotationDirection) ||
        (CameraCoordinateSystem.RightHanded == CoordinateSystem &&
            CameraRotationDirection.Clockwise == rotationDirection))
    {
        rotation *= -1f;
    }

    Vector3 relativeXAxisDirection = GetRelativeXAxisDirection();
    Matrix rotationTransformation = Matrix.RotationAxis(relativeXAxisDirection, rotation);
    Rotate(rotationTransformation, centerPosition);
}

public void RelativeRotateY(float relativeArcLength,
                            CameraTransformationCenterPosition centerPosition,
                            CameraRotationDirection rotationDirection)
{
    const float pi = (float) Math.PI;

    float rotation = relativeArcLength*(pi*2);

    if ((CameraCoordinateSystem.LeftHanded == CoordinateSystem &&
            CameraRotationDirection.CounterClockwise == rotationDirection) ||
        (CameraCoordinateSystem.RightHanded == CoordinateSystem &&
            CameraRotationDirection.Clockwise == rotationDirection))
    {
        rotation *= -1f;
    }

    Vector3 relativeYAxisDirection = GetRelativeYAxisDirection();
    Matrix rotationTransformation = Matrix.RotationAxis(relativeYAxisDirection, rotation);
    Rotate(rotationTransformation, centerPosition);
}

public void RelativeRotateZ(float relativeArcLength,
                            CameraTransformationCenterPosition centerPosition,
                            CameraRotationDirection rotationDirection)
{
    const float pi = (float) Math.PI;

    float rotation = relativeArcLength*(pi*2);

    if ((CameraRotationDirection.Clockwise == rotationDirection &&
            CameraTransformationCenterPosition.CameraPosition == centerPosition) ||
        (CameraRotationDirection.CounterClockwise == rotationDirection &&
            CameraTransformationCenterPosition.TargetPosition == centerPosition))
    {
        if (CameraCoordinateSystem.RightHanded == CoordinateSystem)
        {
            rotation *= -1f;
        }
    }
    else
    {
        if (CameraCoordinateSystem.LeftHanded == CoordinateSystem)
        {
            rotation *= -1f;
        }
    }

    Vector3 relativeZAxisDirection = GetRelativeZAxisDirection();
    Matrix rotationTransformation = Matrix.RotationAxis(relativeZAxisDirection, rotation);
    Rotate(rotationTransformation, centerPosition);
}

Move

The third camera operation we are going to discuss on, is moving.

For moving the camera forwards and backwards, we can:

  1. Get the appropriate distance according to the view's depth and the given relative-distance.
  2. Move the camera by the gotten distance on the relative Z axis.

That can be done as follows:

public virtual void RelativeZMove(float relativeDistance)
{
    float projectionRegionDepth = ZFarPlane - ZNearPlane;
    Vector3 translationVector = GetRelativeZAxisDirection();
    translationVector.Multiply(projectionRegionDepth*relativeDistance);
    Matrix translation = Matrix.Translation(translationVector);

    TransformPosition(translation);
    TransformTarget(translation);
}

For moving the camera horizontally and vertically, relative to the current view, we can:

  1. Get the appropriate distance according to the view's width and height and, the given relative-distances.
  2. Move the camera by the gotten distances on the relative X and Y axes.

That can be done as follows:

public override void RelativeXyMove(float relativeDistanceX, float relativeDistanceY)
{
    Vector3 translateXyDistance = new Vector3(relativeDistanceX, relativeDistanceY, 0);
    Matrix invertedProjectionMatrix = Matrix.Invert(ProjectionMatrix);
    translateXyDistance.TransformCoordinate(invertedProjectionMatrix);

    Vector3 translateX = GetRelativeXAxisDirection();
    translateX.Multiply(translateXyDistance.X);
    Vector3 translateY = GetRelativeYAxisDirection();
    translateY.Multiply(translateXyDistance.Y);

    Matrix translation = Matrix.Translation(translateX) * Matrix.Translation(translateY);
    TransformPosition(translation);
    TransformTarget(translation);            
}

When dealing with an orthographic camera, since the near-plane and the far-plane are with the same dimensions (and so the planes between), moving the camera according to the near-plane's dimensions, gives us the wanted moving effect (we see the whole of the shapes, moving by the wanted distance, no matter how deep they are in the scene). But, when dealing with a perspective camera, moving the camera according to the near-plane's dimensions, doesn't enough to give us the wanted moving effect (the near shapes are moved by more distance than the far shapes). So, in the case of perspective camera, we can make the wanted moving effect, by rotating the camera according to the camera's field-of-view.

For rotating a perspective camera according to its field-of-view, we add an abstract GetFov method to the D3dPerspectiveCameraBase class:

public abstract class D3dPerspectiveCameraBase : D3dCamera
{
    ...

    public abstract void GetFov(out float fovX, out float fovY);

    ...
}

implement it appropriately in the derived classes:

  • For D3dPerspectiveFovCamera, we can set the FieldOfViewY property as the Y's field-of-view and, set the FieldOfViewY multiplied by the AspectRatio as the X's field-of-view:
    public override void GetFov(out float fovX, out float fovY)
    {
        fovX = FieldOfViewY*AspectRatio;
        fovY = FieldOfViewY;
    }
  • For D3dPerspectiveCamera, we can calculate the field-of-view, with the following equation:

    fov move

    =>

    public override void GetFov(out float fovX, out float fovY)
    {
        fovX = (float) Math.Atan(Width/(2*ZNearPlane))*2;
        fovY = (float) Math.Atan(Height/(2*ZNearPlane))*2;
    }
  • For D3dPerspectiveOffCenterCamera, we can:
    1. Calculate the width and the height, according to the Left, the Right, the Bottom and, the Top properties.
      public override void GetFov(out float fovX, out float fovY)
      {
          float width = Right - Left;
          float height = Top - Bottom;
      }
    2. Calculate the field-of-view, as same as we calculated it in the D3dPerspectiveCamera camera, using the calculated width and height:
      public override void GetFov(out float fovX, out float fovY)
      {
          float width = Right - Left;
          float height = Top - Bottom;
      
          fovX = (float)Math.Atan(width / (2 * ZNearPlane)) * 2;
          fovY = (float)Math.Atan(height / (2 * ZNearPlane)) * 2;
      }

and, use it for rotating the camera appropriately:

public override void RelativeXyMove(float relativeDistanceX, float relativeDistanceY)
{
    // Get the field of view.
    float fovX;
    float fovY;
    GetFov(out fovX, out fovY);

    // Adjust rotation for X move
    Vector3 yAxis = GetRelativeYAxisDirection();
    float xAngle = (fovX/2)*relativeDistanceX;
    if (CameraCoordinateSystem.RightHanded == CoordinateSystem)
    {
        xAngle *= -1f;
    }
    Matrix rotationY = Matrix.RotationAxis(yAxis, xAngle);
    Rotate(rotationY, CameraTransformationCenterPosition.CameraPosition);

    // Adjust rotation for Y move
    Vector3 xAxis = GetRelativeXAxisDirection();
    float yAngle = (fovY/2)*relativeDistanceY;
    if (CameraCoordinateSystem.LeftHanded == CoordinateSystem)
    {
        yAngle *= -1f;
    }
    Matrix rotationX = Matrix.RotationAxis(xAxis, yAngle);
    Rotate(rotationX, CameraTransformationCenterPosition.CameraPosition);
}

Zoom to projection region

The fourth camera operation we are going to discuss on, is zooming the camera to view a specific region on the projected scene.

For zooming the camera to view a specific projection region, we can:

  1. Move the camera to look at the center of the projection region.
  2. Zoom the camera according to the dimensions of the projection region.

That can be done as follows:

public void ZoomToProjectionRegion(float projectionRegionLeft, float projectionRegionTop,
    float projectionRegionRight, float projectionRegionBottom, bool keepAspectRatio = false)
{
    // Move the camera to look at the center of the projection region.
    float centerX = (projectionRegionRight + projectionRegionLeft) / 2;
    float centerY = (projectionRegionBottom + projectionRegionTop) / 2;

    // Since the projection coordinates are between -1 and 1 (and we are in  the prjection  coordinates),
    // the relative distance is equal to the actual distance...
    RelativeXyMove(centerX, centerY);

    float projectionRegionWidth = Math.Abs(projectionRegionRight - projectionRegionLeft);
    float projectionRegionHeight = Math.Abs(projectionRegionTop - projectionRegionBottom);

    if (keepAspectRatio)
    {
        float scalingFactor = Math.Max(projectionRegionWidth, projectionRegionHeight) / 2;
        Zoom(scalingFactor, scalingFactor);
    }
    else
    {
        Zoom(projectionRegionWidth / 2, projectionRegionHeight / 2);
    }
}

Adjust camera view

The last camera operation we are going to discuss on, is adjusting the camera to view the whole of the scene's shapes.

For adjusting a camera's view to contain a set of a given shapes, we can:

  1. Get the projection region of the whole of the given shapes:
    public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
    {
        if (null == shapes)
        {
            return;
        }
    
        D3dShape[] shapesArray = shapes.ToArray();
    
        float projectionRegionLeft;
        float projectionRegionTop;
        float projectionRegionFront;
        float projectionRegionRight;
        float projectionRegionBottom;
        float projectionRegionBack;
    
        // Get the shapes' projection region.
        GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
                                    out projectionRegionFront,
                                    out projectionRegionRight, out projectionRegionBottom,
                                    out projectionRegionBack);      
    }
    
    protected virtual void GetShapesProjectionRegion(IEnumerable<D3dShape> shapes, 
                                    out float projectionRegionLeft, out float projectionRegionTop,
                                    out float projectionRegionFront, out float projectionRegionRight,
                                    out float projectionRegionBottom, out float projectionRegionBack)
    {
        // Initialize the values to a full projection region boundaries.
        float minX = -1;
        float minY = -1;
        float minZ = 0;
        float maxX = 1;
        float maxY = 1;
        float maxZ = 1;
    
        if (null != shapes)
        {
            D3dShape[] shapesArray = shapes.ToArray();
    
            if (shapesArray.Any())
            {
                D3dShape firstShape = shapesArray.First();
                GetShapeProjectionRegion(firstShape, out minX, out maxY, out minZ, out maxX,
                                            out minY, out maxZ);
    
                Parallel.ForEach(shapesArray, s =>
                                                    {
                                                        float currMinX;
                                                        float currMinY;
                                                        float currMinZ;
                                                        float currMaxX;
                                                        float currMaxY;
                                                        float currMaxZ;
    
                                                        GetShapeProjectionRegion(s, out currMinX, out currMaxY, out currMinZ, out currMaxX,
                                                                                out currMinY, out currMaxZ);
    
                                                        lock (this)
                                                        {
                                                            minX = Math.Min(minX, currMinX);
                                                            minY = Math.Min(minY, currMinY);
                                                            minZ = Math.Min(minZ, currMinZ);
                                                            maxX = Math.Max(maxX, currMaxX);
                                                            maxY = Math.Max(maxY, currMaxY);
                                                            maxZ = Math.Max(maxZ, currMaxZ);
                                                        }
                                                    });
            }
        }
    
        projectionRegionLeft = minX;
        projectionRegionTop = maxY;
        projectionRegionFront = minZ;
        projectionRegionRight = maxX;
        projectionRegionBottom = minY;
        projectionRegionBack = maxZ;
    }
    
    protected virtual void GetShapeProjectionRegion(D3dShape shape, out float projectionRegionLeft, out float projectionRegionTop,
                                    out float projectionRegionFront, out float projectionRegionRight,
                                    out float projectionRegionBottom, out float projectionRegionBack)
    {
        // Initialize the values to a full projection region boundaries.
        float minX = -1;
        float minY = -1;
        float minZ = 0;
        float maxX = 1;
        float maxY = 1;
        float maxZ = 1;
    
        if (null != shape)
        {
            BoundingBox boundingBox = shape.GetBoundingBoxBeforeTransformation();
    
            boundingBox.TransformCoordinates(shape.GetActualWorldMatrix() * ViewMatrix);
            float projectionRegionDepth = ZFarPlane - ZNearPlane;
    
            minZ = boundingBox.GetMinimalZ();
            maxZ = boundingBox.GetMaximalZ();
    
            if (CameraCoordinateSystem.RightHanded == CoordinateSystem)
            {
                minZ *= -1;
                maxZ *= -1;
            }
    
            minZ -= ZNearPlane;
            maxZ -= ZNearPlane;
    
            minZ /= projectionRegionDepth;
            maxZ /= projectionRegionDepth;
    
            boundingBox.TransformCoordinates(ProjectionMatrix);
    
            minX = boundingBox.GetMinimalX();
            minY = boundingBox.GetMinimalY();
            maxX = boundingBox.GetMaximalX();
            maxY = boundingBox.GetMaximalY();
        }
    
        projectionRegionLeft = minX;
        projectionRegionTop = maxY;
        projectionRegionFront = (CameraCoordinateSystem.RightHanded == CoordinateSystem) ? maxZ : minZ;
        projectionRegionRight = maxX;
        projectionRegionBottom = minY;
        projectionRegionBack = (CameraCoordinateSystem.RightHanded == CoordinateSystem) ? minZ : maxZ;
    }
  2. Move the camera to look a the center of the projection region and, get the updated projection region:
    public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
    {
        ...
    
        float centerX;
        float centerY;
    
        // Move the camera to look at the center of the projection region.
        centerX = (projectionRegionRight + projectionRegionLeft)/2;
        centerY = (projectionRegionBottom + projectionRegionTop)/2;
    
        // Since the projection coordinates are between -1 and 1 (and we are in  the prjection  coordinates),
        // the relative distance is equal to the actual distance...
        RelativeXyMove(centerX, centerY);
    
        // Get the shapes' projection region, after the move.
        GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
                                    out projectionRegionFront,
                                    out projectionRegionRight, out projectionRegionBottom,
                                    out projectionRegionBack);   
    }
  3. If the near plane of the updated projection region is out of the camera's view, move the camera to contain the near plane of the updated projection region and, get the updated projection region:
    public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
    {
        ...
    
        if (0 > projectionRegionFront || 1 < projectionRegionFront)
        {
            float zMove = projectionRegionFront - ((projectionRegionBack - projectionRegionFront)*0.1f);
            RelativeZMove(zMove);
    
            // Get the shapes' projection region, after the move.
            GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
                                        out projectionRegionFront,
                                        out projectionRegionRight, out projectionRegionBottom,
                                        out projectionRegionBack);
    
        }     
    }
  4. If the far plane of the updated projection region is out of the camera's view, set the camera's far plane appropriately and, get the updated projection region:
    public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
    {
        ...
    
        if (1 < projectionRegionBack)
        {
            ZFarPlane = ZNearPlane + (ZFarPlane - ZNearPlane)*(projectionRegionBack + 0.2f);
    
            // Get the shapes' projection region, after the far-plane change.
            GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
                                        out projectionRegionFront,
                                        out projectionRegionRight, out projectionRegionBottom,
                                        out projectionRegionBack);
        }
    }
  5. Zoom the camera to contain the updated projection region:
    public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
    {
        ...
    
        // Zoom to contain the projection region.
        ZoomToProjectionRegion(projectionRegionLeft, projectionRegionTop,
                                projectionRegionRight, projectionRegionBottom, keepAspectRatio);
    }

Since the dimensions of the shapes' projection region can be different for each projection, the adjustment algorithm above doesn't always give us the exact wanted result. That problem can be solved, if we repeat that algorithm a couple of times:

public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
{
    if (null == shapes)
    {
        return;
    }

    D3dShape[] shapesArray = shapes.ToArray();

    float projectionRegionLeft;
    float projectionRegionTop;
    float projectionRegionFront;
    float projectionRegionRight;
    float projectionRegionBottom;
    float projectionRegionBack;

    int moveTriesCount = 0;
    float centerX;
    float centerY;
    const float epsilon = 0.000000000001f;
    do
    {
        // Get the shapes' projection region.
        GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
                                    out projectionRegionFront,
                                    out projectionRegionRight, out projectionRegionBottom,
                                    out projectionRegionBack);

        // Move the camera to look at the center of the projection region.
        centerX = (projectionRegionRight + projectionRegionLeft)/2;
        centerY = (projectionRegionBottom + projectionRegionTop)/2;

        // Since the projection coordinates are between -1 and 1 (and we are in  the prjection  coordinates),
        // the relative distance is equal to the actual distance...
        RelativeXyMove(centerX, centerY);

        // Get the shapes' projection region, after the move.
        GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
                                    out projectionRegionFront,
                                    out projectionRegionRight, out projectionRegionBottom,
                                    out projectionRegionBack);

        if (0 > projectionRegionFront || 1 < projectionRegionFront)
        {
            float zMove = projectionRegionFront - ((projectionRegionBack - projectionRegionFront)*0.1f);
            RelativeZMove(zMove);

            // Get the shapes' projection region, after the move.
            GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
                                        out projectionRegionFront,
                                        out projectionRegionRight, out projectionRegionBottom,
                                        out projectionRegionBack);

        }

        if (1 < projectionRegionBack)
        {
            ZFarPlane = ZNearPlane + (ZFarPlane - ZNearPlane)*(projectionRegionBack + 0.2f);

            // Get the shapes' projection region, after the far-plane change.
            GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
                                        out projectionRegionFront,
                                        out projectionRegionRight, out projectionRegionBottom,
                                        out projectionRegionBack);
        }

        // Zoom to contain the projection region.
        ZoomToProjectionRegion(projectionRegionLeft, projectionRegionTop,
                                projectionRegionRight, projectionRegionBottom, keepAspectRatio);

        ++moveTriesCount;

    } while (moveTriesCount < MaxMoveTriesForAdjustmentAlgorithm &&
                (Math.Abs(0 - centerX) > epsilon || Math.Abs(0 - centerY) > epsilon));
}

public int MaxMoveTriesForAdjustmentAlgorithm { get; set; }

Present the scene

Rendering the scene

For now, we have a platform for rendering and manipulating a 3D scene. The next step is, creating a control that can present our scene on the UI.

For enabling interoperability between our scene and the WPF UI, we can use my D3dHost as the base class of our control.

public class D3dScenePresenter : D3dHost
{
}

In that control we add a property for holding a scene:

public D3dScene Scene
{
    get { return (D3dScene)GetValue(SceneProperty); }
    set { SetValue(SceneProperty, value); }
}

public static readonly DependencyProperty SceneProperty =
    DependencyProperty.Register("Scene", typeof(D3dScene), typeof(D3dScenePresenter),
    new UIPropertyMetadata(null, OnSceneChanged));

private static void  OnSceneChanged(DependencyObject o, DependencyPropertyChangedEventArgs arg)
{
    D3dScenePresenter dsp = o as D3dScenePresenter;
    if (null==dsp)
    {
        return;
    }

    // We can access a dependency-property only in the UI thread.
    // So, store the value in another property, for let accessing in other threads.
    dsp.CurrentScene = dsp.Scene;
}

protected D3dScene CurrentScene { get; private set; }

and, since we don't want to block the UI in cases of heavy scenes, we create a different thread for rendering the scene:

public void InvalidateScene()
{
    if (null == CurrentScene)
    {
        return;
    }

    // Start the thread that renders the scene, if it is needed.
    if (_renderSceneThread == null)
    {
        StartRenderSceneThread();
    }

    // Indicate that the scene render is invalid.
    _renderSceneEvent.Set();
}

#region BeginSceneUpdate & EndSceneUpdate
private readonly object _sceneUpdateLocker = new object();

public void BeginSceneUpdate()
{
    Monitor.Enter(_sceneUpdateLocker);
}

public void EndSceneUpdate()
{
    Monitor.Exit(_sceneUpdateLocker);

    // Present the scene.
    InvalidateScene();
}
#endregion

#region RenderScene
private Thread _renderSceneThread = null;
private bool _continueRenderSceneThread;
private AutoResetEvent _renderSceneEvent = new AutoResetEvent(false);

protected void RenderScene()
{
    if (null == CurrentScene)
    {
        return;
    }

    try
    {
        BeginDrawing();

        Monitor.Enter(_sceneUpdateLocker);

        // Render the scene.
        CurrentScene.Render(D3dDevice);
    }
    catch
    {               
    }
    finally
    {
        Monitor.Exit(_sceneUpdateLocker);

        EndDrawing();
    }            
}

private void StartRenderSceneThread()
{
    if (null == _renderSceneThread)
    {
        _continueRenderSceneThread = true;

        _renderSceneThread = new Thread(new ThreadStart(() =>
        {
            while (_continueRenderSceneThread)
            {
                _renderSceneEvent.WaitOne();

                if (_continueRenderSceneThread)
                {
                    RenderScene();
                }
            }
        }));

        _renderSceneThread.Start();
    }
}

private void StopRenderSceneThread()
{
    if (_renderSceneThread != null)
    {
        _continueRenderSceneThread = false;
        _renderSceneEvent.Set();
        _renderSceneThread.Join();
        _renderSceneThread = null;
    }
}
#endregion

Mouse camera operations

For enabling manipulation on our scene using the mouse, we can:

  1. Add a list for holding the available mouse-move operations:
    public enum CameraOperation
    {
        None,
        Zoom,
        UniformZoom,
        XyMove,
        ZMove,
        TargetXyRotate,
        TargetZRotate,
        CameraXyRotate,
        CameraZRotate,
        ZoomToRegion,
        UniformZoomToRegion
    }
    
    public class MouseMoveOperation
    {
        public MouseMoveOperation()
        {
            Operation = D3dScenePresenter.CameraOperation.None;
    
            LeftButtonState = MouseButtonState.Released;
            MiddleButtonState = MouseButtonState.Released;
            RightButtonState = MouseButtonState.Released;
            XButton1State = MouseButtonState.Released;
            XButton2State = MouseButtonState.Released;
    
            Modifiers = ModifierKeys.None;
        }
    
        #region Properties
    
        public D3dScenePresenter.CameraOperation Operation { get; set; }
    
        #region Mouse buttons state
        public MouseButtonState LeftButtonState { get; set; }
        public MouseButtonState MiddleButtonState { get; set; }
        public MouseButtonState RightButtonState { get; set; }
        public MouseButtonState XButton1State { get; set; }
        public MouseButtonState XButton2State { get; set; }
        #endregion
    
        public ModifierKeys Modifiers { get; set; }
    
        #endregion
    
        public bool IsCurrentStateFitting()
        {
            return IsMouseCurrentStateFitting() && IsKeyboardCurrentStateFitting();
        }
    
        public bool IsMouseCurrentStateFitting()
        {
            return Mouse.LeftButton == LeftButtonState &&
                    Mouse.MiddleButton == MiddleButtonState &&
                    Mouse.RightButton == RightButtonState &&
                    Mouse.XButton1 == XButton1State &&
                    Mouse.XButton2 == XButton2State;
        }
    
        public bool IsKeyboardCurrentStateFitting()
        {
            ModifierKeys currentModifiers = ModifierKeys.None;
    
            if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
            {
                currentModifiers |= ModifierKeys.Shift;
            }
    
            if (Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt))
            {
                currentModifiers |= ModifierKeys.Alt;
            }
    
            if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
            {
                currentModifiers |= ModifierKeys.Control;
            }
    
            if (Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin))
            {
                currentModifiers |= ModifierKeys.Windows;
            }
    
            return currentModifiers == Modifiers;
        }
    }
    
    #region MouseMoveOperations
    private List<MouseMoveOperation> _mouseMoveOperations;
    public List<MouseMoveOperation> MouseMoveOperations
    {
        get { return _mouseMoveOperations ?? (_mouseMoveOperations = new List<MouseMoveOperation>()); }
    }
    #endregion
  2. Initialize it with default mouse operations:
    private void InitActions()
    {
        // Add default mouse-move operations
        lock (MouseMoveOperations)
        {
            MouseMoveOperations.Add(new MouseMoveOperation
                                        {
                                            Operation = CameraOperation.XyMove,
                                            LeftButtonState = MouseButtonState.Pressed,
                                            Modifiers = ModifierKeys.Shift
                                        });
            MouseMoveOperations.Add(new MouseMoveOperation
                                        {
                                            Operation = CameraOperation.TargetZRotate,
                                            LeftButtonState = MouseButtonState.Pressed,
                                            Modifiers = ModifierKeys.Alt
                                        });
            MouseMoveOperations.Add(new MouseMoveOperation
                                        {
                                            Operation = CameraOperation.CameraZRotate,
                                            LeftButtonState = MouseButtonState.Pressed,
                                            Modifiers = ModifierKeys.Alt | ModifierKeys.Control
                                        });
    
            MouseMoveOperations.Add(new MouseMoveOperation
                                        {
                                            Operation = CameraOperation.ZMove,
                                            RightButtonState = MouseButtonState.Pressed,
                                            Modifiers = ModifierKeys.Shift
                                        });
            MouseMoveOperations.Add(new MouseMoveOperation
                                        {
                                            Operation = CameraOperation.TargetXyRotate,
                                            RightButtonState = MouseButtonState.Pressed,
                                            Modifiers = ModifierKeys.Alt
                                        });
            MouseMoveOperations.Add(new MouseMoveOperation
                                        {
                                            Operation = CameraOperation.CameraXyRotate,
                                            RightButtonState = MouseButtonState.Pressed,
                                            Modifiers = ModifierKeys.Alt | ModifierKeys.Control
                                        });
    
            MouseMoveOperations.Add(new MouseMoveOperation
                                        {
                                            Operation = CameraOperation.Zoom,
                                            MiddleButtonState = MouseButtonState.Pressed
                                        });
            MouseMoveOperations.Add(new MouseMoveOperation
                                        {
                                            Operation = CameraOperation.UniformZoom,
                                            MiddleButtonState = MouseButtonState.Pressed,
                                            Modifiers = ModifierKeys.Shift
                                        });
    
            MouseMoveOperations.Add(new MouseMoveOperation
                                        {
                                            Operation = CameraOperation.ZoomToRegion,
                                            RightButtonState = MouseButtonState.Pressed,
                                            LeftButtonState = MouseButtonState.Pressed
                                        });
            MouseMoveOperations.Add(new MouseMoveOperation
                                        {
                                            Operation = CameraOperation.UniformZoomToRegion,
                                            RightButtonState = MouseButtonState.Pressed,
                                            LeftButtonState = MouseButtonState.Pressed,
                                            Modifiers = ModifierKeys.Shift
                                        });
        }           
    }
  3. Implement the available operations:
    • Zoom:
      protected void PerformZoom(float relativeDeltaX, float relativeDeltaY)
      {
          CurrentScene.Camera.Zoom(1 - relativeDeltaX, 1 + relativeDeltaY);
      }
    • UniformZoom:
      protected void PerformUniformZoom(float relativeDeltaX, float relativeDeltaY)
      {
          float relativeZoomDelta = Math.Abs(relativeDeltaX) > Math.Abs(relativeDeltaY)
                                          ? relativeDeltaX
                                          : -relativeDeltaY;
      
          PerformZoom(relativeZoomDelta, -relativeZoomDelta);
      }
    • XyMove:
      protected void PerformXyMove(float relativeDeltaX, float relativeDeltaY)
      {
          CurrentScene.Camera.RelativeXyMove(-relativeDeltaX * 2, relativeDeltaY * 2);
      }
    • ZMove:
      protected void PerformZMove(float relativeDeltaX, float relativeDeltaY)
      {
          float relativeDeltaZ = Math.Abs(relativeDeltaX) > Math.Abs(relativeDeltaY)
                              ? relativeDeltaX
                              : -relativeDeltaY;
      
          CurrentScene.Camera.RelativeZMove(relativeDeltaZ);
      }
    • TargetXyRotate:
      protected void PerformTargetXyRotate(float relativeDeltaX, float relativeDeltaY)
      {
          CurrentScene.Camera.RelativeRotateX(relativeDeltaY/8, 
              CameraTransformationCenterPosition.CameraPosition, CameraRotationDirection.CounterClockwise);
          CurrentScene.Camera.RelativeRotateY(relativeDeltaX/8, 
              CameraTransformationCenterPosition.CameraPosition, CameraRotationDirection.CounterClockwise);
      }
    • TargetZRotate:
      protected void PerformTargetZRotate(float relativeDeltaX, float relativeDeltaY)
      {            
          float relativeArcLength = relativeDeltaX + relativeDeltaY;
          CurrentScene.Camera.RelativeRotateZ(relativeArcLength,
              CameraTransformationCenterPosition.CameraPosition, CameraRotationDirection.Clockwise);
      }
    • CameraXyRotate:
      protected void PerformCameraXyRotate(float relativeDeltaX, float relativeDeltaY)
      {
          CurrentScene.Camera.RelativeRotateX(relativeDeltaY, 
              CameraTransformationCenterPosition.TargetPosition, CameraRotationDirection.CounterClockwise);
          CurrentScene.Camera.RelativeRotateY(relativeDeltaX, 
              CameraTransformationCenterPosition.TargetPosition, CameraRotationDirection.CounterClockwise);
      }
    • CameraZRotate:
      protected void PerformCameraZRotate(float relativeDeltaX, float relativeDeltaY)
      {
          float relativeArcLength = relativeDeltaX + relativeDeltaY;
          CurrentScene.Camera.RelativeRotateZ(relativeArcLength, 
              CameraTransformationCenterPosition.TargetPosition, CameraRotationDirection.Clockwise);
      }
    • ZoomToRegion and UniformZoomToRegion:
      • Add an Adorner for indicating the selected region:
        protected class RegionIndicatorAdorner : Adorner
        {
            public RegionIndicatorAdorner(UIElement adornedElement)
                : base(adornedElement)
            {
                Visibility = Visibility.Collapsed;
        
                IsHitTestVisible = false;
        
                FillBrush = new SolidColorBrush(new Color
                {
                    A = 128,
                    R = 0,
                    G = 0,
                    B = 0
                });
        
                BorderPen = new Pen(new SolidColorBrush(new Color
                {
                    A = 128,
                    R = 255,
                    G = 255,
                    B = 255
                }), 2)
                {
                    DashStyle = new DashStyle(new double[] { 5, 5 }, 0)
                };
            }
        
            #region Properties
        
            public double RectangleTop { get; set; }
            public double RectangleLeft { get; set; }
            public double RectangleWidth { get; set; }
            public double RectangleHeight { get; set; }
        
            public double ActualRectangleTop { get; private set; }
            public double ActualRectangleLeft { get; private set; }
        
            #region ActualRectangleWidth
            private double _actualRectangleWidth;
            public double ActualRectangleWidth { get { return _actualRectangleWidth; } }
            #endregion
        
            #region ActualRectangleHeight
            private double _actualRectangleHeight;
            public double ActualRectangleHeight { get { return _actualRectangleHeight; } }
            #endregion
        
            public bool KeepAspectRatio { get; set; }
        
            public Brush FillBrush { get; set; }
            public Pen BorderPen { get; set; }
        
            #endregion
        
            protected override void OnRender(DrawingContext drawingContext)
            {
                _actualRectangleWidth = RectangleWidth;
                _actualRectangleHeight = RectangleHeight;
        
                if (KeepAspectRatio)
                {
                    AdjustActualSize(ref _actualRectangleWidth, ref _actualRectangleHeight);
                }
        
                ActualRectangleTop = _actualRectangleHeight > 0 ? RectangleTop : RectangleTop + _actualRectangleHeight;
                ActualRectangleLeft = _actualRectangleWidth > 0 ? RectangleLeft : RectangleLeft + _actualRectangleWidth;
        
                Width = Math.Abs(_actualRectangleWidth);
                Height = Math.Abs(_actualRectangleHeight);
        
                Rect r = new Rect(ActualRectangleLeft, ActualRectangleTop, Width, Height);
        
                drawingContext.DrawRectangle(FillBrush, BorderPen, r);
            }
        
            private void AdjustActualSize(ref double actualRectangleWidth, ref double actualRectangleHeight)
            {
                Rectangle adornedRectangle = AdornedElement as Rectangle;
                if (null == adornedRectangle)
                {
                    return;
                }
        
                double relativeWidth = Math.Abs(actualRectangleWidth / adornedRectangle.ActualWidth);
                double relativeHeight = Math.Abs(actualRectangleHeight / adornedRectangle.ActualHeight);
        
                if (relativeWidth < relativeHeight)
                {
                    actualRectangleHeight *= (relativeWidth / relativeHeight);
                }
                else
                {
                    actualRectangleWidth *= (relativeHeight / relativeWidth);
                }
            }
        }
        
        protected RegionIndicatorAdorner _regionIndicatorAdorner;
      • Handle the operation using that Adorner:
        protected bool _isInZoomToRegionMode = false;
        
        protected void HandleZoomToRegion()
        {
            // Get the current ZoomToRegion operation state
            CameraOperation zoomToRegionOperation = CameraOperation.None;
        
            lock (MouseMoveOperations)
            {
                List<CameraOperation> cameraOperations =
                    MouseMoveOperations.Where(o => (CameraOperation.ZoomToRegion == o.Operation || 
                                                    CameraOperation.UniformZoomToRegion == o.Operation)
                        && o.IsCurrentStateFitting()).
                    Select(o => o.Operation).Distinct().ToList();
        
                if (cameraOperations.Any())
                {
                    zoomToRegionOperation = cameraOperations.First();
                }
            }
        
            if (CameraOperation.None == zoomToRegionOperation)
            {
                if (_isInZoomToRegionMode)
                {
                    // The region's selection operation has just ended...
                    ApplyZoomToRegion();
                }
            }
            else
            {
                // We are in ZoomToRegion state...
        
                if (!_isInZoomToRegionMode)
                {
                    // The region's selection operation has just started...
                    StartZoomToRegion();
                }
                else
                {
                    // We are in a middle of a region's selection operation...
        
                    if (null != _regionIndicatorAdorner)
                    {
                        _regionIndicatorAdorner.KeepAspectRatio = CameraOperation.UniformZoomToRegion ==
                                                                zoomToRegionOperation;
                    }
                }
            }
        }
        
        private void StartZoomToRegion()
        {
            if (null == _regionIndicatorAdorner)
            {
                return;
            }
        
            _regionIndicatorAdorner.RectangleTop = _lastSurfaceMousePosition.Y *
                                                    (D3dRegionActualHeight / D3dSurfaceHeight);
            _regionIndicatorAdorner.RectangleLeft = _lastSurfaceMousePosition.X *
                                                    (D3dRegionActualWidth / D3dSurfaceWidth);
            _regionIndicatorAdorner.RectangleWidth = 0;
            _regionIndicatorAdorner.RectangleHeight = 0;
        
            _regionIndicatorAdorner.Visibility = Visibility.Visible;
            _regionIndicatorAdorner.InvalidateVisual();
        
            _isInZoomToRegionMode = true;
        }
        
        private void ApplyZoomToRegion()
        {
            if (null == _regionIndicatorAdorner)
            {
                return;
            }
        
            if (Math.Abs(_regionIndicatorAdorner.ActualRectangleHeight) > 0 &&
                Math.Abs(_regionIndicatorAdorner.ActualRectangleWidth) > 0)
            {
                float selectedRegionLeft =
                    (float) (_regionIndicatorAdorner.ActualRectangleLeft/(D3dRegionActualWidth/2) - 1);
                float selectedRegionTop =
                    (float) (1 - _regionIndicatorAdorner.ActualRectangleTop/(D3dRegionActualHeight/2));
                float selectedRegionRight =
                    (float) (selectedRegionLeft + _regionIndicatorAdorner.Width/(D3dRegionActualWidth/2));
                float selectedRegionBottom =
                    (float) (selectedRegionTop - _regionIndicatorAdorner.Height/(D3dRegionActualHeight/2));
        
                PerformSceneAction(() => CurrentScene.Camera.ZoomToProjectionRegion(
                    selectedRegionLeft, selectedRegionTop, selectedRegionRight,
                    selectedRegionBottom, false));
            }
        
            _regionIndicatorAdorner.Visibility = Visibility.Collapsed;
            _regionIndicatorAdorner.InvalidateVisual();
        
            _isInZoomToRegionMode = false;
        }
  4. Handle the D3dSurfaceMouseMove event to perform the appropriate operation:
    private void InitActions()
    {
        D3dSurfaceMouseMove += OnD3dSurfaceMouseMove;
    
        ...
    }
    
    private void OnD3dSurfaceMouseMove(object sender, D3dSurfaceMouseEventArgs e)
    {
        HandleZoomToRegion();
    
        List<CameraOperation> cameraOperations;
    
        lock (MouseMoveOperations)
        {
            cameraOperations =
                MouseMoveOperations.Where(o => o.IsCurrentStateFitting()).Select(o => o.Operation).Distinct().ToList();
        }
    
        if (cameraOperations.Any())
        {
            Point currentSurfacePosition = e.D3dSurfaceMousePosition;
            Vector delta = currentSurfacePosition - _lastSurfaceMousePosition;
            float relativeDeltaX = (float)(delta.X / D3dSurfaceWidth);
            float relativeDeltaY = (float)(delta.Y / D3dSurfaceHeight);
    
            foreach (CameraOperation operation in cameraOperations)
            {
                PerformOperation(operation, relativeDeltaX, relativeDeltaY);
            }
    
            if (_isInZoomToRegionMode && null != _regionIndicatorAdorner)
            {
                _regionIndicatorAdorner.RectangleWidth += delta.X * (D3dRegionActualWidth / D3dSurfaceWidth);
                _regionIndicatorAdorner.RectangleHeight += delta.Y * (D3dRegionActualHeight / D3dSurfaceHeight);
                _regionIndicatorAdorner.InvalidateVisual();
            }
        }
    
        _lastSurfaceMousePosition = e.D3dSurfaceMousePosition;
    }
    
    protected List<Action> _pendingActions = new List<Action>();
    
    private void PerformOperation(CameraOperation operation, float relativeDeltaX, float relativeDeltaY)
    {
        switch (operation)
        {
            case CameraOperation.Zoom:
                PerformSceneAction(() => PerformZoom(relativeDeltaX, relativeDeltaY));
                break;
            case CameraOperation.UniformZoom:
                PerformSceneAction(() => PerformUniformZoom(relativeDeltaX, relativeDeltaY));
                break;
            case CameraOperation.XyMove:
                PerformSceneAction(() => PerformXyMove(relativeDeltaX, relativeDeltaY));
                break;
            case CameraOperation.ZMove:
                PerformSceneAction(() => PerformZMove(relativeDeltaX, relativeDeltaY));
                break;
            case CameraOperation.TargetXyRotate:
                PerformSceneAction(() => PerformTargetXyRotate(relativeDeltaX, relativeDeltaY));
                break;
            case CameraOperation.TargetZRotate:
                AdjustZRotateDelta(ref relativeDeltaX, ref relativeDeltaY);
                PerformSceneAction(() => PerformTargetZRotate(relativeDeltaX, relativeDeltaY));
                break;
            case CameraOperation.CameraXyRotate:
                PerformSceneAction(() => PerformCameraXyRotate(relativeDeltaX, relativeDeltaY));
                break;
            case CameraOperation.CameraZRotate:
                AdjustZRotateDelta(ref relativeDeltaX, ref relativeDeltaY);
                PerformSceneAction(() => PerformCameraZRotate(relativeDeltaX, relativeDeltaY));
                break;
            case CameraOperation.ZoomToRegion:
            case CameraOperation.UniformZoomToRegion:
                HandleZoomToRegion();
                break;
        }
    }
    
    public void PerformSceneAction(Action a)
    {
        AddPendingAction(a);
        InvalidateScene();
    }
    
    public void AddPendingAction(Action a)
    {
        if (null == a)
        {
            return;
        }
    
        lock (_pendingActions)
        {
            _pendingActions.Add(a);
        }
    }
    
    private void AdjustZRotateDelta(ref float relativeDeltaX, ref float relativeDeltaY)
    {
        relativeDeltaX /= (D3dSurfaceHeight / 2) > _lastSurfaceMousePosition.Y ? 4 : -4;
        relativeDeltaY /= (D3dSurfaceWidth / 2) < _lastSurfaceMousePosition.X ? 4 : -4;
    }
    
    protected void RenderScene()
    {
        ...
     
        List<Action> currentActions = new List<Action>();
                
        lock (_pendingActions)
        {
            currentActions.AddRange(_pendingActions);
            _pendingActions.Clear();
        }
    
        try
        {
            ...
    
            // Perform scene actions.
            currentActions.ForEach(a => a());
    
            // Render the scene.
            CurrentScene.Render(D3dDevice);
        }
        catch
        {               
        }
        finally
        {
            ...
        }            
    }

Commands

For enabling command-bindings for manipulating the scene, we add a RoutedCommand for each operation. For example, here is the RoutedCommand for the UniformZoom operation:

#region UniformZoomCommand

private static RoutedCommand _uniformZoomCommand;
public static RoutedCommand UniformZoomCommand
{
    get
    {
        return _uniformZoomCommand ??
                (_uniformZoomCommand =
                new RoutedCommand("UniformZoom", typeof(D3dScenePresenter)));
    }
}

protected static void CanExecuteUniformZoomCommand(object sender, CanExecuteRoutedEventArgs e)
{
    D3dScenePresenter dsp = sender as D3dScenePresenter;
    if (null == dsp)
    {
        return;
    }

    e.CanExecute = true;
}

protected static void ExecuteUniformZoomCommand(object sender, ExecutedRoutedEventArgs e)
{
    D3dScenePresenter dsp = sender as D3dScenePresenter;
    if (null == dsp)
    {
        return;
    }

    float scalingFactor = 1;
    float.TryParse(e.Parameter.ToString(), out scalingFactor);
    dsp.UniformZoom(scalingFactor);
}

protected void UniformZoom(float scalingFactor)
{
    PerformSceneAction(() =>
    {
        if (null != CurrentScene)
        {
            CurrentScene.Camera.Zoom(scalingFactor, scalingFactor);
        }
    });
}

#endregion

static D3dScenePresenter()
{
    ...

    CommandBinding uniformZoomCommandBinding =
        new CommandBinding(UniformZoomCommand, ExecuteUniformZoomCommand, CanExecuteUniformZoomCommand);
    CommandManager.RegisterClassCommandBinding(typeof(D3dScenePresenter), uniformZoomCommandBinding);

    ...
}

For enabling command-binding for adjusting our camera view, we add another RoutedCommand that performs the AdjustCameraView operation on our scene:

#region AdjustCameraViewCommand

private static RoutedCommand _adjustCameraViewCommand;
public static RoutedCommand AdjustCameraViewCommand
{
    get
    {
        return _adjustCameraViewCommand ??
                (_adjustCameraViewCommand =
                new RoutedCommand("AdjustCameraView", typeof(D3dScenePresenter)));
    }
}

protected static void CanExecuteAdjustCameraViewCommand(object sender, CanExecuteRoutedEventArgs e)
{
    D3dScenePresenter dsp = sender as D3dScenePresenter;
    if (null == dsp)
    {
        return;
    }

    e.CanExecute = true;
}

protected static void ExecuteAdjustCameraViewCommand(object sender, ExecutedRoutedEventArgs e)
{
    D3dScenePresenter dsp = sender as D3dScenePresenter;
    if (null == dsp)
    {
        return;
    }

    dsp.AdjustCameraView();
}

protected void AdjustCameraView()
{
    PerformSceneAction(() =>
                            {
                                if (null != CurrentScene)
                                {
                                    CurrentScene.AdjustCameraView();
                                }
                            });
}

#endregion

static D3dScenePresenter()
{
    CommandBinding adjustCameraViewBinding =
        new CommandBinding(AdjustCameraViewCommand, ExecuteAdjustCameraViewCommand,
                            CanExecuteAdjustCameraViewCommand);
    CommandManager.RegisterClassCommandBinding(typeof (D3dScenePresenter), adjustCameraViewBinding);

    ...
}

Picking

We can also use the picking ability of our scene, for notifying the current mouse-over shape. That can be done by:

  1. Adding a RoutedEvent for raising when the current mouse-over shape has been changed:
    public class MouseOverShapeChangedRoutedEventArgs : RoutedEventArgs
    {
        #region Constructors
        public MouseOverShapeChangedRoutedEventArgs()
        {
        }
    
        public MouseOverShapeChangedRoutedEventArgs(RoutedEvent routedEvent)
            : base(routedEvent)
        {           
        }
    
        public MouseOverShapeChangedRoutedEventArgs(RoutedEvent routedEvent, object source)
            : base(routedEvent, source)
        {            
        }
        #endregion
    
        public D3dShape OldShape { get; set; }
        public D3dShape NewShape { get; set; }
    }
    
    public delegate void MouseOverShapeChangedRoutedEventHandler(object sender, MouseOverShapeChangedRoutedEventArgs e);
    
    #region MouseOverShapeChanged
    
    public static readonly RoutedEvent MouseOverShapeChangedEvent = EventManager.RegisterRoutedEvent(
        "MouseOverShapeChanged", RoutingStrategy.Bubble, typeof (MouseOverShapeChangedRoutedEventHandler),
        typeof (D3dScenePresenter));
    
    public event MouseOverShapeChangedRoutedEventHandler MouseOverShapeChanged
    {
        add { AddHandler(MouseOverShapeChangedEvent, value); }
        remove { RemoveHandler(MouseOverShapeChangedEvent, value); }
    }
    
    #endregion
  2. Adding a property for holding the current mouse-over shape:
    private D3dShape _currentMouseOverShape;
    public D3dShape CurrentMouseOverShape
    {
        get { return _currentMouseOverShape; }
        protected set
        {
            if (value != _currentMouseOverShape)
            {
                D3dShape oldValue = _currentMouseOverShape;
                _currentMouseOverShape = value;
    
                // Raise MouseOverShapeChanged event
                Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                                        new ThreadStart(() =>
                                                            {
                                                                MouseOverShapeChangedRoutedEventArgs arg =
                                                                    new MouseOverShapeChangedRoutedEventArgs(
                                                                        D3dScenePresenter.MouseOverShapeChangedEvent)
                                                                        {
                                                                            OldShape = oldValue,
                                                                            NewShape = _currentMouseOverShape
                                                                        };
    
                                                                RaiseEvent(arg);
                                                            }));
            }
        }
    }
  3. Creating a thread for picking the shape that is under the mouse cursor:
    private Thread _pickThread = null;
    private bool _continuePickThread;
    private AutoResetEvent _pickEvent = new AutoResetEvent(false);
    
    protected void UpdateCurrentMouseOverShape()
    {
        if (null == CurrentScene)
        {
            return;
        }
    
        Monitor.Enter(_sceneUpdateLocker);
    
        try
        {
            D3dShape currShape = (0 <= _lastSurfaceMousePosition.X && 0 <= _lastSurfaceMousePosition.Y)
                                        ? Pick(_lastSurfaceMousePosition.X, _lastSurfaceMousePosition.Y)
                                        : null;
    
            CurrentMouseOverShape = currShape;
        }
        catch
        {
        }
        finally
        {
            Monitor.Exit(_sceneUpdateLocker);
        }
    }
    
    private void StartPickThread()
    {
        if (null == _pickThread)
        {
            _continuePickThread = true;
    
            _pickThread = new Thread(new ThreadStart(() =>
            {
                while (_continuePickThread)
                {
                    _pickEvent.WaitOne();
    
                    if (_continuePickThread)
                    {
                        UpdateCurrentMouseOverShape();
                    }
                }
            }));
    
            _pickThread.Start();
        }
    }
    
    private void StopPickThread()
    {
        if (_pickThread != null)
        {
            _continuePickThread = false;
            _pickEvent.Set();
            _pickThread.Join();
            _pickThread = null;
        }
    }
    
    public D3dShape Pick(double surfaceX, double surfaceY)
    {
        if (null == CurrentScene)
        {
            return null;
        }
    
        return CurrentScene.Pick(D3dDevice, (float) surfaceX, (float) surfaceY);
    }
    
    public void InvalidateCurrentMouseOverShape()
    {
        if (null == CurrentScene)
        {
            return;
        }
    
        // Start the thread that renders the scene, if it is needed.
        if (_pickThread == null)
        {
            StartPickThread();
        }
    
        // Indicate that the scene render is invalid.
        _pickEvent.Set();
    }
  4. Picking the current mouse-over shape, for each mouse-move or, a render of the scene:
    private void OnD3dSurfaceMouseMove(object sender, D3dSurfaceMouseEventArgs e)
    {
        ...
    
        if (cameraOperations.Any())
        {
            ...
        }
        else
        {
            // Pick the mouse-over shape, if needed.
            if (_isMouseOverShapeTestEnabled)
            {
                InvalidateCurrentMouseOverShape();
            }
        }
    
        ...
    }
    
    protected void RenderScene()
    {
        ...
     
        try
        {
            ...
    
            // Render the scene.
            CurrentScene.Render(D3dDevice);
    
            // Pick the mouse-over shape, if needed.
            if (_isMouseOverShapeTestEnabled)
            {
                InvalidateCurrentMouseOverShape();
            }
        }
        catch
        {               
        }
        finally
        {
            ...
        }            
    }
    
    #region IsMouseOverShapeTestEnabled
    
    public bool IsMouseOverShapeTestEnabled
    {
        get { return (bool)GetValue(IsMouseOverShapeTestEnabledProperty); }
        set { SetValue(IsMouseOverShapeTestEnabledProperty, value); }
    }
    
    public static readonly DependencyProperty IsMouseOverShapeTestEnabledProperty =
        DependencyProperty.Register("IsMouseOverShapeTestEnabled", typeof(bool), typeof(D3dScenePresenter),
        new UIPropertyMetadata(false, OnIsMouseOverShapeTestEnabledChanged));
    
    private static void OnIsMouseOverShapeTestEnabledChanged(DependencyObject o, DependencyPropertyChangedEventArgs arg)
    {
        D3dScenePresenter dsp = o as D3dScenePresenter;
        if (null == dsp)
        {
            return;
        }
    
        // We can access a dependency-property only in the UI thread.
        // So, store the value in another property, for let accessing in other threads.
        dsp._isMouseOverShapeTestEnabled = dsp.IsMouseOverShapeTestEnabled;
    }
    
    private bool _isMouseOverShapeTestEnabled;
    
    #endregion

How to use it

Build and manipulate a scene

Create shapes

For demonstrating the use of the D3dScenePresenter control for presenting and manipulating a scene, we create a window that presents some shapes and enables scene's manipulations with those shapes.

For creating a single shape, we can create a class that derives from D3dSingleShape:

public class Pyramid : D3dSingleShape
{
}

and implement the InitDrawing and the Draw methods of it:

  • For initializing the pyramid vertices' buffer we:
    1. Create the vertices' positions (in the scale of -0.5..0.5 for each axis):
      Vector3 topPosition = new Vector3(0, 0.5f, 0);
      Vector3 frontBottomLeftPosition = new Vector3(-0.5f, -0.5f, 0.5f);
      Vector3 frontBottomRightPosition = new Vector3(0.5f, -0.5f, 0.5f);
      Vector3 backBottomLeftPosition = new Vector3(-0.5f, -0.5f, -0.5f);
      Vector3 backBottomRightPosition = new Vector3(0.5f, -0.5f, -0.5f);
    2. Calculate the normals for each pyramid's side:
    3. pyramid side normal

      =>

      float yNormal = (float) Math.Sqrt(0.2);
      float sideDirectionNormal = (float) Math.Sqrt(0.8);
      Vector3 frontNormal = new Vector3(0, yNormal, sideDirectionNormal);
      Vector3 backNormal = new Vector3(0, yNormal, -sideDirectionNormal);
      Vector3 leftNormal = new Vector3(-sideDirectionNormal, yNormal, 0);
      Vector3 rightNormal = new Vector3(sideDirectionNormal, yNormal, 0);
      Vector3 downNormal = new Vector3(0, -1, 0);
  • Create the pyramid's vertices using those positions and normals:
    private const int _verticesNumber = 18; // ((4 sides ) + ((1 base) * (2 triangles))) * (3 vertices)
    private static CustomVertex.PositionNormal[] _pyramidVertices = null;
    
    protected override void InitDrawing()
    {
        if (null != _pyramidVertices)
        {
            // The vertices are already initiated.
            return;
        }
    
        lock (_shapeLoaderLock)
        {
            if (null == _pyramidVertices)
            {
                // Positions
                Vector3 topPosition = new Vector3(0, 0.5f, 0);
                Vector3 frontBottomLeftPosition = new Vector3(-0.5f, -0.5f, 0.5f);
                Vector3 frontBottomRightPosition = new Vector3(0.5f, -0.5f, 0.5f);
                Vector3 backBottomLeftPosition = new Vector3(-0.5f, -0.5f, -0.5f);
                Vector3 backBottomRightPosition = new Vector3(0.5f, -0.5f, -0.5f);
    
                // Normals
                float yNormal = (float) Math.Sqrt(0.2);
                float sideDirectionNormal = (float) Math.Sqrt(0.8);
                Vector3 frontNormal = new Vector3(0, yNormal, sideDirectionNormal);
                Vector3 backNormal = new Vector3(0, yNormal, -sideDirectionNormal);
                Vector3 leftNormal = new Vector3(-sideDirectionNormal, yNormal, 0);
                Vector3 rightNormal = new Vector3(sideDirectionNormal, yNormal, 0);
                Vector3 downNormal = new Vector3(0, -1, 0);
    
                // Vertices
                CustomVertex.PositionNormal[] vertices = new CustomVertex.PositionNormal[_verticesNumber];
    
                // Front
                SetPositionNormalTriangle(vertices, 0, frontBottomRightPosition, topPosition,
                                            frontBottomLeftPosition, frontNormal);
    
                // Left
                SetPositionNormalTriangle(vertices, 3, frontBottomLeftPosition, topPosition, backBottomLeftPosition,
                                            leftNormal);
    
                // Right
                SetPositionNormalTriangle(vertices, 6, backBottomRightPosition, topPosition,
                                            frontBottomRightPosition, rightNormal);
    
                // Back
                SetPositionNormalTriangle(vertices, 9, backBottomLeftPosition, topPosition, backBottomRightPosition,
                                            backNormal);
    
                // Down
                SetPositionNormalRectangle(vertices, 12, frontBottomRightPosition, frontBottomLeftPosition,
                                            backBottomLeftPosition, backBottomRightPosition, downNormal);
    
                _pyramidVertices = vertices;
            }
        }
    }
    
    private void SetPositionNormalTriangle(CustomVertex.PositionNormal[] verticesArray, int startIndex,
        Vector3 firstPosition, Vector3 secondPosition, Vector3 thirdPosition, Vector3 normal)
    {
        verticesArray[startIndex].Position = firstPosition;
        verticesArray[startIndex].Normal = normal;
        verticesArray[startIndex + 1].Position = secondPosition;
        verticesArray[startIndex + 1].Normal = normal;
        verticesArray[startIndex + 2].Position = thirdPosition;
        verticesArray[startIndex + 2].Normal = normal;
    }
    
    private void SetPositionNormalRectangle(CustomVertex.PositionNormal[] verticesArray, int startIndex,
        Vector3 firstPosition, Vector3 secondPosition, Vector3 thirdPosition, Vector3 fourthPosition, Vector3 normal)
    {
        SetPositionNormalTriangle(verticesArray, startIndex, firstPosition, secondPosition, thirdPosition, normal);
        SetPositionNormalTriangle(verticesArray, startIndex + 3, thirdPosition, fourthPosition, firstPosition,
                                    normal);
    }
  • For drawing the pyramid, we just draw the created vertices:
    protected override void Draw(Device d3dDevice)
    {
        if (null == _pyramidVertices)
        {
            return;
        }
    
        d3dDevice.VertexFormat = CustomVertex.PositionNormal.Format;
        d3dDevice.DrawUserPrimitives(PrimitiveType.TriangleList, _verticesNumber / 3, _pyramidVertices);
    }

For enabling picking on our shape, we have to implement the GetTrianglesPointsPositions method too:

private const int _trianglesNumber = 6; // (4 sides ) + ((1 base) * (2 triangles))
private static TrianglePointsPositions[] _pyramidTriangles = null;

protected override IEnumerable<TrianglePointsPositions> GetTrianglesPointsPositions()
{
    if (null != _pyramidTriangles)
    {
        // The triangles are already initiated.
        return _pyramidTriangles;
    }

    if (null == _pyramidVertices)
    {
        return null;
    }

    lock (_shapeLoaderLock)
    {
        if (null == _pyramidTriangles)
        {
            TrianglePointsPositions[] triangles = new TrianglePointsPositions[_trianglesNumber];

            for (int triangleInx = 0; triangleInx < _trianglesNumber; triangleInx++)
            {
                triangles[triangleInx] = new TrianglePointsPositions
                {
                    Position1 = _pyramidVertices[triangleInx * 3].Position,
                    Position2 = _pyramidVertices[(triangleInx * 3) + 1].Position,
                    Position3 = _pyramidVertices[(triangleInx * 3) + 2].Position
                };
            }

            _pyramidTriangles = triangles;
        }
    }

    return _pyramidTriangles;
}

For creating a composed shape, we can:

  1. Create a class the derives from D3dShape.
  2. Add a D3dComposedShape data-member for holding the collection of the shapes.
  3. Implement the Render, AddPointIntersections and the AddRayIntersections methods, to use the methods of the D3dComposedShape.

For demonstrating it, we add two more shapes:

  • House:
    public class House : D3dShape
    {
        private D3dComposedShape _composedHouse;
        private D3dBox _houseBody;
        private Pyramid _houseRoof;
    
        public House()
        {
            BodyMaterial = DefaultMaterial;
            RoofMaterial = DefaultMaterial;
    
            _composedHouse = new D3dComposedShape();
    
            _houseBody = new D3dBox
            {
                ScalingY = 0.5f,
                TranslationY = -0.25f,
                IsPickable = false
            };
            _composedHouse.Shapes.Add(_houseBody);
    
            _houseRoof = new Pyramid
            {
                ScalingY = 0.5f,
                TranslationY = 0.25f,
                IsPickable = false
            };
            _composedHouse.Shapes.Add(_houseRoof);
        }
    
        #region D3dShape implementation
    
        public override void Render(Device d3dDevice)
        {
            _composedHouse.IsVisible = IsVisible;
    
            _houseBody.DefaultMaterial = BodyMaterial;
            _houseRoof.DefaultMaterial = RoofMaterial;
    
            _composedHouse.Parent = this;
            _composedHouse.EnvironmentData = EnvironmentData;
    
            _composedHouse.Render(d3dDevice);
        }
    
        public override void AddPointIntersections(Device d3dDevice, float surfaceX, 
               float surfaceY, List<IntersectResult> targetIntersectionsList)
        {
            _composedHouse.IsHitTestVisible = IsHitTestVisible;
            _composedHouse.AddPointIntersections(d3dDevice, surfaceX, surfaceY, targetIntersectionsList);
        }
    
        public override void AddRayIntersections(Vector3 rayOrigin, Vector3 rayDirection, 
               List<IntersectResult> targetIntersectionsList)
        {
            _composedHouse.IsHitTestVisible = IsHitTestVisible;
            _composedHouse.AddRayIntersections(rayOrigin, rayDirection, targetIntersectionsList);
        }
    
        #endregion
    
        #region Properties
    
        public Material BodyMaterial { get; set; }
        public Material RoofMaterial { get; set; }
    
        #endregion
    }
  • Star:
    public class Star : D3dShape
    {
        protected D3dComposedShape _composedStar;
    
        public Star()
        {
            _composedStar = new D3dComposedShape
            {
                RenderShapesParallelly = true
            };
    
            InitStar();
        }
    
        private void InitStar()
        {
            float pyramidBaseScaling = 0.2f;
            float pyramidHeightScaling = (1 - pyramidBaseScaling) / 2;
            float pyramidTranslateDistance = 0.5f - pyramidHeightScaling / 2;
    
            Pyramid upPyramid = new Pyramid
            {
                ScalingX = pyramidBaseScaling,
                ScalingY = pyramidHeightScaling,
                ScalingZ = pyramidBaseScaling,
                TranslationY = pyramidTranslateDistance,
                IsPickable = false
            };
            _composedStar.Shapes.Add(upPyramid);
    
            Pyramid downPyramid = new Pyramid
            {
                ScalingX = pyramidBaseScaling,
                ScalingY = pyramidHeightScaling,
                ScalingZ = pyramidBaseScaling,
                RotationZ = (float)Math.PI,
                TranslationY = -pyramidTranslateDistance,
                IsPickable = false
            };
            _composedStar.Shapes.Add(downPyramid);
    
            Pyramid rightPyramid = new Pyramid
            {
                ScalingX = pyramidBaseScaling,
                ScalingY = pyramidHeightScaling,
                ScalingZ = pyramidBaseScaling,
                RotationZ = -(float)Math.PI / 2,
                TranslationX = pyramidTranslateDistance,
                IsPickable = false
            };
            _composedStar.Shapes.Add(rightPyramid);
    
            Pyramid leftPyramid = new Pyramid
            {
                ScalingX = pyramidBaseScaling,
                ScalingY = pyramidHeightScaling,
                ScalingZ = pyramidBaseScaling,
                RotationZ = (float)Math.PI / 2,
                TranslationX = -pyramidTranslateDistance,
                IsPickable = false
            };
            _composedStar.Shapes.Add(leftPyramid);
    
            Pyramid frontPyramid = new Pyramid
            {
                ScalingX = pyramidBaseScaling,
                ScalingY = pyramidHeightScaling,
                ScalingZ = pyramidBaseScaling,
                RotationX = (float)Math.PI / 2,
                TranslationZ = pyramidTranslateDistance,
                IsPickable = false
            };
            _composedStar.Shapes.Add(frontPyramid);
    
            Pyramid backPyramid = new Pyramid
            {
                ScalingX = pyramidBaseScaling,
                ScalingY = pyramidHeightScaling,
                ScalingZ = pyramidBaseScaling,
                RotationX = -(float)Math.PI / 2,
                TranslationZ = -pyramidTranslateDistance,
                IsPickable = false
            };
            _composedStar.Shapes.Add(backPyramid);
        }
    
        #region D3dShape implementation
    
        public override void Render(Device d3dDevice)
        {
            _composedStar.IsVisible = IsVisible;
            _composedStar.DefaultMaterial = DefaultMaterial;
            _composedStar.Shapes.ForEach(s => s.DefaultMaterial = DefaultMaterial);
    
            _composedStar.RenderAlsoIfOutOfView = RenderAlsoIfOutOfView;
            _composedStar.Parent = this;
            _composedStar.EnvironmentData = EnvironmentData;
    
            _composedStar.Render(d3dDevice);
        }
    
        public override void AddPointIntersections(Device d3dDevice, float surfaceX, 
               float surfaceY, List<IntersectResult> targetIntersectionsList)
        {
            _composedStar.IsHitTestVisible = IsHitTestVisible;
            _composedStar.AddPointIntersections(d3dDevice, surfaceX, surfaceY, targetIntersectionsList);
        }
    
        public override void AddRayIntersections(Vector3 rayOrigin, Vector3 rayDirection, 
               List<IntersectResult> targetIntersectionsList)
        {
            _composedStar.IsHitTestVisible = IsHitTestVisible;
            _composedStar.AddRayIntersections(rayOrigin, rayDirection, targetIntersectionsList);
        }
    
        #endregion
    }

Build the scene

For presenting our shapes, we:

  1. Add a D3dScenePresenter:
    <Border BorderBrush="DarkGreen" BorderThickness="2" Margin="10">
        <MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
                                            D3dSurfaceWidth="2000"
                                            D3dSurfaceHeight="2000" />
    </Border>
  2. Create the scene using the shapes:
    private D3dScene _scene;
    
    private void InitScene()
    {
        _scene = new D3dScene
                        {
                            ClearColor = System.Drawing.Color.Gray
                        };
    
        // Set the light
        _scene.Lights.Add(new D3dDirectionalLight
                                {
                                    XDirection = 1,
                                    YDirection = -1,
                                    ZDirection = -1,
                                    Diffuse = System.Drawing.Color.DimGray
                                });
    
        _scene.Lights.Add(new D3dDirectionalLight
        {
            XDirection = -1,
            YDirection = 1,
            ZDirection = 1,
            Diffuse = System.Drawing.Color.Gray
        });
    
        // Add shapes.
        _scene.Shapes.Add(new Pyramid
                                {
                                    DefaultMaterial = new Material {Diffuse = System.Drawing.Color.Red},
                                    ScalingX = 200,
                                    ScalingY = 200,
                                    ScalingZ = 200
                                });
    
        _scene.Shapes.Add(new Star
                                {
                                    DefaultMaterial = new Material {Diffuse = System.Drawing.Color.Yellow},
                                    ScalingX = 300,
                                    ScalingY = 300,
                                    ScalingZ = 300,
                                    TranslationX = 600
                                });
    
        _scene.Shapes.Add(new House
                                {
                                    BodyMaterial = new Material {Diffuse = System.Drawing.Color.Green},
                                    RoofMaterial = new Material {Diffuse = System.Drawing.Color.Red},
                                    ScalingX = 150,
                                    ScalingZ = 150,
                                    ScalingY = 300,
                                    TranslationX = -600
                                });
    }
  3. Set the scene of the D3dScenePresenter, to the created scene:
    private void InitScene()
    {
        ...
    
        mdxSceneHost.Scene = _scene;
    }

Manipulate the scene

Using mouse operations

For manipulating our scene using mouse operations, we can use the default mouse-operations' settings as described in the following table:

Operation Mouse and Keyboard combination
ZoomMouse middle button
UniformZoomShift + Mouse middle button
XyMoveShift + Mouse left button
ZMoveShift + Mouse right button
TargetXyRotateAlt + Mouse right button
TargetZRotateAlt + Mouse left button
CameraXyRotateControl + Alt + Mouse right button
CameraZRotateControl + Alt + Mouse left button
ZoomToRegionMouse left button + Mouse right button
UniformZoomToRegionShift + Mouse left button + Mouse right button

We can change those default settings by changing the MouseMoveOperations list of the D3dScenePresenter control.

Using manipulation buttons

For manipulating our scene using UI buttons, we can add buttons and bind the Command of each button to the appropriate RoutedCommand of the D3dScenePresenter control. For example, here are the buttons for the UniformZoomCommand command:

<TextBlock Text="Uniform Zoom: " Grid.Row="2"
            HorizontalAlignment="Left"
            VerticalAlignment="Center" />
<RepeatButton Content="-" Grid.Column="1" Grid.Row="2" Margin="5"
    Command="{x:Static MdxSceneControls:D3dScenePresenter.UniformZoomCommand}"
    CommandParameter="1.1"
    CommandTarget="{Binding ElementName=mdxSceneHost}"/>
<RepeatButton Content="+" Grid.Column="2" Grid.Row="2" Margin="5"
    Command="{x:Static MdxSceneControls:D3dScenePresenter.UniformZoomCommand}"
    CommandParameter="0.9"
    CommandTarget="{Binding ElementName=mdxSceneHost}"/>

Zoom using the mouse-wheel

For enabling zooming using the mouse-wheel, we can add an event-handler to the D3dSurfaceMouseWheel event:

<MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
                                    D3dSurfaceWidth="2000"
                                    D3dSurfaceHeight="2000"
                                    D3dSurfaceMouseWheel="mdxSceneHost_D3dSurfaceMouseWheel" />

and, implement it to use the UniformZoomCommand command:

private void mdxSceneHost_D3dSurfaceMouseWheel(object sender, D3dSurfaceMouseWheelEventArgs e)
{
    float relativeZoomDelta = e.MouseWheelEventArgs.Delta/1000f;
    float scalingFactor = 1 - relativeZoomDelta;
    D3dScenePresenter.UniformZoomCommand.Execute(scalingFactor, mdxSceneHost);
}

Implement a mouse-over effect

For implementing a mouse-over effect, we can:

  1. Create a shape for marking the current mouse-over shape:
    public class MarkBox : D3dBox
    {
        public MarkBox()
        {
            DefaultMaterial = new Material { DiffuseColor = new ColorValue(255, 255, 255, 0.5f) };
    
            IsHitTestVisible = false;
            IsPickable = false;
            IsVisible = false;
        }
    
        #region MarkedShape
    
        private D3dShape _markedShape;
        public D3dShape MarkedShape
        {
            get { return _markedShape; }
            set
            {
                _markedShape = value;
                IsVisible = (null != _markedShape);
            }
        }
    
        #endregion
    
        public override Matrix GetActualWorldMatrix()
        {
            if (null == MarkedShape)
            {
                return base.GetActualWorldMatrix();
            }
    
            return Matrix.Scaling(1.1f, 1.1f, 1.1f) * MarkedShape.GetActualWorldMatrix();
        }
    
        protected override void Draw(Device d3dDevice)
        {
            // Store the original values.
            Blend orgSourceBlend = d3dDevice.RenderState.SourceBlend;
            Blend orgDestinationBlend = d3dDevice.RenderState.DestinationBlend;
            bool orgAlphaBlendEnable = d3dDevice.RenderState.AlphaBlendEnable;
    
            // Enable alpha blending.
            d3dDevice.RenderState.SourceBlend = Blend.SourceAlpha;
            d3dDevice.RenderState.DestinationBlend = Blend.InvSourceAlpha;
            d3dDevice.RenderState.AlphaBlendEnable = true;
    
            // Draw the box.
            base.Draw(d3dDevice);
    
            // Restore the original values.
            d3dDevice.RenderState.SourceBlend = orgSourceBlend;
            d3dDevice.RenderState.DestinationBlend = orgDestinationBlend;
            d3dDevice.RenderState.AlphaBlendEnable = orgAlphaBlendEnable;
        }
    }
  2. Add this shape to the scene:
    private void InitScene()
    {
        ...
     
        // Add the mouse-over mark.
        _pickMarkBox = new MarkBox();
        _scene.Shapes.Add(_pickMarkBox);
    
        mdxSceneHost.Scene = _scene;
    }
  3. Enable mouse-over shape test:
    <MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
                                        D3dSurfaceWidth="2000"
                                        D3dSurfaceHeight="2000"
                                        D3dSurfaceMouseWheel="mdxSceneHost_D3dSurfaceMouseWheel"
                                        IsMouseOverShapeTestEnabled="True" />
  4. Add an event-handler to the MouseOverShapeChanged event:
    <MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
                                        D3dSurfaceWidth="2000"
                                        D3dSurfaceHeight="2000"
                                        D3dSurfaceMouseWheel="mdxSceneHost_D3dSurfaceMouseWheel"
                                        IsMouseOverShapeTestEnabled="True"
                                        MouseOverShapeChanged="mdxSceneHost_MouseOverShapeChanged" />
  5. Implement it to set the current mouse-over shape as the marked shape:
    private void mdxSceneHost_MouseOverShapeChanged(object sender, MouseOverShapeChangedRoutedEventArgs e)
    {
        D3dShape pickedShape = e.NewShape;
    
        mdxSceneHost.PerformSceneAction(() => _pickMarkBox.MarkedShape = pickedShape);
    }

The result is as follows:

Manipulate scene example

Render heavy scenes

For demonstrating how we can use the parallel rendering and the out-of-view shapes filtering abilities of the scene, to improve the rendering performance, we create a window that presents some rotating stars and, enables controlling on the rendered stars' count and and how they are rendered.

For presenting our rotating stars scene, we:

  1. Add a D3dScenePresenter:
    <Border Grid.Row="1"
            BorderBrush="DarkBlue" BorderThickness="2" Margin="10">
        <MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
                                            D3dSurfaceWidth="2000"
                                            D3dSurfaceHeight="2000" />
    </Border>
  2. Extend the Star class (from the previous example), to hold an indication for its rotation axis:
    public class ExtendedStar : Star
    {
        public int RotationAxis { get; set; }
    }
  3. Build a scene using that ExtendedStar:
    private ExtendedD3dScene _scene;
    private D3dShape[] _shapes;
    private int _maxShapesCount = 10000;
    private int _currentShapesCount = 0;
    
    private void InitShapes()
    {
        _shapes = new D3dShape[_maxShapesCount];
    
        int starsBoxCellsPerAxis = (int)(Math.Sqrt(_maxShapesCount) * 1.5);
    
        bool[, ,] shapesExistenceStatus = new bool[starsBoxCellsPerAxis, starsBoxCellsPerAxis, starsBoxCellsPerAxis];
    
        System.Drawing.Color[] colors = new System.Drawing.Color[]
                                                {
                                                    System.Drawing.Color.Red,
                                                    System.Drawing.Color.Green,
                                                    System.Drawing.Color.Blue,
                                                    System.Drawing.Color.Yellow,
                                                    System.Drawing.Color.Purple
                                                };
    
        Random rand = new Random(DateTime.Now.Millisecond);
    
        for (int shapeInx = 0; shapeInx < _maxShapesCount; shapeInx++)
        {
            int xIndex = rand.Next(starsBoxCellsPerAxis);
            int yIndex = rand.Next(starsBoxCellsPerAxis);
            int zIndex = rand.Next(starsBoxCellsPerAxis);
    
            while (shapesExistenceStatus[xIndex, yIndex, zIndex])
            {
                xIndex = rand.Next(starsBoxCellsPerAxis);
                yIndex = rand.Next(starsBoxCellsPerAxis);
                zIndex = rand.Next(starsBoxCellsPerAxis);
            }
    
            int colorIndex = rand.Next(colors.Length);
            int rotationAxis = rand.Next(3);
    
            float starSize = 30;
    
            D3dShape currShape = new ExtendedStar
            {
                DefaultMaterial = new Material { Diffuse = colors[colorIndex] },
                TranslationX = ((-(float)starsBoxCellsPerAxis) / 2 + xIndex) * starSize,
                TranslationY = ((-(float)starsBoxCellsPerAxis) / 2 + yIndex) * starSize,
                TranslationZ = ((-(float)starsBoxCellsPerAxis) / 2 + zIndex) * starSize,
                ScalingX = starSize,
                ScalingY = starSize,
                ScalingZ = starSize,
                RotationAxis = rotationAxis
            };
            _shapes[shapeInx] = currShape;
    
            shapesExistenceStatus[xIndex, yIndex, zIndex] = true;
        }
    }
    
    private void InitScene()
    {
        _scene = new D3dScene
        {
            ClearColor = System.Drawing.Color.Gray
        };
    
        _scene.Camera.ZFarPlane = 60000;
    
        // Set the light
        _scene.Lights.Add(new D3dDirectionalLight
        {
            XDirection = 1,
            YDirection = -1,
            ZDirection = -1,
            Diffuse = System.Drawing.Color.DimGray
        });
    
        _scene.Lights.Add(new D3dDirectionalLight
        {
            XDirection = -1,
            YDirection = 1,
            ZDirection = 1,
            Diffuse = System.Drawing.Color.Gray
        });
    
        _currentShapesCount = _maxShapesCount / 2;
        _scene.Shapes.AddRange(_shapes.Take(_currentShapesCount));
        txtTotalStars.Text = _currentShapesCount.ToString();
    
        mdxSceneHost.Scene = _scene;
    }
  4. Run a thread for updating the rotation of each ExtendedStar:
    private Thread _updateThread;
    private bool _continueUpdateThread;
    
    private void UpdateScene()
    {
        if (_scene.Shapes.Count != _currentShapesCount)
        {
            _scene.Shapes.Clear();
            _scene.Shapes.AddRange(
                _shapes.Take(_currentShapesCount));
        }
    
        Parallel.ForEach(_scene.Shapes, shape =>
        {
            float rotationRadians = 0.1f;
    
            ExtendedStar es = shape as ExtendedStar;
            int rotateAxisNumber = null != es ? es.RotationAxis : 0;
    
            switch (rotateAxisNumber)
            {
                case 0:
                    shape.RotationX += rotationRadians;
                    break;
                case 1:
                    shape.RotationY += rotationRadians;
                    break;
                case 2:
                    shape.RotationZ += rotationRadians;
                    break;
            }
        });
    }
    
    private void StartUpdateThread()
    {
        _continueUpdateThread = true;
        _updateThread = new Thread(new ThreadStart(() =>
        {
            while (_continueUpdateThread)
            {
                mdxSceneHost.PerformSceneAction(UpdateScene);
    
                Thread.Sleep(10);
            }
        }));
        _updateThread.Start();
    }
    
    private void StopUpdateThread()
    {
        if (_updateThread != null)
        {
            _continueUpdateThread = false;
            _updateThread.Join();
            _updateThread = null;
        }
    }

For determining if we discard out-of-view shapes, we

  1. Create a static class for holding the current rendered stars' number:
    public static class DemoContext
    {
        public static int RenderedStars { get; set; }
    }
  2. Extend the D3dScene class, to reset the current rendered stars' number before rendering and, fire an event after rendering:
    public class ExtendedD3dScene : D3dScene
    {
        public override void Render(Microsoft.DirectX.Direct3D.Device d3dDevice)
        {
            DemoContext.RenderedStars = 0;
    
            base.Render(d3dDevice);
    
            if (null != SceneRenderd)
            {
                SceneRenderd(DemoContext.RenderedStars);
            }
        }
    
        public event Action<int> SceneRenderd;
    }
  3. Override the Render method in the ExtendedStar class, to increase the current rendered stars' number, for every rendered star:
    public class ExtendedStar : Star
    {
        public int RotationAxis { get; set; }
    
        public override void Render(Device d3dDevice)
        {
            base.Render(d3dDevice);
    
            if (RenderAlsoIfOutOfView || !_composedStar.IsBoundingBoxOutOfView())
            {
                DemoContext.RenderedStars++;
            }
        }
    }
  4. Add a TextBlock for indicating the current rendered stars' count:
    <TextBlock Text="Rendered stars: " />
    <TextBlock Name="txtRenderedStars" />
  5. Add an event-handler, for updating the indication of the current rendered stars' count, on every rendition:
    private void InitScene()
    {
        _scene = new ExtendedD3dScene
        {
            ClearColor = System.Drawing.Color.Gray
        };
    
        _scene.Camera.ZFarPlane = 60000;
    
        _scene.SceneRenderd +=
            renderedStars =>
            Dispatcher.Invoke(new ThreadStart(() => txtRenderedStars.Text = renderedStars.ToString()),
                                TimeSpan.FromMilliseconds(2000));
    
        ...
    }
  6. Add a CheckBox for indicating if we filtering out-of-view shapes:
    <CheckBox Name="cbDiscardOutOfView" Grid.Column="1"
                Content="Discard out-of-view shapes"
                IsChecked="{Binding DiscardOutOfView, Mode=TwoWay}" />
  7. Update the RenderShapesAlsoIfOutOfView property of our scene, according to that CheckBox:
    public bool DiscardOutOfView
    {
        get { return (bool)GetValue(DiscardOutOfViewProperty); }
        set { SetValue(DiscardOutOfViewProperty, value); }
    }
    
    public static readonly DependencyProperty DiscardOutOfViewProperty =
        DependencyProperty.Register("DiscardOutOfView", typeof(bool), 
        typeof(HeavySceneExampleWindow), new UIPropertyMetadata(false, OnDiscardOutOfViewChanged));
    
    private static void OnDiscardOutOfViewChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        HeavySceneExampleWindow win = sender as HeavySceneExampleWindow;
        if (win == null)
        {
            return;
        }
    
        bool isDiscardOutOfView = win.DiscardOutOfView;
    
        D3dScene scene = win._scene;
        win.mdxSceneHost.PerformSceneAction(() => scene.RenderShapesAlsoIfOutOfView = !isDiscardOutOfView);
    }

For determining if the scene is rendered parallelly, we add a CheckBox for indicating if we render our scene parallelly:

<CheckBox Name="cbRenderParallelly" 
            Content="Parallel rendering"
            IsChecked="{Binding RenderParallelly, Mode=TwoWay}" />

and, update the RenderShapesParallelly property of our scene, according to that CheckBox:

public bool RenderParallelly
{
    get { return (bool)GetValue(RenderParallellyProperty); }
    set { SetValue(RenderParallellyProperty, value); }
}

public static readonly DependencyProperty RenderParallellyProperty =
    DependencyProperty.Register("RenderParallelly", typeof(bool), 
    typeof(HeavySceneExampleWindow), new UIPropertyMetadata(false, OnRenderParallellyChanged));

private static void OnRenderParallellyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    HeavySceneExampleWindow win = sender as HeavySceneExampleWindow;
    if (win == null)
    {
        return;
    }

    bool isRenderParallelly = win.RenderParallelly;

    D3dScene scene = win._scene;
    win.mdxSceneHost.PerformSceneAction(() => scene.RenderShapesParallelly = isRenderParallelly);
}

For controlling the count of the rendered stars, we a Slider:

<Slider Name="starsCountSlider" Minimum="0" Maximum="1" Value="0.5" 
        ValueChanged="starsCountSlider_ValueChanged" />

and, update the current shapes count according to its value:

private void starsCountSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    if (!IsLoaded)
    {
        return;
    }

    _currentShapesCount = (int)(_maxShapesCount * starsCountSlider.Value);
    txtTotalStars.Text = _currentShapesCount.ToString();
}

The result is as follows:

Heavy scene example

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Shmuel Zang
Software Developer
Israel Israel
No Biography provided
Follow on   LinkedIn

Comments and Discussions

 
QuestionWhy MDX? Pinmemberscosta_FST19-Feb-13 21:01 
AnswerRe: Why MDX? PinmemberShmuel Zang21-Feb-13 23:40 
GeneralMy vote of 1 PinmvpDave Kreskowiak2-Feb-13 16:22 
GeneralRe: My vote of 1 PinmemberShmuel Zang2-Feb-13 19:21 
GeneralRe: My vote of 1 PinmvpDave Kreskowiak3-Feb-13 4:29 
GeneralRe: My vote of 1 [modified] PinmemberShmuel Zang3-Feb-13 10:08 

This article doesn't contain a lot of theoretical text, because it is more like an "How to do" tutorial than a magazine or a research and, it means how to do in CODE.

Every "GIGANTIC code" (in your words), comes after a line that describes its purpose. I don't explain every simple line of the code (since, as I wrote, this article assumes a basic understanding of the C# language) but, I do explain what the code-snippets do and, the key concepts of the solution.

I don't think that every article should contain a lot of theoretical explanations. Sometimes, people just search a solution to a problem and, that what it is.

This article describes a practical solution to common tasks and, this solution is a CODE. I describe every part of the code and, provides examples for demonstrating how to use it but, this article is about a CODE solution.



modified 3-Feb-13 16:36pm.

GeneralRe: My vote of 1 PinmvpDave Kreskowiak3-Feb-13 13:13 
GeneralRe: My vote of 1 [modified] PinmemberiJam_j7-Apr-13 5:02 
GeneralRe: My vote of 1 PinmvpDave Kreskowiak7-Apr-13 5:12 
GeneralRe: My vote of 1 PinmemberShmuel Zang1-Jun-13 10:39 
GeneralMy vote of 5 PinmemberBen_131-Jan-13 10:28 
GeneralRe: My vote of 5 PinmemberShmuel Zang31-Jan-13 20:01 
GeneralVery Impressive, I vote Five PinmemberBen_131-Jan-13 10:24 
GeneralRe: Very Impressive, I vote Five PinmemberShmuel Zang31-Jan-13 20:00 
GeneralMy vote of 5 PinmemberYvar Birx30-Jan-13 8:37 
GeneralRe: My vote of 5 PinmemberShmuel Zang31-Jan-13 9:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140709.1 | Last Updated 15 Feb 2013
Article Copyright 2013 by Shmuel Zang
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid