Table of contents
Introduction
When developing 3D scenes for our applications, we sometimes want to enable some manipulations on the scenes. We may want to move or rotate our camera relative to the current view.
We may want to perform zooming on our scene. We may want to show the full scene or, a specific region of it.
We may also want to pick a shape in a specific point on the rendered surface.
This article shows how we can implement those operations and, how we can create a UI control that enables performing them.
Background
In this article I discuss on picking (find the graphic that behind a specific surface's point) and, on how we can perform common camera transformations for the whole of the available cameras of the MDX (Managed DirectX) framework.
This article assumes an understanding of the C# language and a basic knowledge on DirectX concepts.
You can read more about Direct3D on the Direct3D Graphics MSDN topic.
How it works
The scene
Create the scene elements
The main discussion of this article is about operations that can be done on a scene. But, we cannot perform any scene's operation without a scene to perform that operation on. This section presents the components of our scene.
The discussion on the DirectX's scene's elements (matrices, lights, etc..), is beyond the scope of this article. So, we just present the parts of the scene's classes, without discussing deeply on how they actually works. But, if you are familiar with the concepts, it is easily understandable. You can read more on those concepts on the Direct3D Tutorials MSDN topic.
As I wrote, this section just presents the code for our scene's components. So, if you want to get directly to the main discussion of this article, you can skip this section and, jump to the Camera transformations section or to the Picking section.
For our scene, we create classes for implementing the available cameras:
Create classes for implementing the available lights:
- Base class for lights implementation:
public abstract class D3dLight
{
protected D3dLight()
{
Diffuse = Color.White;
Enabled = true;
}
public virtual void Render(Device d3dDevice)
{
if (0 > Index)
{
return;
}
lock (d3dDevice)
{
SetSpecificLightValues(d3dDevice);
d3dDevice.Lights[Index].Diffuse = Diffuse;
d3dDevice.Lights[Index].Ambient = Ambient;
d3dDevice.Lights[Index].Specular = Specular;
d3dDevice.Lights[Index].Enabled = Enabled;
}
}
#region properties
public int Index { get; set; }
public Color Diffuse { get; set; }
public Color Ambient { get; set; }
public Color Specular { get; set; }
public bool Enabled { get; set; }
#endregion
protected abstract void SetSpecificLightValues(Device d3dDevice);
}
- Directional light:
public class D3dDirectionalLight : D3dLight
{
public D3dDirectionalLight()
{
XDirection = 1;
YDirection = -1;
ZDirection = -1;
}
#region D3dLight implementation
protected override void SetSpecificLightValues(Device d3dDevice)
{
d3dDevice.Lights[Index].Type = LightType.Directional;
d3dDevice.Lights[Index].XDirection = XDirection;
d3dDevice.Lights[Index].YDirection = YDirection;
d3dDevice.Lights[Index].ZDirection = ZDirection;
}
#endregion
#region properties
public float XDirection { get; set; }
public float YDirection { get; set; }
public float ZDirection { get; set; }
#endregion
}
- Point light:
public class D3dPointLight : D3dLight
{
public D3dPointLight()
{
XPosition = YPosition = ZPosition = 0;
Range = 1000;
Attenuation0 = 1;
Attenuation1 = 0;
Attenuation2 = 0;
}
#region D3dLight implementation
protected override void SetSpecificLightValues(Device d3dDevice)
{
d3dDevice.Lights[Index].Type = LightType.Point;
d3dDevice.Lights[Index].XPosition = XPosition;
d3dDevice.Lights[Index].YPosition = YPosition;
d3dDevice.Lights[Index].ZPosition = ZPosition;
d3dDevice.Lights[Index].Range = Range;
d3dDevice.Lights[Index].Attenuation0 = Attenuation0;
d3dDevice.Lights[Index].Attenuation1 = Attenuation1;
d3dDevice.Lights[Index].Attenuation2 = Attenuation2;
}
#endregion
#region properties
public float XPosition { get; set; }
public float YPosition { get; set; }
public float ZPosition { get; set; }
public float Range { get; set; }
public float Attenuation0 { get; set; }
public float Attenuation1 { get; set; }
public float Attenuation2 { get; set; }
#endregion
}
- Spot light:
public class D3dSpotLight : D3dLight
{
public D3dSpotLight()
{
XPosition = YPosition = ZPosition = 0;
XDirection = 1;
YDirection = -1;
ZDirection = -1;
Range = 1000;
Attenuation0 = 1;
Attenuation1 = 0;
Attenuation2 = 0;
InnerConeAngle = (float)(Math.PI / 16);
OuterConeAngle = (float)(Math.PI / 4);
Falloff = 1;
}
#region D3dLight implementation
protected override void SetSpecificLightValues(Device d3dDevice)
{
d3dDevice.Lights[Index].Type = LightType.Spot;
d3dDevice.Lights[Index].XPosition = XPosition;
d3dDevice.Lights[Index].YPosition = YPosition;
d3dDevice.Lights[Index].ZPosition = ZPosition;
d3dDevice.Lights[Index].XDirection = XDirection;
d3dDevice.Lights[Index].YDirection = YDirection;
d3dDevice.Lights[Index].ZDirection = ZDirection;
d3dDevice.Lights[Index].Range = Range;
d3dDevice.Lights[Index].Attenuation0 = Attenuation0;
d3dDevice.Lights[Index].Attenuation1 = Attenuation1;
d3dDevice.Lights[Index].Attenuation2 = Attenuation2;
d3dDevice.Lights[Index].InnerConeAngle = InnerConeAngle;
d3dDevice.Lights[Index].OuterConeAngle = OuterConeAngle;
d3dDevice.Lights[Index].Falloff = Falloff;
}
#endregion
#region properties
public float XPosition { get; set; }
public float YPosition { get; set; }
public float ZPosition { get; set; }
public float XDirection { get; set; }
public float YDirection { get; set; }
public float ZDirection { get; set; }
public float Range { get; set; }
public float Attenuation0 { get; set; }
public float Attenuation1 { get; set; }
public float Attenuation2 { get; set; }
public float InnerConeAngle { get; set; }
public float OuterConeAngle { get; set; }
public float Falloff { get; set; }
#endregion
}
Create classes for implementing shapes:
- Base class for shapes implementation:
- Shape's material:
public abstract class D3dShape
{
protected D3dShape()
{
DefaultMaterial = new Material
{
DiffuseColor = new ColorValue(1.0f, 1.0f, 1.0f, 1.0f)
};
IsVisible = true;
}
protected static object _shapeLoaderLock = "Shape load lock";
public Material DefaultMaterial { get; set; }
public D3dShape Parent { get; set; }
public bool IsVisible { get; set; }
}
- World matrix:
- Properties for holding the shape's translation:
#region TranslationX
private float _translationX;
public float TranslationX
{
get { return _translationX; }
set
{
lock (this)
{
_translationX = value;
_isOwnWorldMatrixValid = false;
}
}
}
#endregion
#region TranslationY
private float _translationY;
public float TranslationY
{
get { return _translationY; }
set
{
lock (this)
{
_translationY = value;
_isOwnWorldMatrixValid = false;
}
}
}
#endregion
#region TranslationZ
private float _translationZ;
public float TranslationZ
{
get { return _translationZ; }
set
{
lock (this)
{
_translationZ = value;
_isOwnWorldMatrixValid = false;
}
}
}
#endregion
- Properties for holding the shape's scaling:
#region ScalingX
private float _scalingX;
public float ScalingX
{
get { return _scalingX; }
set
{
lock (this)
{
_scalingX = value;
_isOwnWorldMatrixValid = false;
}
}
}
#endregion
#region ScalingY
private float _scalingY;
public float ScalingY
{
get { return _scalingY; }
set
{
lock (this)
{
_scalingY = value;
_isOwnWorldMatrixValid = false;
}
}
}
#endregion
#region ScalingZ
private float _scalingZ;
public float ScalingZ
{
get { return _scalingZ; }
set
{
lock (this)
{
_scalingZ = value;
_isOwnWorldMatrixValid = false;
}
}
}
#endregion
- Properties for holding the shape's rotation:
#region RotationX
private float _rotationX;
public float RotationX
{
get { return _rotationX; }
set
{
lock (this)
{
_rotationX = value;
_isOwnWorldMatrixValid = false;
}
}
}
#endregion
#region RotationY
private float _rotationY;
public float RotationY
{
get { return _rotationY; }
set
{
lock (this)
{
_rotationY = value;
_isOwnWorldMatrixValid = false;
}
}
}
#endregion
#region RotationZ
private float _rotationZ;
public float RotationZ
{
get { return _rotationZ; }
set
{
lock (this)
{
_rotationZ = value;
_isOwnWorldMatrixValid = false;
}
}
}
#endregion
- Property for holding the own world matrix:
private Matrix _ownWorldMatrix;
public Matrix OwnWorldMatrix
{
get
{
lock (this)
{
if (!_isOwnWorldMatrixValid)
{
_ownWorldMatrix = CalculateOwnWorldMatrix();
_isOwnWorldMatrixValid = true;
}
}
return _ownWorldMatrix;
}
}
protected virtual Matrix CalculateOwnWorldMatrix()
{
return Matrix.Scaling(new Vector3(ScalingX, ScalingY, ScalingZ))*
Matrix.RotationX(RotationX)*Matrix.RotationY(RotationY)*
Matrix.RotationZ(RotationZ)*
Matrix.Translation(new Vector3(TranslationX, TranslationY, TranslationZ));
}
protected bool _isOwnWorldMatrixValid;
- Method for getting the actual world matrix:
public virtual Matrix GetActualWorldMatrix()
{
Matrix res = OwnWorldMatrix;
if (null != Parent)
{
res *= Parent.GetActualWorldMatrix();
}
return res;
}
public Matrix CurrentWorldMatrix { get; protected set; }
- Abstract method for rendering the shape:
public abstract void Render(Device d3dDevice);
- Base class for single shapes implementation:
public abstract class D3dSingleShape : D3dShape
{
#region D3dShape implementation
public override void Render(Device d3dDevice)
{
if (null == d3dDevice)
{
return;
}
if (!IsVisible)
{
return;
}
InitDrawing();
CurrentWorldMatrix = GetActualWorldMatrix();
lock (d3dDevice)
{
d3dDevice.Material = DefaultMaterial;
d3dDevice.Transform.World = CurrentWorldMatrix;
Draw(d3dDevice);
}
}
#endregion
protected abstract void InitDrawing();
protected abstract void Draw(Device d3dDevice);
}
- Composed shape:
public class D3dComposedShape : D3dShape
{
#region D3dShape implementation
public override void Render(Device d3dDevice)
{
if (!IsVisible)
{
return;
}
CurrentWorldMatrix = GetActualWorldMatrix();
Shapes.ForEach(s => RenderShape(s, d3dDevice));
}
#endregion
protected virtual void RenderShape(D3dShape shape, Device d3dDevice)
{
shape.Parent = this;
shape.RenderAlsoIfOutOfView = RenderAlsoIfOutOfView;
shape.EnvironmentData = EnvironmentData;
shape.Render(d3dDevice);
}
#region Shapes
private List<D3dShape> _shapes;
public List<D3dShape> Shapes
{
get { return _shapes ?? (_shapes = new List<D3dShape>()); }
}
#endregion
}
- A simple box:
public class D3dBox : D3dSingleShape
{
private const int _verticesNumber = 36; private static CustomVertex.PositionNormal[] _boxVertices = null;
#region D3dSingleShape implementation
protected override void InitDrawing()
{
if (null != _boxVertices)
{
return;
}
lock (_shapeLoaderLock)
{
if (null == _boxVertices)
{
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);
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);
CustomVertex.PositionNormal[] vertices = new CustomVertex.PositionNormal[_verticesNumber];
SetPositionNormalRectangle(vertices, 0, frontTopRightPosition, frontTopLeftPosition,
frontBottomLeftPosition, frontBottomRightPosition, frontNormal);
SetPositionNormalRectangle(vertices, 6, frontTopLeftPosition, backTopLeftPosition,
backBottomLeftPosition,
frontBottomLeftPosition, leftNormal);
SetPositionNormalRectangle(vertices, 12, frontTopLeftPosition, frontTopRightPosition,
backTopRightPosition, backTopLeftPosition, upNormal);
SetPositionNormalRectangle(vertices, 18, backTopRightPosition, frontTopRightPosition,
frontBottomRightPosition, backBottomRightPosition, rightNormal);
SetPositionNormalRectangle(vertices, 24, frontBottomRightPosition, frontBottomLeftPosition,
backBottomLeftPosition, backBottomRightPosition, downNormal);
SetPositionNormalRectangle(vertices, 30, backTopLeftPosition, backTopRightPosition,
backBottomRightPosition, backBottomLeftPosition, backNormal);
_boxVertices = vertices;
}
}
}
protected override void Draw(Device d3dDevice)
{
if (null == _boxVertices)
{
return;
}
d3dDevice.VertexFormat = CustomVertex.PositionNormal.Format;
d3dDevice.DrawUserPrimitives(PrimitiveType.TriangleList, _verticesNumber / 3, _boxVertices);
}
#endregion
private void SetPositionNormalTriangle(CustomVertex.PositionNormal[] verticesArray, int startIndex,
Vector3 firstPosition, Vector3 secondPosition, Vector3 thirdPosition, Vector3 normal)
{
verticesArray[startIndex].Position = firstPosition;
verticesArray[startIndex].Normal = normal;
verticesArray[startIndex + 1].Position = secondPosition;
verticesArray[startIndex + 1].Normal = normal;
verticesArray[startIndex + 2].Position = thirdPosition;
verticesArray[startIndex + 2].Normal = normal;
}
private void SetPositionNormalRectangle(CustomVertex.PositionNormal[] verticesArray, int startIndex,
Vector3 firstPosition, Vector3 secondPosition, Vector3 thirdPosition, Vector3 fourthPosition, Vector3 normal)
{
SetPositionNormalTriangle(verticesArray, startIndex, firstPosition, secondPosition, thirdPosition, normal);
SetPositionNormalTriangle(verticesArray, startIndex + 3, thirdPosition, fourthPosition, firstPosition,
normal);
}
}
and, create a class for holding a full scene:
public class D3dScene
{
public D3dScene()
{
ClearColor = Color.Black;
}
public virtual void Render(Device d3dDevice)
{
if (null == d3dDevice)
{
return;
}
InitDevice(d3dDevice);
d3dDevice.BeginScene();
RenderCamera(d3dDevice);
RenderLights(d3dDevice);
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)
{
int lightsCount = d3dDevice.Lights.Count;
for (int oldLightInx = 0; oldLightInx < lightsCount; oldLightInx++)
{
d3dDevice.Lights[oldLightInx].Enabled = false;
}
int lightIndex = 0;
foreach (D3dLight light in Lights)
{
if (null != light && light.Enabled)
{
light.Index = lightIndex;
light.Render(d3dDevice);
lightIndex++;
}
}
}
protected virtual void RenderShapes(Device d3dDevice)
{
Shapes.ForEach(s => RenderShape(s, d3dDevice));
}
protected virtual void RenderShape(D3dShape shape, Device d3dDevice)
{
if (null == shape)
{
return;
}
shape.Render(d3dDevice);
}
#region Properties
#region Camera
private D3dCamera _camera;
public D3dCamera Camera
{
get { return _camera ?? (_camera = new D3dPerspectiveFovCamera()); }
set { _camera = value; }
}
#endregion
#region Lights
private List<D3dLight> _lights;
public List<D3dLight> Lights
{
get { return _lights ?? (_lights = new List<D3dLight>()); }
}
#endregion
#region Shapes
private List<D3dShape> _shapes;
public List<D3dShape> Shapes
{
get { return _shapes ?? (_shapes = new List<D3dShape>()); }
}
#endregion
public Color ClearColor { get; set; }
#endregion
}
Render parallelly
For improving the rendering performance, we can render the shapes parallelly using TPL, as follows:
public class D3dScene
{
...
protected virtual void RenderShapes(Device d3dDevice)
{
if (RenderShapesParallelly)
{
Parallel.ForEach(Shapes, s => RenderShape(s, d3dDevice));
}
else
{
Shapes.ForEach(s => RenderShape(s, d3dDevice));
}
}
public bool RenderShapesParallelly { get; set; }
...
}
Ignore out-of-view shapes
Another thing that can improve the rendering performance is discarding the out-of-view shapes. That can be done by notifying the shape about the environment matrices:
public class ShapeEnvironmentData
{
public ShapeEnvironmentData()
{
_viewMatrix = Matrix.Identity;
_projectionMatrix = Matrix.Identity;
ViewProjectionMatrix = Matrix.Identity;
}
#region ViewMatrix
private Matrix _viewMatrix;
public Matrix ViewMatrix
{
get { return _viewMatrix; }
set
{
_viewMatrix = value;
ViewProjectionMatrix = GetViewProjectionMatrix();
}
}
#endregion
#region ProjectionMatrix
private Matrix _projectionMatrix;
public Matrix ProjectionMatrix
{
get { return _projectionMatrix; }
set
{
_projectionMatrix = value;
ViewProjectionMatrix = GetViewProjectionMatrix();
}
}
#endregion
#region ViewProjectionMatrix
public Matrix ViewProjectionMatrix { get; protected set; }
protected Matrix GetViewProjectionMatrix()
{
return ViewMatrix * ProjectionMatrix;
}
#endregion
}
public abstract class D3dShape
{
...
public bool RenderAlsoIfOutOfView { get; set; }
public ShapeEnvironmentData EnvironmentData { get; set; }
...
}
public class D3dScene
{
...
protected virtual void RenderShapes(Device d3dDevice)
{
ShapeEnvironmentData environmentData = GetShapeEnvironmentData();
if (RenderShapesParallelly)
{
Parallel.ForEach(Shapes, s => RenderShape(s, environmentData, d3dDevice));
}
else
{
Shapes.ForEach(s => RenderShape(s, environmentData, d3dDevice));
}
}
protected virtual ShapeEnvironmentData GetShapeEnvironmentData()
{
return new ShapeEnvironmentData
{
ViewMatrix = Camera.ViewMatrix,
ProjectionMatrix = Camera.ProjectionMatrix
};
}
protected virtual void RenderShape(D3dShape shape, ShapeEnvironmentData environmentData, Device d3dDevice)
{
if (null == shape)
{
return;
}
shape.RenderAlsoIfOutOfView = RenderShapesAlsoIfOutOfView;
shape.EnvironmentData = environmentData;
shape.Render(d3dDevice);
}
public bool RenderShapesAlsoIfOutOfView { get; set; }
...
}
and, checking if the shape is in the projection region, for each rendering, as follows:
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);
}
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)
{
BoundingBox boundingBox = GetBoundingBoxBeforeTransformation();
boundingBox.TransformCoordinates(worldViewProjectionMatrix);
return boundingBox.IsOutOfBoundingBox(-1f, -1f, 0f, 1f, 1f, 1f);
}
public virtual bool IsBoundingBoxOutOfView()
{
if (null == EnvironmentData)
{
return false;
}
return IsBoundingBoxOutOfView(CurrentWorldMatrix * EnvironmentData.ViewProjectionMatrix);
}
...
}
public abstract class D3dSingleShape : D3dShape
{
...
public override void Render(Device d3dDevice)
{
if (null == d3dDevice)
{
return;
}
if (!IsVisible)
{
return;
}
InitDrawing();
CurrentWorldMatrix = GetActualWorldMatrix();
if (!RenderAlsoIfOutOfView && IsBoundingBoxOutOfView())
{
return;
}
lock (d3dDevice)
{
d3dDevice.Material = DefaultMaterial;
d3dDevice.Transform.World = CurrentWorldMatrix;
Draw(d3dDevice);
}
}
...
}
Picking
Intersections test
One of the operations we may want to perform on our scene is, checking if a point on the rendered surface is contained in a specific shape.
We can do that by, checking if any of the shapes triangles, intersects with a ray, that contains the whole of the 3D points that are rendered in the wanted surface's point.
For getting the triangles' intersections of a single shape, we can add a method for getting the shapes triangles:
public struct TrianglePointsPositions
{
public Vector3 Position1;
public Vector3 Position2;
public Vector3 Position3;
}
public abstract class D3dSingleShape : D3dShape
{
...
protected abstract IEnumerable<TrianglePointsPositions> GetTrianglesPointsPositions();
}
public class D3dBox : D3dSingleShape
{
private const int _verticesNumber = 36; private static CustomVertex.PositionNormal[] _boxVertices = null;
private const int _trianglesNumber = 12; private static TrianglePointsPositions[] _boxTriangles = null;
...
protected override IEnumerable<TrianglePointsPositions> GetTrianglesPointsPositions()
{
if (null != _boxTriangles)
{
return _boxTriangles;
}
if (null == _boxVertices)
{
return null;
}
lock (_shapeLoaderLock)
{
if (null == _boxTriangles)
{
TrianglePointsPositions[] triangles = new TrianglePointsPositions[_trianglesNumber];
for (int triangleInx = 0; triangleInx < _trianglesNumber; triangleInx++)
{
triangles[triangleInx] = new TrianglePointsPositions
{
Position1 = _boxVertices[triangleInx * 3].Position,
Position2 = _boxVertices[(triangleInx * 3) + 1].Position,
Position3 = _boxVertices[(triangleInx * 3) + 2].Position
};
}
_boxTriangles = triangles;
}
}
return _boxTriangles;
}
}
and, get the triangles' intersections according to a given ray:
public abstract class D3dShape
{
...
public abstract void AddRayIntersections(Vector3 rayOrigin, Vector3 rayDirection,
List<IntersectResult> targetIntersectionsList);
public bool IsHitTestVisible { get; set; }
...
}
public abstract class D3dSingleShape : D3dShape
{
...
public override void AddRayIntersections(Vector3 rayOrigin, Vector3 rayDirection,
List<IntersectResult> targetIntersectionsList)
{
if (!IsHitTestVisible)
{
return;
}
Matrix invertedWorldMatrix = Matrix.Invert(CurrentWorldMatrix);
Vector3 transformedRayOrigin = Vector3.TransformCoordinate(rayOrigin, invertedWorldMatrix);
Vector3 transformedRayDirection =
Vector3.TransformNormal(rayDirection, invertedWorldMatrix);
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)
{
shapeIntersection = new IntersectResult
{
Shape = this
};
lock (targetIntersectionsList)
{
targetIntersectionsList.Add(shapeIntersection);
}
}
currTriangleIntersection.FaceIndex = triangleInx;
shapeIntersection.TriangleIntersections.Add(currTriangleIntersection);
}
triangleInx++;
}
}
...
}
For getting the triangles' intersections of a composed shape, we can merge the triangles' intersections of its inner shapes:
public class D3dComposedShape : D3dShape
{
...
public override void AddRayIntersections(Vector3 rayOrigin,
Vector3 rayDirection, List<IntersectResult> targetIntersectionsList)
{
if (!IsHitTestVisible)
{
return;
}
Parallel.ForEach(Shapes, shape => shape.AddRayIntersections(rayOrigin,
rayDirection, targetIntersectionsList));
}
...
}
Pick the appropriate shape
Now, after we have the intersections check, all what we have to do is, to get the appropriate ray and, to use it for the intersections checks:
public class D3dScene
{
...
public D3dShape Pick(Device d3dDevice, float surfaceX, float surfaceY)
{
LastPickIntersections = GetPointIntersections(d3dDevice, surfaceX, surfaceY);
D3dShape res = GetLastPickClosestShape();
return res;
}
public List<IntersectResult> GetPointIntersections(Device d3dDevice, float surfaceX, float surfaceY)
{
Vector3 nearPlanePoint = new Vector3(surfaceX, surfaceY, 0);
Vector3 farPlanePoint = new Vector3(surfaceX, surfaceY, 1);
nearPlanePoint.Unproject(d3dDevice.Viewport, d3dDevice.Transform.Projection, d3dDevice.Transform.View,
Matrix.Identity);
farPlanePoint.Unproject(d3dDevice.Viewport, d3dDevice.Transform.Projection, d3dDevice.Transform.View,
Matrix.Identity);
Vector3 rayDirection = Vector3.Subtract(farPlanePoint, nearPlanePoint);
List<IntersectResult> res = new List<IntersectResult>();
Parallel.ForEach(Shapes, shape => shape.AddRayIntersections(nearPlanePoint, rayDirection, res));
return res;
}
public D3dShape GetLastPickClosestShape()
{
return LastPickIntersections.OrderBy(ir => ir.TriangleIntersections.Min(ti => ti.Dist)).
Select(ir => ir.Shape).FirstOrDefault();
}
#region LastPickIntersections
private List<IntersectResult> _lastPickIntersections;
public List<IntersectResult> LastPickIntersections
{
get { return _lastPickIntersections ?? (_lastPickIntersections = new List<IntersectResult>()); }
protected set { _lastPickIntersections = value; }
}
#endregion
...
}
For supporting unpickable shapes, we can add a property that indicates if a shape is pickable:
public abstract class D3dShape
{
...
public bool IsPickable { get; set; }
...
}
and, use it in the picking operation:
public class D3dScene
{
...
public D3dShape Pick(Device d3dDevice, float surfaceX, float surfaceY)
{
LastPickIntersections = GetPointIntersections(d3dDevice, surfaceX, surfaceY);
D3dShape res = GetLastPickClosestShape();
while (null != res && !res.IsPickable)
{
res = res.Parent;
}
return res;
}
...
}
Camera transformations
Zoom
The first camera operation we are going to discuss on, is zooming. In that operation, we scale the camera's view according to a given scaling factor.
We can enable zooming in our cameras by, adding an abstract Zoom
method to the base class:
public abstract class D3dCamera
{
...
public abstract void Zoom(float scalingFactorX, float scalingFactorY);
...
}
and, implement it appropriately in the derived classes:
- For zooming a
D3dOrthoCamera
and a D3dPerspectiveCamera
, we can multiply the Width
and the Height
with the given parameters appropriately:
public override void Zoom(float scalingFactorX, float scalingFactorY)
{
Width *= scalingFactorX;
Height *= scalingFactorY;
}
- For zooming a
D3dOrthoOffCenterCamera
and a D3dPerspectiveOffCenterCamera
, we can change the Left
, the Right
, the Bottom
and, the Top
, according to the given parameters:
public override void Zoom(float scalingFactorX, float scalingFactorY)
{
float centerX = (Left + Right)/2;
float centerY = (Bottom + Top)/2;
float halfWidth = ((Right - Left)/2)*scalingFactorX;
float halfHeight = ((Top - Bottom)/2)*scalingFactorY;
Left = centerX - halfWidth;
Right = centerX + halfWidth;
Bottom = centerY - halfHeight;
Top = centerY + halfHeight;
}
- For zooming a
D3dPerspectiveFovCamera
, we have to calculate the new FieldOfViewY
and the new AspectRatio
:
- The
FieldOfViewY
can be calculated with the following equation:
public override void Zoom(float scalingFactorX, float scalingFactorY)
{
double newFov = Math.Atan(Math.Tan(FieldOfViewY/2)*scalingFactorY)*2;
FieldOfViewY = (float) newFov;
}
- The
AspectRatio
's scaling is the quotient of the X-scaling and the Y-scaling:
public override void Zoom(float scalingFactorX, float scalingFactorY)
{
double newFov = Math.Atan(Math.Tan(FieldOfViewY/2)*scalingFactorY)*2;
FieldOfViewY = (float) newFov;
AspectRatio *= (scalingFactorX/scalingFactorY);
}
Rotate
Relative axes
The second camera operation we are going to discuss on, is rotating. Since we want to rotate our camera according to the current view, we have to know the directions of the relative axes:
- The relative Y axis direction is: the direction of the Up vector:
public Vector3 GetRelativeYAxisDirection()
{
Vector3 relativeYAxisDirection = new Vector3(UpX, UpY, UpZ);
relativeYAxisDirection.Normalize();
return relativeYAxisDirection;
}
- The relative Z axis direction (from the camera to the scene) is: the difference between the target and the position:
public Vector3 GetRelativeZAxisDirection()
{
Vector3 cameraPosition = new Vector3(PositionX, PositionY, PositionZ);
Vector3 targetPosition = new Vector3(TargetX, TargetY, TargetZ);
Vector3 relativeZAxisDirection = targetPosition - cameraPosition;
relativeZAxisDirection.Normalize();
return relativeZAxisDirection;
}
- The relative X axis direction is: the Y axis direction, rotated by 90 degrees, around the relative Z axis:
public Vector3 GetRelativeXAxisDirection()
{
Vector3 relativeZAxisDirection = GetRelativeZAxisDirection();
Vector3 relativeXAxisDirection = GetRelativeYAxisDirection();
float rotationRadians = (CameraCoordinateSystem.RightHanded == CoordinateSystem)
? (float) Math.PI/2
: (float) -Math.PI/2;
relativeXAxisDirection.TransformNormal(Matrix.RotationAxis(relativeZAxisDirection, rotationRadians));
return relativeXAxisDirection;
}
Camera and target rotation
Now, after we have the relative axes, we can use them for our rotation.
The rotation can be done around the camera's target (rotation of the camera's position) or, around the camera's position (rotation of the camera's target). For indicating the rotation's center, we add the following enum
:
public enum CameraTransformationCenterPosition
{
CameraPosition,
TargetPosition
}
For rotating the camera around the wanted rotation's center, we can:
- Translate the camera to put the rotation center in the (0,0,0) coordinate.
- Apply the wanted rotation on the other point (the camera's position or, the camera's target).
- Translate the camera back.
That can be done as follows:
public void Rotate(Matrix rotationTransformation, CameraTransformationCenterPosition centerPosition)
{
Matrix translationBefore;
Matrix translationAfter;
Matrix transformation;
if (CameraTransformationCenterPosition.TargetPosition == centerPosition)
{
translationBefore = Matrix.Translation(-TargetX, -TargetY, -TargetZ);
translationAfter = Matrix.Translation(TargetX, TargetY, TargetZ);
transformation = translationBefore*rotationTransformation*translationAfter;
TransformPosition(transformation);
}
else
{
translationBefore = Matrix.Translation(-PositionX, -PositionY, -PositionZ);
translationAfter = Matrix.Translation(PositionX, PositionY, PositionZ);
transformation = translationBefore*rotationTransformation*translationAfter;
TransformTarget(transformation);
}
TransformUp(rotationTransformation);
}
public void TransformPosition(Matrix transformation)
{
Vector3 cameraPosition = new Vector3(PositionX, PositionY, PositionZ);
cameraPosition.TransformCoordinate(transformation);
PositionX = cameraPosition.X;
PositionY = cameraPosition.Y;
PositionZ = cameraPosition.Z;
}
public void TransformTarget(Matrix transformation)
{
Vector3 targetPosition = new Vector3(TargetX, TargetY, TargetZ);
targetPosition.TransformCoordinate(transformation);
TargetX = targetPosition.X;
TargetY = targetPosition.Y;
TargetZ = targetPosition.Z;
}
public void TransformUp(Matrix transformation)
{
Vector3 upDirection = new Vector3(UpX, UpY, UpZ);
upDirection.TransformCoordinate(transformation);
UpX = upDirection.X;
UpY = upDirection.Y;
UpZ = upDirection.Z;
}
Finally, after we have the implementation of the rotation, we can add methods for performing the rotation for each axis:
public enum CameraRotationDirection
{
Clockwise,
CounterClockwise
}
public void RelativeRotateX(float relativeArcLength,
CameraTransformationCenterPosition centerPosition,
CameraRotationDirection rotationDirection)
{
const float pi = (float) Math.PI;
float rotation = relativeArcLength*(pi*2);
if ((CameraCoordinateSystem.LeftHanded == CoordinateSystem &&
CameraRotationDirection.CounterClockwise == rotationDirection) ||
(CameraCoordinateSystem.RightHanded == CoordinateSystem &&
CameraRotationDirection.Clockwise == rotationDirection))
{
rotation *= -1f;
}
Vector3 relativeXAxisDirection = GetRelativeXAxisDirection();
Matrix rotationTransformation = Matrix.RotationAxis(relativeXAxisDirection, rotation);
Rotate(rotationTransformation, centerPosition);
}
public void RelativeRotateY(float relativeArcLength,
CameraTransformationCenterPosition centerPosition,
CameraRotationDirection rotationDirection)
{
const float pi = (float) Math.PI;
float rotation = relativeArcLength*(pi*2);
if ((CameraCoordinateSystem.LeftHanded == CoordinateSystem &&
CameraRotationDirection.CounterClockwise == rotationDirection) ||
(CameraCoordinateSystem.RightHanded == CoordinateSystem &&
CameraRotationDirection.Clockwise == rotationDirection))
{
rotation *= -1f;
}
Vector3 relativeYAxisDirection = GetRelativeYAxisDirection();
Matrix rotationTransformation = Matrix.RotationAxis(relativeYAxisDirection, rotation);
Rotate(rotationTransformation, centerPosition);
}
public void RelativeRotateZ(float relativeArcLength,
CameraTransformationCenterPosition centerPosition,
CameraRotationDirection rotationDirection)
{
const float pi = (float) Math.PI;
float rotation = relativeArcLength*(pi*2);
if ((CameraRotationDirection.Clockwise == rotationDirection &&
CameraTransformationCenterPosition.CameraPosition == centerPosition) ||
(CameraRotationDirection.CounterClockwise == rotationDirection &&
CameraTransformationCenterPosition.TargetPosition == centerPosition))
{
if (CameraCoordinateSystem.RightHanded == CoordinateSystem)
{
rotation *= -1f;
}
}
else
{
if (CameraCoordinateSystem.LeftHanded == CoordinateSystem)
{
rotation *= -1f;
}
}
Vector3 relativeZAxisDirection = GetRelativeZAxisDirection();
Matrix rotationTransformation = Matrix.RotationAxis(relativeZAxisDirection, rotation);
Rotate(rotationTransformation, centerPosition);
}
Move
The third camera operation we are going to discuss on, is moving.
For moving the camera forwards and backwards, we can:
- Get the appropriate distance according to the view's depth and the given relative-distance.
- Move the camera by the gotten distance on the relative Z axis.
That can be done as follows:
public virtual void RelativeZMove(float relativeDistance)
{
float projectionRegionDepth = ZFarPlane - ZNearPlane;
Vector3 translationVector = GetRelativeZAxisDirection();
translationVector.Multiply(projectionRegionDepth*relativeDistance);
Matrix translation = Matrix.Translation(translationVector);
TransformPosition(translation);
TransformTarget(translation);
}
For moving the camera horizontally and vertically, relative to the current view, we can:
- Get the appropriate distance according to the view's width and height and, the given relative-distances.
- Move the camera by the gotten distances on the relative X and Y axes.
That can be done as follows:
public override void RelativeXyMove(float relativeDistanceX, float relativeDistanceY)
{
Vector3 translateXyDistance = new Vector3(relativeDistanceX, relativeDistanceY, 0);
Matrix invertedProjectionMatrix = Matrix.Invert(ProjectionMatrix);
translateXyDistance.TransformCoordinate(invertedProjectionMatrix);
Vector3 translateX = GetRelativeXAxisDirection();
translateX.Multiply(translateXyDistance.X);
Vector3 translateY = GetRelativeYAxisDirection();
translateY.Multiply(translateXyDistance.Y);
Matrix translation = Matrix.Translation(translateX) * Matrix.Translation(translateY);
TransformPosition(translation);
TransformTarget(translation);
}
When dealing with an orthographic camera, since the near-plane and the far-plane are with the same dimensions (and so the planes between), moving the camera according to the near-plane's dimensions, gives us the wanted moving effect (we see the whole of the shapes, moving by the wanted distance, no matter how deep they are in the scene).
But, when dealing with a perspective camera, moving the camera according to the near-plane's dimensions, doesn't enough to give us the wanted moving effect (the near shapes are moved by more distance than the far shapes).
So, in the case of perspective camera, we can make the wanted moving effect, by rotating the camera according to the camera's field-of-view.
For rotating a perspective camera according to its field-of-view, we add an abstract GetFov
method to the D3dPerspectiveCameraBase
class:
public abstract class D3dPerspectiveCameraBase : D3dCamera
{
...
public abstract void GetFov(out float fovX, out float fovY);
...
}
implement it appropriately in the derived classes:
- For
D3dPerspectiveFovCamera
, we can set the FieldOfViewY
property as the Y's field-of-view and, set the FieldOfViewY
multiplied by the AspectRatio
as the X's field-of-view:
public override void GetFov(out float fovX, out float fovY)
{
fovX = FieldOfViewY*AspectRatio;
fovY = FieldOfViewY;
}
- For
D3dPerspectiveCamera
, we can calculate the field-of-view, with the following equation:
public override void GetFov(out float fovX, out float fovY)
{
fovX = (float) Math.Atan(Width/(2*ZNearPlane))*2;
fovY = (float) Math.Atan(Height/(2*ZNearPlane))*2;
}
- For
D3dPerspectiveOffCenterCamera
, we can:
- Calculate the width and the height, according to the
Left
, the Right
, the Bottom
and, the Top
properties.
public override void GetFov(out float fovX, out float fovY)
{
float width = Right - Left;
float height = Top - Bottom;
}
- Calculate the field-of-view, as same as we calculated it in the
D3dPerspectiveCamera
camera, using the calculated width and height:
public override void GetFov(out float fovX, out float fovY)
{
float width = Right - Left;
float height = Top - Bottom;
fovX = (float)Math.Atan(width / (2 * ZNearPlane)) * 2;
fovY = (float)Math.Atan(height / (2 * ZNearPlane)) * 2;
}
and, use it for rotating the camera appropriately:
public override void RelativeXyMove(float relativeDistanceX, float relativeDistanceY)
{
float fovX;
float fovY;
GetFov(out fovX, out fovY);
Vector3 yAxis = GetRelativeYAxisDirection();
float xAngle = (fovX/2)*relativeDistanceX;
if (CameraCoordinateSystem.RightHanded == CoordinateSystem)
{
xAngle *= -1f;
}
Matrix rotationY = Matrix.RotationAxis(yAxis, xAngle);
Rotate(rotationY, CameraTransformationCenterPosition.CameraPosition);
Vector3 xAxis = GetRelativeXAxisDirection();
float yAngle = (fovY/2)*relativeDistanceY;
if (CameraCoordinateSystem.LeftHanded == CoordinateSystem)
{
yAngle *= -1f;
}
Matrix rotationX = Matrix.RotationAxis(xAxis, yAngle);
Rotate(rotationX, CameraTransformationCenterPosition.CameraPosition);
}
Zoom to projection region
The fourth camera operation we are going to discuss on, is zooming the camera to view a specific region on the projected scene.
For zooming the camera to view a specific projection region, we can:
- Move the camera to look at the center of the projection region.
- Zoom the camera according to the dimensions of the projection region.
That can be done as follows:
public void ZoomToProjectionRegion(float projectionRegionLeft, float projectionRegionTop,
float projectionRegionRight, float projectionRegionBottom, bool keepAspectRatio = false)
{
float centerX = (projectionRegionRight + projectionRegionLeft) / 2;
float centerY = (projectionRegionBottom + projectionRegionTop) / 2;
RelativeXyMove(centerX, centerY);
float projectionRegionWidth = Math.Abs(projectionRegionRight - projectionRegionLeft);
float projectionRegionHeight = Math.Abs(projectionRegionTop - projectionRegionBottom);
if (keepAspectRatio)
{
float scalingFactor = Math.Max(projectionRegionWidth, projectionRegionHeight) / 2;
Zoom(scalingFactor, scalingFactor);
}
else
{
Zoom(projectionRegionWidth / 2, projectionRegionHeight / 2);
}
}
Adjust camera view
The last camera operation we are going to discuss on, is adjusting the camera to view the whole of the scene's shapes.
For adjusting a camera's view to contain a set of a given shapes, we can:
- Get the projection region of the whole of the given shapes:
public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
{
if (null == shapes)
{
return;
}
D3dShape[] shapesArray = shapes.ToArray();
float projectionRegionLeft;
float projectionRegionTop;
float projectionRegionFront;
float projectionRegionRight;
float projectionRegionBottom;
float projectionRegionBack;
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)
{
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)
{
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;
}
- Move the camera to look a the center of the projection region and, get the updated projection region:
public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
{
...
float centerX;
float centerY;
centerX = (projectionRegionRight + projectionRegionLeft)/2;
centerY = (projectionRegionBottom + projectionRegionTop)/2;
RelativeXyMove(centerX, centerY);
GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
out projectionRegionFront,
out projectionRegionRight, out projectionRegionBottom,
out projectionRegionBack);
}
- If the near plane of the updated projection region is out of the camera's view, move the camera to contain the near plane of the updated projection region and, get the updated projection region:
public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
{
...
if (0 > projectionRegionFront || 1 < projectionRegionFront)
{
float zMove = projectionRegionFront - ((projectionRegionBack - projectionRegionFront)*0.1f);
RelativeZMove(zMove);
GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
out projectionRegionFront,
out projectionRegionRight, out projectionRegionBottom,
out projectionRegionBack);
}
}
- If the far plane of the updated projection region is out of the camera's view, set the camera's far plane appropriately and, get the updated projection region:
public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
{
...
if (1 < projectionRegionBack)
{
ZFarPlane = ZNearPlane + (ZFarPlane - ZNearPlane)*(projectionRegionBack + 0.2f);
GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
out projectionRegionFront,
out projectionRegionRight, out projectionRegionBottom,
out projectionRegionBack);
}
}
- Zoom the camera to contain the updated projection region:
public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
{
...
ZoomToProjectionRegion(projectionRegionLeft, projectionRegionTop,
projectionRegionRight, projectionRegionBottom, keepAspectRatio);
}
Since the dimensions of the shapes' projection region can be different for each projection, the adjustment algorithm above doesn't always give us the exact wanted result.
That problem can be solved, if we repeat that algorithm a couple of times:
public virtual void AdjustView(IEnumerable<D3dShape> shapes, bool keepAspectRatio = false)
{
if (null == shapes)
{
return;
}
D3dShape[] shapesArray = shapes.ToArray();
float projectionRegionLeft;
float projectionRegionTop;
float projectionRegionFront;
float projectionRegionRight;
float projectionRegionBottom;
float projectionRegionBack;
int moveTriesCount = 0;
float centerX;
float centerY;
const float epsilon = 0.000000000001f;
do
{
GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
out projectionRegionFront,
out projectionRegionRight, out projectionRegionBottom,
out projectionRegionBack);
centerX = (projectionRegionRight + projectionRegionLeft)/2;
centerY = (projectionRegionBottom + projectionRegionTop)/2;
RelativeXyMove(centerX, centerY);
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);
GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
out projectionRegionFront,
out projectionRegionRight, out projectionRegionBottom,
out projectionRegionBack);
}
if (1 < projectionRegionBack)
{
ZFarPlane = ZNearPlane + (ZFarPlane - ZNearPlane)*(projectionRegionBack + 0.2f);
GetShapesProjectionRegion(shapesArray, out projectionRegionLeft, out projectionRegionTop,
out projectionRegionFront,
out projectionRegionRight, out projectionRegionBottom,
out projectionRegionBack);
}
ZoomToProjectionRegion(projectionRegionLeft, projectionRegionTop,
projectionRegionRight, projectionRegionBottom, keepAspectRatio);
++moveTriesCount;
} while (moveTriesCount < MaxMoveTriesForAdjustmentAlgorithm &&
(Math.Abs(0 - centerX) > epsilon || Math.Abs(0 - centerY) > epsilon));
}
public int MaxMoveTriesForAdjustmentAlgorithm { get; set; }
Present the scene
Rendering the scene
For now, we have a platform for rendering and manipulating a 3D scene. The next step is, creating a control that can present our scene on the UI.
For enabling interoperability between our scene and the WPF UI, we can use my D3dHost as the base class of our control.
public class D3dScenePresenter : D3dHost
{
}
In that control we add a property for holding a scene:
public D3dScene Scene
{
get { return (D3dScene)GetValue(SceneProperty); }
set { SetValue(SceneProperty, value); }
}
public static readonly DependencyProperty SceneProperty =
DependencyProperty.Register("Scene", typeof(D3dScene), typeof(D3dScenePresenter),
new UIPropertyMetadata(null, OnSceneChanged));
private static void OnSceneChanged(DependencyObject o, DependencyPropertyChangedEventArgs arg)
{
D3dScenePresenter dsp = o as D3dScenePresenter;
if (null==dsp)
{
return;
}
dsp.CurrentScene = dsp.Scene;
}
protected D3dScene CurrentScene { get; private set; }
and, since we don't want to block the UI in cases of heavy scenes, we create a different thread for rendering the scene:
public void InvalidateScene()
{
if (null == CurrentScene)
{
return;
}
if (_renderSceneThread == null)
{
StartRenderSceneThread();
}
_renderSceneEvent.Set();
}
#region BeginSceneUpdate & EndSceneUpdate
private readonly object _sceneUpdateLocker = new object();
public void BeginSceneUpdate()
{
Monitor.Enter(_sceneUpdateLocker);
}
public void EndSceneUpdate()
{
Monitor.Exit(_sceneUpdateLocker);
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);
CurrentScene.Render(D3dDevice);
}
catch
{
}
finally
{
Monitor.Exit(_sceneUpdateLocker);
EndDrawing();
}
}
private void StartRenderSceneThread()
{
if (null == _renderSceneThread)
{
_continueRenderSceneThread = true;
_renderSceneThread = new Thread(new ThreadStart(() =>
{
while (_continueRenderSceneThread)
{
_renderSceneEvent.WaitOne();
if (_continueRenderSceneThread)
{
RenderScene();
}
}
}));
_renderSceneThread.Start();
}
}
private void StopRenderSceneThread()
{
if (_renderSceneThread != null)
{
_continueRenderSceneThread = false;
_renderSceneEvent.Set();
_renderSceneThread.Join();
_renderSceneThread = null;
}
}
#endregion
Mouse camera operations
For enabling manipulation on our scene using the mouse, we can:
- Add a list for holding the available mouse-move operations:
public enum CameraOperation
{
None,
Zoom,
UniformZoom,
XyMove,
ZMove,
TargetXyRotate,
TargetZRotate,
CameraXyRotate,
CameraZRotate,
ZoomToRegion,
UniformZoomToRegion
}
public class MouseMoveOperation
{
public MouseMoveOperation()
{
Operation = D3dScenePresenter.CameraOperation.None;
LeftButtonState = MouseButtonState.Released;
MiddleButtonState = MouseButtonState.Released;
RightButtonState = MouseButtonState.Released;
XButton1State = MouseButtonState.Released;
XButton2State = MouseButtonState.Released;
Modifiers = ModifierKeys.None;
}
#region Properties
public D3dScenePresenter.CameraOperation Operation { get; set; }
#region Mouse buttons state
public MouseButtonState LeftButtonState { get; set; }
public MouseButtonState MiddleButtonState { get; set; }
public MouseButtonState RightButtonState { get; set; }
public MouseButtonState XButton1State { get; set; }
public MouseButtonState XButton2State { get; set; }
#endregion
public ModifierKeys Modifiers { get; set; }
#endregion
public bool IsCurrentStateFitting()
{
return IsMouseCurrentStateFitting() && IsKeyboardCurrentStateFitting();
}
public bool IsMouseCurrentStateFitting()
{
return Mouse.LeftButton == LeftButtonState &&
Mouse.MiddleButton == MiddleButtonState &&
Mouse.RightButton == RightButtonState &&
Mouse.XButton1 == XButton1State &&
Mouse.XButton2 == XButton2State;
}
public bool IsKeyboardCurrentStateFitting()
{
ModifierKeys currentModifiers = ModifierKeys.None;
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
currentModifiers |= ModifierKeys.Shift;
}
if (Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt))
{
currentModifiers |= ModifierKeys.Alt;
}
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
currentModifiers |= ModifierKeys.Control;
}
if (Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin))
{
currentModifiers |= ModifierKeys.Windows;
}
return currentModifiers == Modifiers;
}
}
#region MouseMoveOperations
private List<MouseMoveOperation> _mouseMoveOperations;
public List<MouseMoveOperation> MouseMoveOperations
{
get { return _mouseMoveOperations ?? (_mouseMoveOperations = new List<MouseMoveOperation>()); }
}
#endregion
- Initialize it with default mouse operations:
private void InitActions()
{
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
});
}
}
- Implement the available operations:
Zoom
:
protected void PerformZoom(float relativeDeltaX, float relativeDeltaY)
{
CurrentScene.Camera.Zoom(1 - relativeDeltaX, 1 + relativeDeltaY);
}
UniformZoom
:
protected void PerformUniformZoom(float relativeDeltaX, float relativeDeltaY)
{
float relativeZoomDelta = Math.Abs(relativeDeltaX) > Math.Abs(relativeDeltaY)
? relativeDeltaX
: -relativeDeltaY;
PerformZoom(relativeZoomDelta, -relativeZoomDelta);
}
XyMove
:
protected void PerformXyMove(float relativeDeltaX, float relativeDeltaY)
{
CurrentScene.Camera.RelativeXyMove(-relativeDeltaX * 2, relativeDeltaY * 2);
}
ZMove
:
protected void PerformZMove(float relativeDeltaX, float relativeDeltaY)
{
float relativeDeltaZ = Math.Abs(relativeDeltaX) > Math.Abs(relativeDeltaY)
? relativeDeltaX
: -relativeDeltaY;
CurrentScene.Camera.RelativeZMove(relativeDeltaZ);
}
TargetXyRotate
:
protected void PerformTargetXyRotate(float relativeDeltaX, float relativeDeltaY)
{
CurrentScene.Camera.RelativeRotateX(relativeDeltaY/8,
CameraTransformationCenterPosition.CameraPosition, CameraRotationDirection.CounterClockwise);
CurrentScene.Camera.RelativeRotateY(relativeDeltaX/8,
CameraTransformationCenterPosition.CameraPosition, CameraRotationDirection.CounterClockwise);
}
TargetZRotate
:
protected void PerformTargetZRotate(float relativeDeltaX, float relativeDeltaY)
{
float relativeArcLength = relativeDeltaX + relativeDeltaY;
CurrentScene.Camera.RelativeRotateZ(relativeArcLength,
CameraTransformationCenterPosition.CameraPosition, CameraRotationDirection.Clockwise);
}
CameraXyRotate
:
protected void PerformCameraXyRotate(float relativeDeltaX, float relativeDeltaY)
{
CurrentScene.Camera.RelativeRotateX(relativeDeltaY,
CameraTransformationCenterPosition.TargetPosition, CameraRotationDirection.CounterClockwise);
CurrentScene.Camera.RelativeRotateY(relativeDeltaX,
CameraTransformationCenterPosition.TargetPosition, CameraRotationDirection.CounterClockwise);
}
CameraZRotate
:
protected void PerformCameraZRotate(float relativeDeltaX, float relativeDeltaY)
{
float relativeArcLength = relativeDeltaX + relativeDeltaY;
CurrentScene.Camera.RelativeRotateZ(relativeArcLength,
CameraTransformationCenterPosition.TargetPosition, CameraRotationDirection.Clockwise);
}
ZoomToRegion
and UniformZoomToRegion
:
- Add an Adorner for indicating the selected region:
protected class RegionIndicatorAdorner : Adorner
{
public RegionIndicatorAdorner(UIElement adornedElement)
: base(adornedElement)
{
Visibility = Visibility.Collapsed;
IsHitTestVisible = false;
FillBrush = new SolidColorBrush(new Color
{
A = 128,
R = 0,
G = 0,
B = 0
});
BorderPen = new Pen(new SolidColorBrush(new Color
{
A = 128,
R = 255,
G = 255,
B = 255
}), 2)
{
DashStyle = new DashStyle(new double[] { 5, 5 }, 0)
};
}
#region Properties
public double RectangleTop { get; set; }
public double RectangleLeft { get; set; }
public double RectangleWidth { get; set; }
public double RectangleHeight { get; set; }
public double ActualRectangleTop { get; private set; }
public double ActualRectangleLeft { get; private set; }
#region ActualRectangleWidth
private double _actualRectangleWidth;
public double ActualRectangleWidth { get { return _actualRectangleWidth; } }
#endregion
#region ActualRectangleHeight
private double _actualRectangleHeight;
public double ActualRectangleHeight { get { return _actualRectangleHeight; } }
#endregion
public bool KeepAspectRatio { get; set; }
public Brush FillBrush { get; set; }
public Pen BorderPen { get; set; }
#endregion
protected override void OnRender(DrawingContext drawingContext)
{
_actualRectangleWidth = RectangleWidth;
_actualRectangleHeight = RectangleHeight;
if (KeepAspectRatio)
{
AdjustActualSize(ref _actualRectangleWidth, ref _actualRectangleHeight);
}
ActualRectangleTop = _actualRectangleHeight > 0 ? RectangleTop : RectangleTop + _actualRectangleHeight;
ActualRectangleLeft = _actualRectangleWidth > 0 ? RectangleLeft : RectangleLeft + _actualRectangleWidth;
Width = Math.Abs(_actualRectangleWidth);
Height = Math.Abs(_actualRectangleHeight);
Rect r = new Rect(ActualRectangleLeft, ActualRectangleTop, Width, Height);
drawingContext.DrawRectangle(FillBrush, BorderPen, r);
}
private void AdjustActualSize(ref double actualRectangleWidth, ref double actualRectangleHeight)
{
Rectangle adornedRectangle = AdornedElement as Rectangle;
if (null == adornedRectangle)
{
return;
}
double relativeWidth = Math.Abs(actualRectangleWidth / adornedRectangle.ActualWidth);
double relativeHeight = Math.Abs(actualRectangleHeight / adornedRectangle.ActualHeight);
if (relativeWidth < relativeHeight)
{
actualRectangleHeight *= (relativeWidth / relativeHeight);
}
else
{
actualRectangleWidth *= (relativeHeight / relativeWidth);
}
}
}
protected RegionIndicatorAdorner _regionIndicatorAdorner;
- Handle the operation using that
Adorner
:
protected bool _isInZoomToRegionMode = false;
protected void HandleZoomToRegion()
{
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)
{
ApplyZoomToRegion();
}
}
else
{
if (!_isInZoomToRegionMode)
{
StartZoomToRegion();
}
else
{
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;
}
- Handle the
D3dSurfaceMouseMove
event to perform the appropriate operation:
private void InitActions()
{
D3dSurfaceMouseMove += OnD3dSurfaceMouseMove;
...
}
private void OnD3dSurfaceMouseMove(object sender, D3dSurfaceMouseEventArgs e)
{
HandleZoomToRegion();
List<CameraOperation> cameraOperations;
lock (MouseMoveOperations)
{
cameraOperations =
MouseMoveOperations.Where(o => o.IsCurrentStateFitting()).Select(o => o.Operation).Distinct().ToList();
}
if (cameraOperations.Any())
{
Point currentSurfacePosition = e.D3dSurfaceMousePosition;
Vector delta = currentSurfacePosition - _lastSurfaceMousePosition;
float relativeDeltaX = (float)(delta.X / D3dSurfaceWidth);
float relativeDeltaY = (float)(delta.Y / D3dSurfaceHeight);
foreach (CameraOperation operation in cameraOperations)
{
PerformOperation(operation, relativeDeltaX, relativeDeltaY);
}
if (_isInZoomToRegionMode && null != _regionIndicatorAdorner)
{
_regionIndicatorAdorner.RectangleWidth += delta.X * (D3dRegionActualWidth / D3dSurfaceWidth);
_regionIndicatorAdorner.RectangleHeight += delta.Y * (D3dRegionActualHeight / D3dSurfaceHeight);
_regionIndicatorAdorner.InvalidateVisual();
}
}
_lastSurfaceMousePosition = e.D3dSurfaceMousePosition;
}
protected List<Action> _pendingActions = new List<Action>();
private void PerformOperation(CameraOperation operation, float relativeDeltaX, float relativeDeltaY)
{
switch (operation)
{
case CameraOperation.Zoom:
PerformSceneAction(() => PerformZoom(relativeDeltaX, relativeDeltaY));
break;
case CameraOperation.UniformZoom:
PerformSceneAction(() => PerformUniformZoom(relativeDeltaX, relativeDeltaY));
break;
case CameraOperation.XyMove:
PerformSceneAction(() => PerformXyMove(relativeDeltaX, relativeDeltaY));
break;
case CameraOperation.ZMove:
PerformSceneAction(() => PerformZMove(relativeDeltaX, relativeDeltaY));
break;
case CameraOperation.TargetXyRotate:
PerformSceneAction(() => PerformTargetXyRotate(relativeDeltaX, relativeDeltaY));
break;
case CameraOperation.TargetZRotate:
AdjustZRotateDelta(ref relativeDeltaX, ref relativeDeltaY);
PerformSceneAction(() => PerformTargetZRotate(relativeDeltaX, relativeDeltaY));
break;
case CameraOperation.CameraXyRotate:
PerformSceneAction(() => PerformCameraXyRotate(relativeDeltaX, relativeDeltaY));
break;
case CameraOperation.CameraZRotate:
AdjustZRotateDelta(ref relativeDeltaX, ref relativeDeltaY);
PerformSceneAction(() => PerformCameraZRotate(relativeDeltaX, relativeDeltaY));
break;
case CameraOperation.ZoomToRegion:
case CameraOperation.UniformZoomToRegion:
HandleZoomToRegion();
break;
}
}
public void PerformSceneAction(Action a)
{
AddPendingAction(a);
InvalidateScene();
}
public void AddPendingAction(Action a)
{
if (null == a)
{
return;
}
lock (_pendingActions)
{
_pendingActions.Add(a);
}
}
private void AdjustZRotateDelta(ref float relativeDeltaX, ref float relativeDeltaY)
{
relativeDeltaX /= (D3dSurfaceHeight / 2) > _lastSurfaceMousePosition.Y ? 4 : -4;
relativeDeltaY /= (D3dSurfaceWidth / 2) < _lastSurfaceMousePosition.X ? 4 : -4;
}
protected void RenderScene()
{
...
List<Action> currentActions = new List<Action>();
lock (_pendingActions)
{
currentActions.AddRange(_pendingActions);
_pendingActions.Clear();
}
try
{
...
currentActions.ForEach(a => a());
CurrentScene.Render(D3dDevice);
}
catch
{
}
finally
{
...
}
}
Commands
For enabling command-bindings for manipulating the scene, we add a RoutedCommand for each operation.
For example, here is the RoutedCommand
for the UniformZoom
operation:
#region UniformZoomCommand
private static RoutedCommand _uniformZoomCommand;
public static RoutedCommand UniformZoomCommand
{
get
{
return _uniformZoomCommand ??
(_uniformZoomCommand =
new RoutedCommand("UniformZoom", typeof(D3dScenePresenter)));
}
}
protected static void CanExecuteUniformZoomCommand(object sender, CanExecuteRoutedEventArgs e)
{
D3dScenePresenter dsp = sender as D3dScenePresenter;
if (null == dsp)
{
return;
}
e.CanExecute = true;
}
protected static void ExecuteUniformZoomCommand(object sender, ExecutedRoutedEventArgs e)
{
D3dScenePresenter dsp = sender as D3dScenePresenter;
if (null == dsp)
{
return;
}
float scalingFactor = 1;
float.TryParse(e.Parameter.ToString(), out scalingFactor);
dsp.UniformZoom(scalingFactor);
}
protected void UniformZoom(float scalingFactor)
{
PerformSceneAction(() =>
{
if (null != CurrentScene)
{
CurrentScene.Camera.Zoom(scalingFactor, scalingFactor);
}
});
}
#endregion
static D3dScenePresenter()
{
...
CommandBinding uniformZoomCommandBinding =
new CommandBinding(UniformZoomCommand, ExecuteUniformZoomCommand, CanExecuteUniformZoomCommand);
CommandManager.RegisterClassCommandBinding(typeof(D3dScenePresenter), uniformZoomCommandBinding);
...
}
For enabling command-binding for adjusting our camera view, we add another RoutedCommand
that performs the AdjustCameraView
operation on our scene:
#region AdjustCameraViewCommand
private static RoutedCommand _adjustCameraViewCommand;
public static RoutedCommand AdjustCameraViewCommand
{
get
{
return _adjustCameraViewCommand ??
(_adjustCameraViewCommand =
new RoutedCommand("AdjustCameraView", typeof(D3dScenePresenter)));
}
}
protected static void CanExecuteAdjustCameraViewCommand(object sender, CanExecuteRoutedEventArgs e)
{
D3dScenePresenter dsp = sender as D3dScenePresenter;
if (null == dsp)
{
return;
}
e.CanExecute = true;
}
protected static void ExecuteAdjustCameraViewCommand(object sender, ExecutedRoutedEventArgs e)
{
D3dScenePresenter dsp = sender as D3dScenePresenter;
if (null == dsp)
{
return;
}
dsp.AdjustCameraView();
}
protected void AdjustCameraView()
{
PerformSceneAction(() =>
{
if (null != CurrentScene)
{
CurrentScene.AdjustCameraView();
}
});
}
#endregion
static D3dScenePresenter()
{
CommandBinding adjustCameraViewBinding =
new CommandBinding(AdjustCameraViewCommand, ExecuteAdjustCameraViewCommand,
CanExecuteAdjustCameraViewCommand);
CommandManager.RegisterClassCommandBinding(typeof (D3dScenePresenter), adjustCameraViewBinding);
...
}
Picking
We can also use the picking ability of our scene, for notifying the current mouse-over shape. That can be done by:
- Adding a RoutedEvent for raising when the current mouse-over shape has been changed:
public class MouseOverShapeChangedRoutedEventArgs : RoutedEventArgs
{
#region Constructors
public MouseOverShapeChangedRoutedEventArgs()
{
}
public MouseOverShapeChangedRoutedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
public MouseOverShapeChangedRoutedEventArgs(RoutedEvent routedEvent, object source)
: base(routedEvent, source)
{
}
#endregion
public D3dShape OldShape { get; set; }
public D3dShape NewShape { get; set; }
}
public delegate void MouseOverShapeChangedRoutedEventHandler(object sender, MouseOverShapeChangedRoutedEventArgs e);
#region MouseOverShapeChanged
public static readonly RoutedEvent MouseOverShapeChangedEvent = EventManager.RegisterRoutedEvent(
"MouseOverShapeChanged", RoutingStrategy.Bubble, typeof (MouseOverShapeChangedRoutedEventHandler),
typeof (D3dScenePresenter));
public event MouseOverShapeChangedRoutedEventHandler MouseOverShapeChanged
{
add { AddHandler(MouseOverShapeChangedEvent, value); }
remove { RemoveHandler(MouseOverShapeChangedEvent, value); }
}
#endregion
- Adding a property for holding the current mouse-over shape:
private D3dShape _currentMouseOverShape;
public D3dShape CurrentMouseOverShape
{
get { return _currentMouseOverShape; }
protected set
{
if (value != _currentMouseOverShape)
{
D3dShape oldValue = _currentMouseOverShape;
_currentMouseOverShape = value;
Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new ThreadStart(() =>
{
MouseOverShapeChangedRoutedEventArgs arg =
new MouseOverShapeChangedRoutedEventArgs(
D3dScenePresenter.MouseOverShapeChangedEvent)
{
OldShape = oldValue,
NewShape = _currentMouseOverShape
};
RaiseEvent(arg);
}));
}
}
}
- Creating a thread for picking the shape that is under the mouse cursor:
private Thread _pickThread = null;
private bool _continuePickThread;
private AutoResetEvent _pickEvent = new AutoResetEvent(false);
protected void UpdateCurrentMouseOverShape()
{
if (null == CurrentScene)
{
return;
}
Monitor.Enter(_sceneUpdateLocker);
try
{
D3dShape currShape = (0 <= _lastSurfaceMousePosition.X && 0 <= _lastSurfaceMousePosition.Y)
? Pick(_lastSurfaceMousePosition.X, _lastSurfaceMousePosition.Y)
: null;
CurrentMouseOverShape = currShape;
}
catch
{
}
finally
{
Monitor.Exit(_sceneUpdateLocker);
}
}
private void StartPickThread()
{
if (null == _pickThread)
{
_continuePickThread = true;
_pickThread = new Thread(new ThreadStart(() =>
{
while (_continuePickThread)
{
_pickEvent.WaitOne();
if (_continuePickThread)
{
UpdateCurrentMouseOverShape();
}
}
}));
_pickThread.Start();
}
}
private void StopPickThread()
{
if (_pickThread != null)
{
_continuePickThread = false;
_pickEvent.Set();
_pickThread.Join();
_pickThread = null;
}
}
public D3dShape Pick(double surfaceX, double surfaceY)
{
if (null == CurrentScene)
{
return null;
}
return CurrentScene.Pick(D3dDevice, (float) surfaceX, (float) surfaceY);
}
public void InvalidateCurrentMouseOverShape()
{
if (null == CurrentScene)
{
return;
}
if (_pickThread == null)
{
StartPickThread();
}
_pickEvent.Set();
}
- Picking the current mouse-over shape, for each mouse-move or, a render of the scene:
private void OnD3dSurfaceMouseMove(object sender, D3dSurfaceMouseEventArgs e)
{
...
if (cameraOperations.Any())
{
...
}
else
{
if (_isMouseOverShapeTestEnabled)
{
InvalidateCurrentMouseOverShape();
}
}
...
}
protected void RenderScene()
{
...
try
{
...
CurrentScene.Render(D3dDevice);
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;
}
dsp._isMouseOverShapeTestEnabled = dsp.IsMouseOverShapeTestEnabled;
}
private bool _isMouseOverShapeTestEnabled;
#endregion
How to use it
Build and manipulate a scene
Create shapes
For demonstrating the use of the D3dScenePresenter
control for presenting and manipulating a scene, we create a window that presents some shapes and enables scene's manipulations with those shapes.
For creating a single shape, we can create a class that derives from D3dSingleShape
:
public class Pyramid : D3dSingleShape
{
}
and implement the InitDrawing
and the Draw
methods of it:
- For initializing the pyramid vertices' buffer we:
- Create the vertices' positions (in the scale of
-0.5
..0.5
for each axis):
Vector3 topPosition = new Vector3(0, 0.5f, 0);
Vector3 frontBottomLeftPosition = new Vector3(-0.5f, -0.5f, 0.5f);
Vector3 frontBottomRightPosition = new Vector3(0.5f, -0.5f, 0.5f);
Vector3 backBottomLeftPosition = new Vector3(-0.5f, -0.5f, -0.5f);
Vector3 backBottomRightPosition = new Vector3(0.5f, -0.5f, -0.5f);
- Calculate the normals for each pyramid's side:
float yNormal = (float) Math.Sqrt(0.2);
float sideDirectionNormal = (float) Math.Sqrt(0.8);
Vector3 frontNormal = new Vector3(0, yNormal, sideDirectionNormal);
Vector3 backNormal = new Vector3(0, yNormal, -sideDirectionNormal);
Vector3 leftNormal = new Vector3(-sideDirectionNormal, yNormal, 0);
Vector3 rightNormal = new Vector3(sideDirectionNormal, yNormal, 0);
Vector3 downNormal = new Vector3(0, -1, 0);
- Create the pyramid's vertices using those positions and normals:
private const int _verticesNumber = 18; private static CustomVertex.PositionNormal[] _pyramidVertices = null;
protected override void InitDrawing()
{
if (null != _pyramidVertices)
{
return;
}
lock (_shapeLoaderLock)
{
if (null == _pyramidVertices)
{
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);
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);
CustomVertex.PositionNormal[] vertices = new CustomVertex.PositionNormal[_verticesNumber];
SetPositionNormalTriangle(vertices, 0, frontBottomRightPosition, topPosition,
frontBottomLeftPosition, frontNormal);
SetPositionNormalTriangle(vertices, 3, frontBottomLeftPosition, topPosition, backBottomLeftPosition,
leftNormal);
SetPositionNormalTriangle(vertices, 6, backBottomRightPosition, topPosition,
frontBottomRightPosition, rightNormal);
SetPositionNormalTriangle(vertices, 9, backBottomLeftPosition, topPosition, backBottomRightPosition,
backNormal);
SetPositionNormalRectangle(vertices, 12, frontBottomRightPosition, frontBottomLeftPosition,
backBottomLeftPosition, backBottomRightPosition, downNormal);
_pyramidVertices = vertices;
}
}
}
private void SetPositionNormalTriangle(CustomVertex.PositionNormal[] verticesArray, int startIndex,
Vector3 firstPosition, Vector3 secondPosition, Vector3 thirdPosition, Vector3 normal)
{
verticesArray[startIndex].Position = firstPosition;
verticesArray[startIndex].Normal = normal;
verticesArray[startIndex + 1].Position = secondPosition;
verticesArray[startIndex + 1].Normal = normal;
verticesArray[startIndex + 2].Position = thirdPosition;
verticesArray[startIndex + 2].Normal = normal;
}
private void SetPositionNormalRectangle(CustomVertex.PositionNormal[] verticesArray, int startIndex,
Vector3 firstPosition, Vector3 secondPosition, Vector3 thirdPosition, Vector3 fourthPosition, Vector3 normal)
{
SetPositionNormalTriangle(verticesArray, startIndex, firstPosition, secondPosition, thirdPosition, normal);
SetPositionNormalTriangle(verticesArray, startIndex + 3, thirdPosition, fourthPosition, firstPosition,
normal);
}
- For drawing the pyramid, we just draw the created vertices:
protected override void Draw(Device d3dDevice)
{
if (null == _pyramidVertices)
{
return;
}
d3dDevice.VertexFormat = CustomVertex.PositionNormal.Format;
d3dDevice.DrawUserPrimitives(PrimitiveType.TriangleList, _verticesNumber / 3, _pyramidVertices);
}
For enabling picking on our shape, we have to implement the GetTrianglesPointsPositions
method too:
private const int _trianglesNumber = 6; private static TrianglePointsPositions[] _pyramidTriangles = null;
protected override IEnumerable<TrianglePointsPositions> GetTrianglesPointsPositions()
{
if (null != _pyramidTriangles)
{
return _pyramidTriangles;
}
if (null == _pyramidVertices)
{
return null;
}
lock (_shapeLoaderLock)
{
if (null == _pyramidTriangles)
{
TrianglePointsPositions[] triangles = new TrianglePointsPositions[_trianglesNumber];
for (int triangleInx = 0; triangleInx < _trianglesNumber; triangleInx++)
{
triangles[triangleInx] = new TrianglePointsPositions
{
Position1 = _pyramidVertices[triangleInx * 3].Position,
Position2 = _pyramidVertices[(triangleInx * 3) + 1].Position,
Position3 = _pyramidVertices[(triangleInx * 3) + 2].Position
};
}
_pyramidTriangles = triangles;
}
}
return _pyramidTriangles;
}
For creating a composed shape, we can:
- Create a class the derives from
D3dShape
.
- Add a
D3dComposedShape
data-member for holding the collection of the shapes.
- Implement the
Render
, AddPointIntersections
and the AddRayIntersections
methods, to use the methods of the D3dComposedShape
.
For demonstrating it, we add two more shapes:
House
:
public class House : D3dShape
{
private D3dComposedShape _composedHouse;
private D3dBox _houseBody;
private Pyramid _houseRoof;
public House()
{
BodyMaterial = DefaultMaterial;
RoofMaterial = DefaultMaterial;
_composedHouse = new D3dComposedShape();
_houseBody = new D3dBox
{
ScalingY = 0.5f,
TranslationY = -0.25f,
IsPickable = false
};
_composedHouse.Shapes.Add(_houseBody);
_houseRoof = new Pyramid
{
ScalingY = 0.5f,
TranslationY = 0.25f,
IsPickable = false
};
_composedHouse.Shapes.Add(_houseRoof);
}
#region D3dShape implementation
public override void Render(Device d3dDevice)
{
_composedHouse.IsVisible = IsVisible;
_houseBody.DefaultMaterial = BodyMaterial;
_houseRoof.DefaultMaterial = RoofMaterial;
_composedHouse.Parent = this;
_composedHouse.EnvironmentData = EnvironmentData;
_composedHouse.Render(d3dDevice);
}
public override void AddPointIntersections(Device d3dDevice, float surfaceX,
float surfaceY, List<IntersectResult> targetIntersectionsList)
{
_composedHouse.IsHitTestVisible = IsHitTestVisible;
_composedHouse.AddPointIntersections(d3dDevice, surfaceX, surfaceY, targetIntersectionsList);
}
public override void AddRayIntersections(Vector3 rayOrigin, Vector3 rayDirection,
List<IntersectResult> targetIntersectionsList)
{
_composedHouse.IsHitTestVisible = IsHitTestVisible;
_composedHouse.AddRayIntersections(rayOrigin, rayDirection, targetIntersectionsList);
}
#endregion
#region Properties
public Material BodyMaterial { get; set; }
public Material RoofMaterial { get; set; }
#endregion
}
Star
:
public class Star : D3dShape
{
protected D3dComposedShape _composedStar;
public Star()
{
_composedStar = new D3dComposedShape
{
RenderShapesParallelly = true
};
InitStar();
}
private void InitStar()
{
float pyramidBaseScaling = 0.2f;
float pyramidHeightScaling = (1 - pyramidBaseScaling) / 2;
float pyramidTranslateDistance = 0.5f - pyramidHeightScaling / 2;
Pyramid upPyramid = new Pyramid
{
ScalingX = pyramidBaseScaling,
ScalingY = pyramidHeightScaling,
ScalingZ = pyramidBaseScaling,
TranslationY = pyramidTranslateDistance,
IsPickable = false
};
_composedStar.Shapes.Add(upPyramid);
Pyramid downPyramid = new Pyramid
{
ScalingX = pyramidBaseScaling,
ScalingY = pyramidHeightScaling,
ScalingZ = pyramidBaseScaling,
RotationZ = (float)Math.PI,
TranslationY = -pyramidTranslateDistance,
IsPickable = false
};
_composedStar.Shapes.Add(downPyramid);
Pyramid rightPyramid = new Pyramid
{
ScalingX = pyramidBaseScaling,
ScalingY = pyramidHeightScaling,
ScalingZ = pyramidBaseScaling,
RotationZ = -(float)Math.PI / 2,
TranslationX = pyramidTranslateDistance,
IsPickable = false
};
_composedStar.Shapes.Add(rightPyramid);
Pyramid leftPyramid = new Pyramid
{
ScalingX = pyramidBaseScaling,
ScalingY = pyramidHeightScaling,
ScalingZ = pyramidBaseScaling,
RotationZ = (float)Math.PI / 2,
TranslationX = -pyramidTranslateDistance,
IsPickable = false
};
_composedStar.Shapes.Add(leftPyramid);
Pyramid frontPyramid = new Pyramid
{
ScalingX = pyramidBaseScaling,
ScalingY = pyramidHeightScaling,
ScalingZ = pyramidBaseScaling,
RotationX = (float)Math.PI / 2,
TranslationZ = pyramidTranslateDistance,
IsPickable = false
};
_composedStar.Shapes.Add(frontPyramid);
Pyramid backPyramid = new Pyramid
{
ScalingX = pyramidBaseScaling,
ScalingY = pyramidHeightScaling,
ScalingZ = pyramidBaseScaling,
RotationX = -(float)Math.PI / 2,
TranslationZ = -pyramidTranslateDistance,
IsPickable = false
};
_composedStar.Shapes.Add(backPyramid);
}
#region D3dShape implementation
public override void Render(Device d3dDevice)
{
_composedStar.IsVisible = IsVisible;
_composedStar.DefaultMaterial = DefaultMaterial;
_composedStar.Shapes.ForEach(s => s.DefaultMaterial = DefaultMaterial);
_composedStar.RenderAlsoIfOutOfView = RenderAlsoIfOutOfView;
_composedStar.Parent = this;
_composedStar.EnvironmentData = EnvironmentData;
_composedStar.Render(d3dDevice);
}
public override void AddPointIntersections(Device d3dDevice, float surfaceX,
float surfaceY, List<IntersectResult> targetIntersectionsList)
{
_composedStar.IsHitTestVisible = IsHitTestVisible;
_composedStar.AddPointIntersections(d3dDevice, surfaceX, surfaceY, targetIntersectionsList);
}
public override void AddRayIntersections(Vector3 rayOrigin, Vector3 rayDirection,
List<IntersectResult> targetIntersectionsList)
{
_composedStar.IsHitTestVisible = IsHitTestVisible;
_composedStar.AddRayIntersections(rayOrigin, rayDirection, targetIntersectionsList);
}
#endregion
}
Build the scene
For presenting our shapes, we:
- Add a
D3dScenePresenter
:
<Border BorderBrush="DarkGreen" BorderThickness="2" Margin="10">
<MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
D3dSurfaceWidth="2000"
D3dSurfaceHeight="2000" />
</Border>
- Create the scene using the shapes:
private D3dScene _scene;
private void InitScene()
{
_scene = new D3dScene
{
ClearColor = System.Drawing.Color.Gray
};
_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
});
_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
});
}
- Set the scene of the
D3dScenePresenter
, to the created scene:
private void InitScene()
{
...
mdxSceneHost.Scene = _scene;
}
Manipulate the scene
Using mouse operations
For manipulating our scene using mouse operations, we can use the default mouse-operations' settings as described in the following table:
Operation |
Mouse and Keyboard combination |
Zoom | Mouse middle button |
UniformZoom | Shift + Mouse middle button |
XyMove | Shift + Mouse left button |
ZMove | Shift + Mouse right button |
TargetXyRotate | Alt + Mouse right button |
TargetZRotate | Alt + Mouse left button |
CameraXyRotate | Control + Alt + Mouse right button |
CameraZRotate | Control + Alt + Mouse left button |
ZoomToRegion | Mouse left button + Mouse right button |
UniformZoomToRegion | Shift + Mouse left button + Mouse right button |
We can change those default settings by changing the MouseMoveOperations
list of the D3dScenePresenter
control.
Using manipulation buttons
For manipulating our scene using UI buttons, we can add buttons and bind the Command
of each button to the appropriate RoutedCommand
of the D3dScenePresenter
control. For example, here are the buttons for the UniformZoomCommand
command:
<TextBlock Text="Uniform Zoom: " Grid.Row="2"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
<RepeatButton Content="-" Grid.Column="1" Grid.Row="2" Margin="5"
Command="{x:Static MdxSceneControls:D3dScenePresenter.UniformZoomCommand}"
CommandParameter="1.1"
CommandTarget="{Binding ElementName=mdxSceneHost}"/>
<RepeatButton Content="+" Grid.Column="2" Grid.Row="2" Margin="5"
Command="{x:Static MdxSceneControls:D3dScenePresenter.UniformZoomCommand}"
CommandParameter="0.9"
CommandTarget="{Binding ElementName=mdxSceneHost}"/>
Zoom using the mouse-wheel
For enabling zooming using the mouse-wheel, we can add an event-handler to the D3dSurfaceMouseWheel
event:
<MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
D3dSurfaceWidth="2000"
D3dSurfaceHeight="2000"
D3dSurfaceMouseWheel="mdxSceneHost_D3dSurfaceMouseWheel" />
and, implement it to use the UniformZoomCommand
command:
private void mdxSceneHost_D3dSurfaceMouseWheel(object sender, D3dSurfaceMouseWheelEventArgs e)
{
float relativeZoomDelta = e.MouseWheelEventArgs.Delta/1000f;
float scalingFactor = 1 - relativeZoomDelta;
D3dScenePresenter.UniformZoomCommand.Execute(scalingFactor, mdxSceneHost);
}
Implement a mouse-over effect
For implementing a mouse-over effect, we can:
- Create a shape for marking the current mouse-over shape:
public class MarkBox : D3dBox
{
public MarkBox()
{
DefaultMaterial = new Material { DiffuseColor = new ColorValue(255, 255, 255, 0.5f) };
IsHitTestVisible = false;
IsPickable = false;
IsVisible = false;
}
#region MarkedShape
private D3dShape _markedShape;
public D3dShape MarkedShape
{
get { return _markedShape; }
set
{
_markedShape = value;
IsVisible = (null != _markedShape);
}
}
#endregion
public override Matrix GetActualWorldMatrix()
{
if (null == MarkedShape)
{
return base.GetActualWorldMatrix();
}
return Matrix.Scaling(1.1f, 1.1f, 1.1f) * MarkedShape.GetActualWorldMatrix();
}
protected override void Draw(Device d3dDevice)
{
Blend orgSourceBlend = d3dDevice.RenderState.SourceBlend;
Blend orgDestinationBlend = d3dDevice.RenderState.DestinationBlend;
bool orgAlphaBlendEnable = d3dDevice.RenderState.AlphaBlendEnable;
d3dDevice.RenderState.SourceBlend = Blend.SourceAlpha;
d3dDevice.RenderState.DestinationBlend = Blend.InvSourceAlpha;
d3dDevice.RenderState.AlphaBlendEnable = true;
base.Draw(d3dDevice);
d3dDevice.RenderState.SourceBlend = orgSourceBlend;
d3dDevice.RenderState.DestinationBlend = orgDestinationBlend;
d3dDevice.RenderState.AlphaBlendEnable = orgAlphaBlendEnable;
}
}
- Add this shape to the scene:
private void InitScene()
{
...
_pickMarkBox = new MarkBox();
_scene.Shapes.Add(_pickMarkBox);
mdxSceneHost.Scene = _scene;
}
- Enable mouse-over shape test:
<MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
D3dSurfaceWidth="2000"
D3dSurfaceHeight="2000"
D3dSurfaceMouseWheel="mdxSceneHost_D3dSurfaceMouseWheel"
IsMouseOverShapeTestEnabled="True" />
- Add an event-handler to the
MouseOverShapeChanged
event:
<MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
D3dSurfaceWidth="2000"
D3dSurfaceHeight="2000"
D3dSurfaceMouseWheel="mdxSceneHost_D3dSurfaceMouseWheel"
IsMouseOverShapeTestEnabled="True"
MouseOverShapeChanged="mdxSceneHost_MouseOverShapeChanged" />
- Implement it to set the current mouse-over shape as the marked shape:
private void mdxSceneHost_MouseOverShapeChanged(object sender, MouseOverShapeChangedRoutedEventArgs e)
{
D3dShape pickedShape = e.NewShape;
mdxSceneHost.PerformSceneAction(() => _pickMarkBox.MarkedShape = pickedShape);
}
The result is as follows:
Render heavy scenes
For demonstrating how we can use the parallel rendering and the out-of-view shapes filtering abilities of the scene, to improve the rendering performance, we create a window that presents some rotating stars and, enables controlling on the rendered stars' count and and how they are rendered.
For presenting our rotating stars scene, we:
- Add a
D3dScenePresenter
:
<Border Grid.Row="1"
BorderBrush="DarkBlue" BorderThickness="2" Margin="10">
<MdxSceneControls:D3dScenePresenter Name="mdxSceneHost"
D3dSurfaceWidth="2000"
D3dSurfaceHeight="2000" />
</Border>
- Extend the
Star
class (from the previous example), to hold an indication for its rotation axis:
public class ExtendedStar : Star
{
public int RotationAxis { get; set; }
}
- Build a scene using that
ExtendedStar
:
private ExtendedD3dScene _scene;
private D3dShape[] _shapes;
private int _maxShapesCount = 10000;
private int _currentShapesCount = 0;
private void InitShapes()
{
_shapes = new D3dShape[_maxShapesCount];
int starsBoxCellsPerAxis = (int)(Math.Sqrt(_maxShapesCount) * 1.5);
bool[, ,] shapesExistenceStatus = new bool[starsBoxCellsPerAxis, starsBoxCellsPerAxis, starsBoxCellsPerAxis];
System.Drawing.Color[] colors = new System.Drawing.Color[]
{
System.Drawing.Color.Red,
System.Drawing.Color.Green,
System.Drawing.Color.Blue,
System.Drawing.Color.Yellow,
System.Drawing.Color.Purple
};
Random rand = new Random(DateTime.Now.Millisecond);
for (int shapeInx = 0; shapeInx < _maxShapesCount; shapeInx++)
{
int xIndex = rand.Next(starsBoxCellsPerAxis);
int yIndex = rand.Next(starsBoxCellsPerAxis);
int zIndex = rand.Next(starsBoxCellsPerAxis);
while (shapesExistenceStatus[xIndex, yIndex, zIndex])
{
xIndex = rand.Next(starsBoxCellsPerAxis);
yIndex = rand.Next(starsBoxCellsPerAxis);
zIndex = rand.Next(starsBoxCellsPerAxis);
}
int colorIndex = rand.Next(colors.Length);
int rotationAxis = rand.Next(3);
float starSize = 30;
D3dShape currShape = new ExtendedStar
{
DefaultMaterial = new Material { Diffuse = colors[colorIndex] },
TranslationX = ((-(float)starsBoxCellsPerAxis) / 2 + xIndex) * starSize,
TranslationY = ((-(float)starsBoxCellsPerAxis) / 2 + yIndex) * starSize,
TranslationZ = ((-(float)starsBoxCellsPerAxis) / 2 + zIndex) * starSize,
ScalingX = starSize,
ScalingY = starSize,
ScalingZ = starSize,
RotationAxis = rotationAxis
};
_shapes[shapeInx] = currShape;
shapesExistenceStatus[xIndex, yIndex, zIndex] = true;
}
}
private void InitScene()
{
_scene = new D3dScene
{
ClearColor = System.Drawing.Color.Gray
};
_scene.Camera.ZFarPlane = 60000;
_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;
}
- Run a thread for updating the rotation of each
ExtendedStar
:
private Thread _updateThread;
private bool _continueUpdateThread;
private void UpdateScene()
{
if (_scene.Shapes.Count != _currentShapesCount)
{
_scene.Shapes.Clear();
_scene.Shapes.AddRange(
_shapes.Take(_currentShapesCount));
}
Parallel.ForEach(_scene.Shapes, shape =>
{
float rotationRadians = 0.1f;
ExtendedStar es = shape as ExtendedStar;
int rotateAxisNumber = null != es ? es.RotationAxis : 0;
switch (rotateAxisNumber)
{
case 0:
shape.RotationX += rotationRadians;
break;
case 1:
shape.RotationY += rotationRadians;
break;
case 2:
shape.RotationZ += rotationRadians;
break;
}
});
}
private void StartUpdateThread()
{
_continueUpdateThread = true;
_updateThread = new Thread(new ThreadStart(() =>
{
while (_continueUpdateThread)
{
mdxSceneHost.PerformSceneAction(UpdateScene);
Thread.Sleep(10);
}
}));
_updateThread.Start();
}
private void StopUpdateThread()
{
if (_updateThread != null)
{
_continueUpdateThread = false;
_updateThread.Join();
_updateThread = null;
}
}
For determining if we discard out-of-view shapes, we
- Create a static class for holding the current rendered stars' number:
public static class DemoContext
{
public static int RenderedStars { get; set; }
}
- Extend the
D3dScene
class, to reset the current rendered stars' number before rendering and, fire an event after rendering:
public class ExtendedD3dScene : D3dScene
{
public override void Render(Microsoft.DirectX.Direct3D.Device d3dDevice)
{
DemoContext.RenderedStars = 0;
base.Render(d3dDevice);
if (null != SceneRenderd)
{
SceneRenderd(DemoContext.RenderedStars);
}
}
public event Action<int> SceneRenderd;
}
- Override the
Render
method in the ExtendedStar
class, to increase the current rendered stars' number, for every rendered star:
public class ExtendedStar : Star
{
public int RotationAxis { get; set; }
public override void Render(Device d3dDevice)
{
base.Render(d3dDevice);
if (RenderAlsoIfOutOfView || !_composedStar.IsBoundingBoxOutOfView())
{
DemoContext.RenderedStars++;
}
}
}
- Add a
TextBlock
for indicating the current rendered stars' count:
<TextBlock Text="Rendered stars: " />
<TextBlock Name="txtRenderedStars" />
- Add an event-handler, for updating the indication of the current rendered stars' count, on every rendition:
private void InitScene()
{
_scene = new ExtendedD3dScene
{
ClearColor = System.Drawing.Color.Gray
};
_scene.Camera.ZFarPlane = 60000;
_scene.SceneRenderd +=
renderedStars =>
Dispatcher.Invoke(new ThreadStart(() => txtRenderedStars.Text = renderedStars.ToString()),
TimeSpan.FromMilliseconds(2000));
...
}
- Add a
CheckBox
for indicating if we filtering out-of-view shapes:
<CheckBox Name="cbDiscardOutOfView" Grid.Column="1"
Content="Discard out-of-view shapes"
IsChecked="{Binding DiscardOutOfView, Mode=TwoWay}" />
- Update the
RenderShapesAlsoIfOutOfView
property of our scene, according to that CheckBox
:
public bool DiscardOutOfView
{
get { return (bool)GetValue(DiscardOutOfViewProperty); }
set { SetValue(DiscardOutOfViewProperty, value); }
}
public static readonly DependencyProperty DiscardOutOfViewProperty =
DependencyProperty.Register("DiscardOutOfView", typeof(bool),
typeof(HeavySceneExampleWindow), new UIPropertyMetadata(false, OnDiscardOutOfViewChanged));
private static void OnDiscardOutOfViewChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
HeavySceneExampleWindow win = sender as HeavySceneExampleWindow;
if (win == null)
{
return;
}
bool isDiscardOutOfView = win.DiscardOutOfView;
D3dScene scene = win._scene;
win.mdxSceneHost.PerformSceneAction(() => scene.RenderShapesAlsoIfOutOfView = !isDiscardOutOfView);
}
For determining if the scene is rendered parallelly, we add a CheckBox
for indicating if we render our scene parallelly:
<CheckBox Name="cbRenderParallelly"
Content="Parallel rendering"
IsChecked="{Binding RenderParallelly, Mode=TwoWay}" />
and, update the RenderShapesParallelly
property of our scene, according to that CheckBox
:
public bool RenderParallelly
{
get { return (bool)GetValue(RenderParallellyProperty); }
set { SetValue(RenderParallellyProperty, value); }
}
public static readonly DependencyProperty RenderParallellyProperty =
DependencyProperty.Register("RenderParallelly", typeof(bool),
typeof(HeavySceneExampleWindow), new UIPropertyMetadata(false, OnRenderParallellyChanged));
private static void OnRenderParallellyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
HeavySceneExampleWindow win = sender as HeavySceneExampleWindow;
if (win == null)
{
return;
}
bool isRenderParallelly = win.RenderParallelly;
D3dScene scene = win._scene;
win.mdxSceneHost.PerformSceneAction(() => scene.RenderShapesParallelly = isRenderParallelly);
}
For controlling the count of the rendered stars, we a Slider
:
<Slider Name="starsCountSlider" Minimum="0" Maximum="1" Value="0.5"
ValueChanged="starsCountSlider_ValueChanged" />
and, update the current shapes count according to its value:
private void starsCountSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (!IsLoaded)
{
return;
}
_currentShapesCount = (int)(_maxShapesCount * starsCountSlider.Value);
txtTotalStars.Text = _currentShapesCount.ToString();
}
The result is as follows: