using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Media.Media3D; using System.Windows.Shapes; using Path = System.IO.Path; namespace HelixToolkit { public static class Viewport3DHelper { #region Camera/Viewport info methods based on Charles Petzold's book "3D programming for Windows" #region CameraInfo /// /// Obtains the view transform matrix for a camera. (see page 327) /// /// Camera to obtain the ViewMatrix for /// A Matrix3D object with the camera view transform matrix, or a Matrix3D with all zeros if the "camera" is null. /// if the 'camera' is neither of type MatrixCamera nor ProjectionCamera. public static Matrix3D GetViewMatrix(Camera camera) { if (camera == null) { throw new ArgumentNullException("camera"); } if (camera is MatrixCamera) { return (camera as MatrixCamera).ViewMatrix; } if (camera is ProjectionCamera) { // Reflector on: ProjectionCamera.CreateViewMatrix var projcam = camera as ProjectionCamera; var zAxis = -projcam.LookDirection; zAxis.Normalize(); var xAxis = Vector3D.CrossProduct(projcam.UpDirection, zAxis); xAxis.Normalize(); var yAxis = Vector3D.CrossProduct(zAxis, xAxis); var pos = (Vector3D) projcam.Position; return new Matrix3D( xAxis.X, yAxis.X, zAxis.X, 0, xAxis.Y, yAxis.Y, zAxis.Y, 0, xAxis.Z, yAxis.Z, zAxis.Z, 0, -Vector3D.DotProduct(xAxis, pos), -Vector3D.DotProduct(yAxis, pos), -Vector3D.DotProduct(zAxis, pos), 1); } throw new ApplicationException("unknown camera type"); } /// /// Projection matrix, page 327-331 /// /// /// /// public static Matrix3D GetProjectionMatrix(Camera camera, double aspectRatio) { if (camera == null) { throw new ArgumentNullException("camera"); } if (camera is MatrixCamera) { return (camera as MatrixCamera).ProjectionMatrix; } if (camera is OrthographicCamera) { var orthocam = camera as OrthographicCamera; double xScale = 2/orthocam.Width; double yScale = xScale*aspectRatio; double zNear = orthocam.NearPlaneDistance; double zFar = orthocam.FarPlaneDistance; // Hey, check this out! if (Double.IsPositiveInfinity(zFar)) zFar = 1E10; return new Matrix3D(xScale, 0, 0, 0, 0, yScale, 0, 0, 0, 0, 1/(zNear - zFar), 0, 0, 0, zNear/(zNear - zFar), 1); } if (camera is PerspectiveCamera) { var perscam = camera as PerspectiveCamera; // The angle-to-radian formula is a little off because only // half the angle enters the calculation. double xScale = 1/Math.Tan(Math.PI*perscam.FieldOfView/360); double yScale = xScale*aspectRatio; double zNear = perscam.NearPlaneDistance; double zFar = perscam.FarPlaneDistance; double zScale = (zFar == double.PositiveInfinity ? -1 : (zFar/(zNear - zFar))); double zOffset = zNear*zScale; return new Matrix3D(xScale, 0, 0, 0, 0, yScale, 0, 0, 0, 0, zScale, -1, 0, 0, zOffset, 0); } throw new ApplicationException("unknown camera type"); } /// /// Get the combined view and projection transform /// /// /// /// public static Matrix3D GetTotalTransform(Camera camera, double aspectRatio) { var m = Matrix3D.Identity; if (camera == null) { throw new ArgumentNullException("camera"); } if (camera.Transform != null) { var cameraTransform = camera.Transform.Value; if (!cameraTransform.HasInverse) { throw new ApplicationException("camera transform has no inverse"); } cameraTransform.Invert(); m.Append(cameraTransform); } m.Append(GetViewMatrix(camera)); m.Append(GetProjectionMatrix(camera, aspectRatio)); return m; } public static Matrix3D GetInverseTransform(Camera cam, double aspectRatio) { var m = GetTotalTransform(cam, aspectRatio); if (!m.HasInverse) { throw new ApplicationException("camera transform has no inverse"); } m.Invert(); return m; } #endregion #region ViewportInfo public static Matrix3D GetTotalTransform(Viewport3DVisual vis) { var m = GetCameraTransform(vis); m.Append(GetViewportTransform(vis)); return m; } public static Matrix3D GetTotalTransform(Viewport3D viewport) { var matx = GetCameraTransform(viewport); matx.Append(GetViewportTransform(viewport)); return matx; } public static Matrix3D GetCameraTransform(Viewport3DVisual vis) { return GetTotalTransform(vis.Camera, vis.Viewport.Size.Width/vis.Viewport.Size.Height); } public static Matrix3D GetCameraTransform(Viewport3D viewport) { return GetTotalTransform(viewport.Camera, viewport.ActualWidth/viewport.ActualHeight); } public static Matrix3D GetViewportTransform(Viewport3DVisual vis) { return new Matrix3D(vis.Viewport.Width/2, 0, 0, 0, 0, -vis.Viewport.Height/2, 0, 0, 0, 0, 1, 0, vis.Viewport.X + vis.Viewport.Width/2, vis.Viewport.Y + vis.Viewport.Height/2, 0, 1); } public static Matrix3D GetViewportTransform(Viewport3D viewport) { return new Matrix3D(viewport.ActualWidth/2, 0, 0, 0, 0, -viewport.ActualHeight/2, 0, 0, 0, 0, 1, 0, viewport.ActualWidth/2, viewport.ActualHeight/2, 0, 1); } public static Point Point3DtoPoint2D(Viewport3D viewport, Point3D point) { var matrix = GetTotalTransform(viewport); var pointTransformed = matrix.Transform(point); var pt = new Point(pointTransformed.X, pointTransformed.Y); return pt; } public static Ray3D Point2DtoRay3D(Viewport3D viewport, Point ptIn) { Point3D pointNear, pointFar; if (!Point2DtoPoint3D(viewport, ptIn, out pointNear, out pointFar)) return null; return new Ray3D(pointNear, pointFar); } public static bool Point2DtoPoint3D(Viewport3D viewport, Point ptIn, out Point3D pointNear, out Point3D pointFar) { pointNear = new Point3D(); pointFar = new Point3D(); var pointIn = new Point3D(ptIn.X, ptIn.Y, 0); var matrixViewport = GetViewportTransform(viewport); var matrixCamera = GetCameraTransform(viewport); if (!matrixViewport.HasInverse) return false; if (!matrixCamera.HasInverse) return false; matrixViewport.Invert(); matrixCamera.Invert(); var pointNormalized = matrixViewport.Transform(pointIn); pointNormalized.Z = 0.01; pointNear = matrixCamera.Transform(pointNormalized); pointNormalized.Z = 0.99; pointFar = matrixCamera.Transform(pointNormalized); return true; } #endregion #endregion #region Helper methods based on Eric Sink's twelve days of WPF // The Twelve Days of WPF 3D // http://www.ericsink.com/wpf3d/index.html // http://www.ericsink.com/wpf3d/3_Bitmap.html public static RenderTargetBitmap RenderBitmap(Viewport3D view, Brush background) { var bmp = new RenderTargetBitmap( (int) view.ActualWidth, (int) view.ActualHeight, 96, 96, PixelFormats.Pbgra32); // erase background var vRect = new Rectangle { Width = view.ActualWidth, Height = view.ActualHeight, Fill = background }; vRect.Arrange(new Rect(0, 0, vRect.Width, vRect.Height)); bmp.Render(vRect); bmp.Render(view); return bmp; } public static RenderTargetBitmap RenderBitmap(Viewport3D view, double width, double height, Brush background) { double w = view.Width; double h = view.Height; ResizeAndArrange(view, width, height); var rtb = RenderBitmap(view, background); ResizeAndArrange(view, w, h); return rtb; } public static void Copy(Viewport3D view) { Clipboard.SetImage(RenderBitmap(view, Brushes.White)); } public static void Copy(Viewport3D view, double width, double height, Brush background) { Clipboard.SetImage(RenderBitmap(view, width, height, background)); } public static void SaveBitmap(Viewport3D view, string fileName) { var bmp = RenderBitmap(view, Brushes.White); BitmapEncoder encoder; string ext = Path.GetExtension(fileName); switch (ext.ToLower()) { case ".jpg": var jpg = new JpegBitmapEncoder(); jpg.Frames.Add(BitmapFrame.Create(bmp)); encoder = jpg; break; case ".png": var png = new PngBitmapEncoder(); png.Frames.Add(BitmapFrame.Create(bmp)); encoder = png; break; default: throw new InvalidOperationException("Not supported file format."); } using (Stream stm = File.Create(fileName)) { encoder.Save(stm); } } public static void ResizeAndArrange(Viewport3D view, double width, double height) { view.Width = width; view.Height = height; if (double.IsNaN(width) || double.IsNaN(height)) return; view.Measure(new Size(width, height)); view.Arrange(new Rect(0, 0, width, height)); } // http://www.ericsink.com/wpf3d/7_XAML.html public static void CopyXaml(Viewport3D view) { Clipboard.SetText(XamlWriter.Save(view)); } // http://www.ericsink.com/wpf3d/A_AutoZoom.html /* public static Rect Get2DBoundingBox(Viewport3D vp) { bool bOK; Viewport3DVisual vpv = VisualTreeHelper.GetParent( vp.Children[0]) as Viewport3DVisual; Matrix3D m = _3DTools.MathUtils.TryWorldToViewportTransform(vpv, out bOK); bool bFirst = true; Rect r = new Rect(); foreach (Visual3D v3d in vp.Children) { if (v3d is ModelVisual3D) { ModelVisual3D mv3d = (ModelVisual3D)v3d; if (mv3d.Content is GeometryModel3D) { GeometryModel3D gm3d = (GeometryModel3D)mv3d.Content; if (gm3d.Geometry is MeshGeometry3D) { MeshGeometry3D mg3d = (MeshGeometry3D)gm3d.Geometry; foreach (Point3D p3d in mg3d.Positions) { Point3D pb = m.Transform(p3d); Point p2d = new Point(pb.X, pb.Y); if (bFirst) { r = new Rect(p2d, new Size(1, 1)); bFirst = false; } else { r.Union(p2d); } } } } } } return r; } */ public static void Print(Viewport3D vp, string description) { var dlg = new PrintDialog(); if (dlg.ShowDialog().GetValueOrDefault()) { dlg.PrintVisual(vp, description); } } #endregion public static void Export(Viewport3D view, string fileName) { string ext = Path.GetExtension(fileName).ToLower(); switch (ext) { case ".jpg": case ".png": SaveBitmap(view, fileName); break; case ".xaml": ExportXaml(view, fileName); break; case ".xml": ExportKerkythea(view, fileName); break; case ".obj": ExportObj(view, fileName); break; case ".x3d": ExportX3d(view, fileName); break; default: throw new InvalidOperationException("Not supported file format."); } } private static void ExportX3d(Viewport3D view, string fileName) { var e = new X3DExporter(fileName); e.Export(view); e.Close(); } private static void ExportObj(Viewport3D view, string fileName) { var e = new ObjExporter(fileName); e.Export(view); e.Close(); } private static void ExportKerkythea(Viewport3D view, string fileName) { ExportKerkythea(view, fileName, Colors.White, (int) view.ActualWidth, (int) view.ActualHeight); } private static void ExportKerkythea(Viewport3D view, string fileName, Color background, int width, int height) { var e = new KerkytheaExporter(fileName) {Width = width, Height = height, BackgroundColor = background}; e.Export(view); e.Close(); } private static void ExportXaml(Viewport3D view, string fileName) { var e = new XamlExporter(fileName); e.Export(view); e.Close(); } /// /// Get all lights in the Viewport3D /// /// /// public static Light[] GetLights(Viewport3D viewport) { var models = SearchFor(viewport.Children); return models.Select(m => m as Light).ToArray(); } /// /// Recursive search in a collection for objects of given type T /// /// /// /// public static List SearchFor(IEnumerable collection) { var output = new List(); SearchFor(collection, typeof (T), output); return output; } /// /// Recursive search for an object of a given type /// /// /// /// private static void SearchFor(IEnumerable collection, Type type, ICollection output) { foreach (var visual in collection) { var modelVisual = visual as ModelVisual3D; if (modelVisual != null) { var model = modelVisual.Content; if (model != null) { if (type.IsAssignableFrom(model.GetType())) output.Add(model); // recursive SearchFor(modelVisual.Children, type, output); } var modelGroup = model as Model3DGroup; if (modelGroup != null) { SearchFor(modelGroup.Children, type, output); } } } } private static void SearchFor(IEnumerable collection, Type type, ICollection output) { foreach (var model in collection) { if (type.IsAssignableFrom(model.GetType())) output.Add(model); var group = model as Model3DGroup; if (group != null) { SearchFor(group.Children, type, output); } } } /// /// Find the Visual3D that is nearest given a 2D position in the viewport /// /// /// /// public static Visual3D FindNearestVisual(Viewport3D viewport, Point position) { Point3D p; Vector3D n; DependencyObject obj; if (FindNearest(viewport, position, out p, out n, out obj)) return obj as Visual3D; return null; } /// /// Find the coordinates of the nearest point given a 2D position in the viewport /// /// /// /// public static Point3D? FindNearestPoint(Viewport3D viewport, Point position) { Point3D p; Vector3D n; DependencyObject obj; if (FindNearest(viewport, position, out p, out n, out obj)) return p; return null; } public static bool FindNearest(Viewport3D viewport, Point position, out Point3D point, out Vector3D normal, out DependencyObject visual) { var camera = viewport.Camera as ProjectionCamera; if (camera == null) { point = new Point3D(); normal = new Vector3D(); visual = null; return false; } var hitParams = new PointHitTestParameters(position); double minimumDistance = double.MaxValue; var nearestPoint = new Point3D(); var nearestNormal = new Vector3D(); DependencyObject nearestObject = null; VisualTreeHelper.HitTest(viewport, null, delegate(HitTestResult hit) { var rayHit = hit as RayMeshGeometry3DHitTestResult; if (rayHit != null) { var mesh = rayHit.MeshHit; if (mesh != null) { var p1 = mesh.Positions[rayHit.VertexIndex1]; var p2 = mesh.Positions[rayHit.VertexIndex2]; var p3 = mesh.Positions[rayHit.VertexIndex3]; double x = p1.X*rayHit.VertexWeight1 + p2.X*rayHit.VertexWeight2 + p3.X*rayHit.VertexWeight3; double y = p1.Y*rayHit.VertexWeight1 + p2.Y*rayHit.VertexWeight2 + p3.Y*rayHit.VertexWeight3; double z = p1.Z*rayHit.VertexWeight1 + p2.Z*rayHit.VertexWeight2 + p3.Z*rayHit.VertexWeight3; // point in local coordinates var p = new Point3D(x, y, z); // transform to global coordinates // first transform the Model3D hierarchy var t2 = GetTransform(rayHit.VisualHit, rayHit.ModelHit); if (t2 != null) p = t2.Transform(p); // then transform the Visual3D hierarchy up to the Viewport3D ancestor var t = GetTransform(viewport, rayHit.VisualHit); if (t != null) p = t.Transform(p); double distance = (camera.Position - p).LengthSquared; if (distance < minimumDistance) { minimumDistance = distance; nearestPoint = p; nearestNormal = Vector3D.CrossProduct(p2 - p1, p3 - p1); nearestObject = hit.VisualHit; } } } return HitTestResultBehavior.Continue; }, hitParams); point = nearestPoint; visual = nearestObject; normal = nearestNormal; if (minimumDistance == double.MaxValue) return false; normal.Normalize(); return true; } public class HitResult { public Vector3D Normal { get; set; } public Point3D Position { get; set; } public double Distance { get; set; } public RayMeshGeometry3DHitTestResult RayHit { get; set; } public MeshGeometry3D Mesh { get { return RayHit.MeshHit; } } public Model3D Model { get { return RayHit.ModelHit; } } public Visual3D Visual { get { return RayHit.VisualHit; } } } /// /// Finds the hits for a given 2D viewport position. /// /// The viewport. /// The position. /// List of hits, sorted with the nearest hit first. public static List FindHits(Viewport3D viewport, Point position) { var camera = viewport.Camera as ProjectionCamera; if (camera == null) return null; var result = new List(); HitTestResultCallback callback = hit => { var rayHit = hit as RayMeshGeometry3DHitTestResult; if (rayHit != null) { if (rayHit.MeshHit != null) { var p = GetGlobalHitPosition(rayHit, viewport); var nn = GetNormalHit(rayHit); var n = nn.HasValue ? nn.Value : new Vector3D(0, 0, 1); result.Add(new HitResult { Distance = (camera.Position - p).Length, RayHit = rayHit, Normal = n, Position = p }); } } return HitTestResultBehavior.Continue; }; var hitParams = new PointHitTestParameters(position); VisualTreeHelper.HitTest(viewport, null, callback, hitParams); result.OrderBy(k => k.Distance); return result; } /// /// Gets the hit position transformed to global (viewport) coordinates. /// /// The hit structure. /// The viewport. /// private static Point3D GetGlobalHitPosition(RayMeshGeometry3DHitTestResult rayHit, Viewport3D viewport) { var p = rayHit.PointHit; // first transform the Model3D hierarchy var t2 = GetTransform(rayHit.VisualHit, rayHit.ModelHit); if (t2 != null) p = t2.Transform(p); // then transform the Visual3D hierarchy up to the Viewport3D ancestor var t = GetTransform(viewport, rayHit.VisualHit); if (t != null) p = t.Transform(p); return p; } /// /// Gets the normal for a hit test result. /// /// The ray hit. /// private static Vector3D? GetNormalHit(RayMeshGeometry3DHitTestResult rayHit) { if ( (rayHit.MeshHit.Normals == null) || (rayHit.MeshHit.Normals.Count < 1) ) return null; return rayHit.MeshHit.Normals[rayHit.VertexIndex1]*rayHit.VertexWeight1 + rayHit.MeshHit.Normals[rayHit.VertexIndex2]*rayHit.VertexWeight2 + rayHit.MeshHit.Normals[rayHit.VertexIndex3]*rayHit.VertexWeight3; } public static Viewport3D GetViewport(Visual3D visual) { DependencyObject parent = visual; while (parent != null) { var vp = parent as Viewport3DVisual; if (vp!=null) { return vp.Parent as Viewport3D; } parent = VisualTreeHelper.GetParent(parent); } return null; } /// /// Get the total transform of a Visual3D /// /// /// /// public static GeneralTransform3D GetTransform(Viewport3D viewport, Visual3D visual) { if (visual == null) return null; foreach (var ancestor in viewport.Children) { if (visual.IsDescendantOf(ancestor)) { var g = new GeneralTransform3DGroup(); // this includes the visual.Transform var ta = visual.TransformToAncestor(ancestor); if (ta != null) g.Children.Add(ta); // add the transform of the top-level ancestor g.Children.Add(ancestor.Transform); return g; } } return visual.Transform; } /// /// Gets the transform from the specified Visual3D to the Model3D. /// /// The source visual. /// The target model. /// public static GeneralTransform3D GetTransform(Visual3D visual, Model3D model) { var mv = visual as ModelVisual3D; if (mv != null) return GetTransform(mv.Content, model, Transform3D.Identity); return null; } private static GeneralTransform3D GetTransform(Model3D current, Model3D model, Transform3D parentTransform) { var currentTransform = Transform3DHelper.CombineTransform(current.Transform, parentTransform); if (current == model) return currentTransform; var mg = current as Model3DGroup; if (mg != null) { foreach (var m in mg.Children) { var result = GetTransform(m, model, currentTransform); if (result != null) return result; } } return null; } } }