65.9K
CodeProject 正在变化。 阅读更多。
Home

D3dScenePresenter - 如何使用 MDX 显示和操作 3D 场景

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (13投票s)

2013 年 1 月 30 日

CPOL

14分钟阅读

viewsIcon

49446

downloadIcon

1628

本文介绍了如何使用托管 DirectX 显示 3D 场景,并对其执行常见操作(缩放、旋转、移动、缩放到特定区域、调整相机以查看整个场景以及在渲染表面特定区域拾取 3D 形状)。

Sample Image

目录

引言

在为我们的应用程序开发 3D 场景时,有时我们希望对场景启用某些操作。我们可能希望相对于当前视图移动或旋转相机。我们可能希望对场景进行缩放。我们可能希望显示整个场景或其中的特定区域。我们也可能希望在渲染表面的特定点拾取一个形状。

本文介绍了如何实现这些操作,以及如何创建一个 UI 控件来执行这些操作。

背景

在本文中,我将讨论拾取(查找特定表面点后面的图形)以及如何为 MDX(托管 DirectX)框架的所有可用相机执行常见的相机变换。

本文假设您理解 C# 语言并对 DirectX 概念有基本了解。

您可以在 Direct3D 图形 MSDN 主题中了解更多关于 Direct3D 的信息。

工作原理

场景

创建场景元素

本文的主要讨论内容是可以在场景上执行的操作。但是,没有场景就无法执行任何场景操作。本节介绍了我们场景的组成部分。

关于 DirectX 场景元素(矩阵、灯光等)的讨论超出了本文的范围。因此,我们仅介绍场景类的一部分,而不深入讨论它们实际如何工作。但是,如果您熟悉这些概念,很容易理解。您可以在 Direct3D 教程 MSDN 主题中了解更多关于这些概念的信息。

正如我所写,本节仅介绍我们场景组件的代码。因此,如果您想直接阅读本文的主要讨论内容,可以跳过本节,然后跳转到 相机变换 部分或 拾取 部分。

对于我们的场景,我们创建类来实现可用的相机

  • 一个 `enum` 用于指示相机的坐标系
    public enum CameraCoordinateSystem
    {
        RightHanded,
        LeftHanded
    }
  • 相机实现基类
    • 相机坐标系
      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
      }
    • 视图矩阵
      • 用于保存相机位置的属性
        #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
      • 用于保存相机目标位置的属性
        #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
      • 用于保存相机上向量的属性
        #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
      • 用于保存视图矩阵的属性
        #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;
        }
    • 投影矩阵
      • 用于保存近平面和远平面距离的属性
        #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
      • 用于保存投影矩阵的属性
        #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();
    • 应用相机设置
      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; }
  • 正交相机
    • 正交相机实现基类
      public abstract class D3dOrthoCameraBase : D3dCamera
      {
      }
    • 正交相机
      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
      }
    • 正交非中心相机
      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
      }
  • 透视相机
    • 透视相机实现基类
      public abstract class D3dPerspectiveCameraBase : D3dCamera
      {
      }
    • 透视相机
      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
      }
    • 透视非中心相机
      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
      }
    • 透视 FOV 相机
      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
      }

创建类来实现可用的灯光

  • 灯光实现基类
    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);
    }
  • 方向光
    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
    }
  • 点光源
    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
    }
  • 聚光灯
    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
    }

创建类来实现形状

  • 形状实现基类
    • 形状材质
      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; }
      }
    • 世界矩阵
      • 用于保存形状平移的属性
        #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
      • 用于保存形状缩放的属性
        #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
      • 用于保存形状旋转的属性
        #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
      • 用于保存自身世界矩阵的属性
        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;
      • 获取实际世界矩阵的方法
        public virtual Matrix GetActualWorldMatrix()
        {
            Matrix res = OwnWorldMatrix;
        
            if (null != Parent)
            {
                res *= Parent.GetActualWorldMatrix();
            }
        
            return res;
        }
        
        public Matrix CurrentWorldMatrix { get;  protected set; }
    • 渲染形状的抽象方法
      public abstract void Render(Device d3dDevice);
  • 单形状实现基类
    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);
    }
  • 组合形状
    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
    }
  • 一个简单的盒子
    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);
        }
    }

并创建一个类来保存整个场景

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
}

并行渲染

为了提高渲染性能,我们可以使用 TPL 并行渲染形状,如下所示

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; }

    ...
}

忽略视线外形状

另一个可以提高渲染性能的事情是丢弃视线外的形状。这可以通过通知形状环境矩阵来完成

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; }

    ...
}

并在每次渲染时检查形状是否在投影区域内,如下所示

// 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);
        }
    }

    ...
}

拾取

相交测试

我们可能想对场景执行的操作之一是检查渲染表面上的一个点是否包含在特定形状中。我们可以通过检查任何形状的三角形是否与一个包含在所需表面点中的所有 3D 点的射线相交来实现。

为了获取单个形状的三角形交集,我们可以添加一个方法来获取形状的三角形

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;
    }
}

并根据给定射线获取三角形交集

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++;
        }
    }

    ...
}

为了获取组合形状的三角形交集,我们可以合并其内部形状的三角形交集

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));
    }

    ...
}

拾取合适的形状

现在,在我们进行了交集检查后,我们需要做的就是获取合适的射线并使用它进行交集检查。

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

    ...
}

为了支持不可拾取形状,我们可以添加一个属性来指示形状是否可拾取

public abstract class D3dShape
{
    ...

    public bool IsPickable { get; set; }

    ...
}

并在拾取操作中使用它

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;
    }

    ...
}

相机变换

缩放

我们将要讨论的第一个相机操作是缩放。在此操作中,我们根据给定的缩放因子缩放相机的视图。

我们可以通过向基类添加一个抽象的 `Zoom` 方法来启用相机缩放

public abstract class D3dCamera
{
    ...

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

    ...
}

并在派生类中适当地实现它

  • 对于 `D3dOrthoCamera` 和 `D3dPerspectiveCamera` 的缩放,我们可以适当地将 `Width` 和 `Height` 与给定参数相乘
    public override void Zoom(float scalingFactorX, float scalingFactorY)
    {
        Width *= scalingFactorX;
        Height *= scalingFactorY;
    }
  • 对于 `D3dOrthoOffCenterCamera` 和 `D3dPerspectiveOffCenterCamera` 的缩放,我们可以根据给定参数更改 `Left`、`Right`、`Bottom` 和 `Top`。
    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;
    }
  • 对于 `D3dPerspectiveFovCamera` 的缩放,我们需要计算新的 `FieldOfViewY` 和新的 `AspectRatio`。
    • 可以使用以下方程计算 `FieldOfViewY`
    • fov zoom

      =>

      public override void Zoom(float scalingFactorX, float scalingFactorY)
      {
          double newFov = Math.Atan(Math.Tan(FieldOfViewY/2)*scalingFactorY)*2;
          FieldOfViewY = (float) newFov;
      }
    • `AspectRatio` 的缩放是 X 缩放和 Y 缩放的商。
      public override void Zoom(float scalingFactorX, float scalingFactorY)
      {
          double newFov = Math.Atan(Math.Tan(FieldOfViewY/2)*scalingFactorY)*2;
          FieldOfViewY = (float) newFov;
      
          AspectRatio *= (scalingFactorX/scalingFactorY);
      }

旋转

相对轴

我们将要讨论的第二个相机操作是旋转。由于我们希望根据当前视图旋转相机,因此我们必须知道相对轴的方向。

  • 相对 Y 轴方向是:上向量的方向。
    public Vector3 GetRelativeYAxisDirection()
    {
        Vector3 relativeYAxisDirection = new Vector3(UpX, UpY, UpZ);
        relativeYAxisDirection.Normalize();
    
        return relativeYAxisDirection;
    }
  • 相对 Z 轴方向(从相机到场景)是:目标与位置之间的差值。
    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;
    }
  • 相对 X 轴方向是:Y 轴方向,绕相对 Z 轴旋转 90 度。
    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;
    }
相机和目标旋转

现在,在我们有了相对轴之后,就可以用它们进行旋转了。

旋转可以围绕相机目标(相机位置的旋转)或围绕相机位置(相机目标的旋转)进行。为了指示旋转中心,我们添加以下 `enum`:

public enum CameraTransformationCenterPosition
{
    CameraPosition,
    TargetPosition
}

为了围绕所需的旋转中心旋转相机,我们可以:

  1. 平移相机,将旋转中心置于 (0,0,0) 坐标。
  2. 对另一个点(相机位置或相机目标)应用所需的旋转。
  3. 将相机平移回原位。

这可以按如下方式完成:

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;
}

最后,在我们实现了旋转后,就可以添加方法来执行每个轴的旋转了。

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);
}

移动

我们将要讨论的第三个相机操作是移动。

为了向前和向后移动相机,我们可以:

  1. 根据视图深度和给定的相对距离获取适当的距离。
  2. 沿相对 Z 轴按获取的距离移动相机。

这可以按如下方式完成:

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);
}

为了相对当前视图水平和垂直移动相机,我们可以:

  1. 根据视图的宽度和高度以及给定的相对距离获取适当的距离。
  2. 沿相对 X 和 Y 轴按获取的距离移动相机。

这可以按如下方式完成:

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);            
}

当处理正交相机时,由于近平面和远平面的尺寸相同(以及它们之间的平面),因此根据近平面的尺寸移动相机可以获得所需的移动效果(我们看到所有形状都在以所需的距离移动,无论它们在场景中的深度如何)。但是,当处理透视相机时,根据近平面的尺寸移动相机不足以获得所需的移动效果(近处的形状比远处的形状移动的距离更大)。因此,在透视相机的情况下,我们可以通过旋转相机以适应相机的视场角来实现所需的移动效果。

为了根据透视相机的视场角进行旋转,我们向 `D3dPerspectiveCameraBase` 类添加一个抽象的 `GetFov` 方法。

public abstract class D3dPerspectiveCameraBase : D3dCamera
{
    ...

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

    ...
}

并在派生类中适当地实现它。

  • 对于 `D3dPerspectiveFovCamera`,我们可以将 `FieldOfViewY` 属性设置为 Y 的视场角,并将 `FieldOfViewY` 乘以 `AspectRatio` 作为 X 的视场角。
    public override void GetFov(out float fovX, out float fovY)
    {
        fovX = FieldOfViewY*AspectRatio;
        fovY = FieldOfViewY;
    }
  • 对于 `D3dPerspectiveCamera`,我们可以使用以下方程计算视场角:

    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;
    }
  • 对于 `D3dPerspectiveOffCenterCamera`,我们可以:
    1. 根据 `Left`、`Right`、`Bottom` 和 `Top` 属性计算宽度和高度。
      public override void GetFov(out float fovX, out float fovY)
      {
          float width = Right - Left;
          float height = Top - Bottom;
      }
    2. 使用计算出的宽度和高度,与在 `D3dPerspectiveCamera` 相机中计算视场角相同的方式,计算视场角。
      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;
      }

并用它来适当地旋转相机。

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);
}

缩放到投影区域

我们将要讨论的第四个相机操作是缩放到查看投影场景中的特定区域。

为了缩放到查看特定投影区域,我们可以:

  1. 将相机移动到投影区域的中心。
  2. 根据投影区域的尺寸缩放相机。

这可以按如下方式完成:

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);
    }
}

调整相机视图

我们将要讨论的最后一个相机操作是调整相机以查看场景中所有形状。

为了调整相机的视图以包含一组给定的形状,我们可以:

  1. 获取所有给定形状的投影区域。
    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. 将相机移动到投影区域的中心,并获取更新的投影区域。
    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. 如果更新的投影区域的近平面超出了相机的视图,则将相机移动到包含更新的投影区域的近平面,并获取更新的投影区域。
    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. 如果更新的投影区域的远平面超出了相机的视图,则适当地设置相机的远平面,并获取更新的投影区域。
    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. 缩放相机以包含更新的投影区域。
    public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
    {
        ...
    
        // Zoom to contain the projection region.
        ZoomToProjectionRegion(projectionRegionLeft, projectionRegionTop,
                                projectionRegionRight, projectionRegionBottom, keepAspectRatio);
    }

由于形状投影区域的尺寸可能因投影而异,因此上述调整算法并不总是能给出精确的所需结果。如果重复该算法几次,可以解决这个问题。

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; }

显示场景

渲染场景

现在,我们有了一个渲染和操作 3D 场景的平台。下一步是创建一个控件来在 UI 上显示我们的场景。

为了实现我们的场景与 WPF UI 之间的互操作性,我们可以使用我的 D3dHost 作为我们控件的基类。

public class D3dScenePresenter : D3dHost
{
}

在该控件中,我们添加一个属性来保存场景。

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; }

而且,由于我们不希望在场景复杂的情况下阻塞 UI,因此我们创建了一个单独的线程来渲染场景。

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

鼠标相机操作

为了使用鼠标对我们的场景进行操作,我们可以:

  1. 添加一个列表来保存可用的鼠标移动操作。
    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. 使用默认鼠标操作对其进行初始化。
    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. 实现可用的操作。
    • 缩放:
      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);
      }
    • ZoomToRegionUniformZoomToRegion
      • 添加一个 Adorner 来指示选定的区域。
        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;
      • 使用该 `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. 处理 `D3dSurfaceMouseMove` 事件以执行相应操作。
    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

为了启用用于操作场景的命令绑定,我们为每个操作添加一个 RoutedCommand。例如,这是 `UniformZoom` 操作的 `RoutedCommand`:

#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);

    ...
}

为了启用用于调整我们相机视图的命令绑定,我们添加另一个 `RoutedCommand` 来对我们的场景执行 `AdjustCameraView` 操作。

#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);

    ...
}

拾取

我们还可以利用场景的拾取能力来通知当前的鼠标悬停形状。这可以通过以下方式实现:

  1. 添加一个 RoutedEvent,在当前鼠标悬停形状更改时触发。
    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. 添加一个属性来保存当前的鼠标悬停形状。
    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. 创建一个线程来拾取鼠标光标下的形状。
    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. 拾取当前的鼠标悬停形状,用于每次鼠标移动或场景渲染。
    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

如何使用

构建和操作场景

创建形状

为了演示 `D3dScenePresenter` 控件在显示和操作场景中的用法,我们创建了一个窗口,该窗口显示一些形状并允许对这些形状进行场景操作。

要创建单个形状,我们可以创建一个派生自 `D3dSingleShape` 的类。

public class Pyramid : D3dSingleShape
{
}

并实现它的 `InitDrawing` 和 `Draw` 方法。

  • 为了初始化金字塔顶点缓冲区,我们:
    1. 创建顶点位置(每个轴的比例为 `-0.5`..`0.5`)。
      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. 计算金字塔每个侧面的法线。
    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);
  • 使用这些位置和法线创建金字塔的顶点。
    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);
    }
  • 为了绘制金字塔,我们只绘制创建的顶点。
    protected override void Draw(Device d3dDevice)
    {
        if (null == _pyramidVertices)
        {
            return;
        }
    
        d3dDevice.VertexFormat = CustomVertex.PositionNormal.Format;
        d3dDevice.DrawUserPrimitives(PrimitiveType.TriangleList, _verticesNumber / 3, _pyramidVertices);
    }

为了在我们的形状上启用拾取,我们还必须实现 `GetTrianglesPointsPositions` 方法。

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;
}

要创建组合形状,我们可以:

  1. 创建一个派生自 `D3dShape` 的类。
  2. 添加一个 `D3dComposedShape` 数据成员来保存形状的集合。
  3. 实现 `Render`、`AddPointIntersections` 和 `AddRayIntersections` 方法,以使用 `D3dComposedShape` 的方法。

为了演示,我们添加了另外两个形状:

  • 房子:
    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
    }
  • 星星:
    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
    }

构建场景

为了显示我们的形状,我们:

  1. 添加一个 `D3dScenePresenter`。
    <Border BorderBrush="DarkGreen" BorderThickness="2" Margin="10">
        <MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
                                            D3dSurfaceWidth="2000"
                                            D3dSurfaceHeight="2000" />
    </Border>
  2. 使用形状创建场景。
    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. 将 `D3dScenePresenter` 的场景设置为创建的场景。
    private void InitScene()
    {
        ...
    
        mdxSceneHost.Scene = _scene;
    }

操作场景

使用鼠标操作

为了使用鼠标操作我们的场景,我们可以使用下表中描述的默认鼠标操作设置。

操作 (Operation) 鼠标和键盘组合
缩放鼠标中键
UniformZoomShift + 鼠标中键
XyMoveShift + 鼠标左键
ZMoveShift + 鼠标右键
TargetXyRotateAlt + 鼠标右键
TargetZRotateAlt + 鼠标左键
CameraXyRotateControl + Alt + 鼠标右键
CameraZRotateControl + Alt + 鼠标左键
ZoomToRegion鼠标左键 + 鼠标右键
UniformZoomToRegionShift + 鼠标左键 + 鼠标右键

我们可以通过更改 `D3dScenePresenter` 控件的 `MouseMoveOperations` 列表来更改这些默认设置。

使用操作按钮

为了使用 UI 按钮操作我们的场景,我们可以添加按钮并将每个按钮的 `Command` 绑定到 `D3dScenePresenter` 控件的相应 `RoutedCommand`。例如,这里是 `UniformZoomCommand` 命令的按钮:

<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}"/>

使用鼠标滚轮缩放

为了使用鼠标滚轮进行缩放,我们可以向 `D3dSurfaceMouseWheel` 事件添加一个事件处理程序。

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

并实现它以使用 `UniformZoomCommand` 命令。

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

实现鼠标悬停效果

为了实现鼠标悬停效果,我们可以:

  1. 创建一个形状来标记当前的鼠标悬停形状。
    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. 将此形状添加到场景中。
    private void InitScene()
    {
        ...
     
        // Add the mouse-over mark.
        _pickMarkBox = new MarkBox();
        _scene.Shapes.Add(_pickMarkBox);
    
        mdxSceneHost.Scene = _scene;
    }
  3. 启用鼠标悬停形状测试。
    <MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
                                        D3dSurfaceWidth="2000"
                                        D3dSurfaceHeight="2000"
                                        D3dSurfaceMouseWheel="mdxSceneHost_D3dSurfaceMouseWheel"
                                        IsMouseOverShapeTestEnabled="True" />
  4. 添加一个事件处理程序到 `MouseOverShapeChanged` 事件。
    <MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
                                        D3dSurfaceWidth="2000"
                                        D3dSurfaceHeight="2000"
                                        D3dSurfaceMouseWheel="mdxSceneHost_D3dSurfaceMouseWheel"
                                        IsMouseOverShapeTestEnabled="True"
                                        MouseOverShapeChanged="mdxSceneHost_MouseOverShapeChanged" />
  5. 实现它以将当前的鼠标悬停形状设置为标记形状。
    private void mdxSceneHost_MouseOverShapeChanged(object sender, MouseOverShapeChangedRoutedEventArgs e)
    {
        D3dShape pickedShape = e.NewShape;
    
        mdxSceneHost.PerformSceneAction(() => _pickMarkBox.MarkedShape = pickedShape);
    }

结果如下

Manipulate scene example

渲染复杂场景

为了演示如何使用场景的并行渲染和视线外形状过滤能力来提高渲染性能,我们创建了一个窗口,该窗口显示一些旋转的星星,并允许控制渲染的星星数量以及它们的渲染方式。

为了显示我们旋转的星星场景,我们:

  1. 添加一个 `D3dScenePresenter`。
    <Border Grid.Row="1"
            BorderBrush="DarkBlue" BorderThickness="2" Margin="10">
        <MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
                                            D3dSurfaceWidth="2000"
                                            D3dSurfaceHeight="2000" />
    </Border>
  2. 扩展 `Star` 类(来自前面的示例),以包含其旋转轴的指示。
    public class ExtendedStar : Star
    {
        public int RotationAxis { get; set; }
    }
  3. 使用该 `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. 运行一个线程来更新每个 `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;
        }
    }

为了确定是否丢弃视线外形状,我们:

  1. 创建一个静态类来保存当前渲染的星星数量。
    public static class DemoContext
    {
        public static int RenderedStars { get; set; }
    }
  2. 扩展 `D3dScene` 类,在渲染前重置当前渲染的星星数量,并在渲染后触发一个事件。
    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. 在 `ExtendedStar` 类中覆盖 `Render` 方法,为每个渲染的星星增加当前渲染的星星数量。
    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. 添加一个 `TextBlock` 来指示当前渲染的星星数量。
    <TextBlock Text="Rendered stars: " />
    <TextBlock Name="txtRenderedStars" />
  5. 添加一个事件处理程序,用于在每次渲染时更新当前渲染的星星数量的指示。
    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. 添加一个 `CheckBox` 来指示是否过滤视线外形状。
    <CheckBox Name="cbDiscardOutOfView" Grid.Column="1"
                Content="Discard out-of-view shapes"
                IsChecked="{Binding DiscardOutOfView, Mode=TwoWay}" />
  7. 根据该 `CheckBox` 更新我们场景的 `RenderShapesAlsoIfOutOfView` 属性。
    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);
    }

为了确定场景是否并行渲染,我们添加一个 `CheckBox` 来指示我们是否并行渲染我们的场景。

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

并根据该 `CheckBox` 更新我们场景的 `RenderShapesParallelly` 属性。

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);
}

为了控制渲染的星星数量,我们添加了一个 `Slider`。

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

并根据其值更新当前形状的数量。

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

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

结果如下:

Heavy scene example

© . All rights reserved.