/// Copyright (c) 2021 Iiro Iivanainen, Harri Linna, Jere Pakkanen, Riikka Vilavaara /// /// Permission is hereby granted, free of charge, to any person obtaining /// a copy of this software and associated documentation files (the /// "Software"), to deal in the Software without restriction, including /// without limitation the rights to use, copy, modify, merge, publish, /// distribute, sublicense, and/or sell copies of the Software, and to /// permit persons to whom the Software is furnished to do so, subject to /// the following conditions: /// /// The above copyright notice and this permission notice shall be included /// in all copies or substantial portions of the Software. /// /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, /// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF /// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. /// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY /// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, /// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE /// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. /// /// @created 01.05.2021 /// @version 08.05.2021, ImageMagick /// @version 13.05.2021 /// @version 15.05.2021, Rotate using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Numerics; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using SharpGL; using SharpGL.WPF; using SharpGL.SceneGraph; using SharpGL.SceneGraph.Assets; using ImageMagick; namespace WPFOpenGL { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { public static Vector3 eulerAngle = new Vector3(0f,0f,0f); public List renderShapes = new List(); public List userShapes = new List(); public int selectedShape { get; set; } private Quad quadXY; private Quad quadXZ; private Quad quadYZ; private Planes currentPlane = Planes.XY; private Modes currentMode = Modes.defaultSpectator; private OrbitCamera camera; private Stopwatch sw = new Stopwatch(); // drawToPlane draws new quad // editDrawnShapes selects quad on mouse left button // defaultSpectator allows only camera movement public enum Modes { drawToPlane, editDrawnShapes, defaultSpectator } public enum Planes {XY, XZ, YZ } public MainWindow() { InitializeComponent(); // mostly mouse and keyboard input handlers Custom.OpenGLDraw += OpenGLControl_OpenGLDraw; Custom.OpenGLInitialized += OpenGLControl_OpenGLInitialized; Custom.Resized += OpenGLControl_Resized; Custom.MouseDown += OnMouseDown; Custom.MouseUp += OnMouseUp; Custom.MouseMove += OnMouseMove; Custom.MouseWheel += OnMouseWheel; Custom.KeyDown += OnKeyDown; Custom.DrawFPS = true; Custom.FrameRate = 20; Custom.Focusable = true; Custom.Focus(); currentMode = Modes.drawToPlane; currentPlane = Planes.XY; camera = new OrbitCamera(new Vector3(0f,0f,2.445f), new Vector3(0f,0f,-1f)); quadXY = new Quad(new Vector3(0f,0f,0f),new Vector3(0f,0f,0f),new Vector2(2f,2f)); quadXZ = new Quad(new Vector3(0f,0f,0f),new Vector3(90f,0f,0f),new Vector2(2f,2f)); quadYZ = new Quad(new Vector3(0f,0f,0f),new Vector3(0f,90f,0f),new Vector2(2f,2f)); renderShapes.Add(new Cube(new Vector3(0f,0f,0f), new Vector3(2f,2f,2f))); renderShapes.Add(quadXY); renderShapes.Add(quadXZ); renderShapes.Add(quadYZ); quadXY.SetOutlineColor(new Vector4(0f,0f,1f,1f)); quadXZ.SetOutlineColor(new Vector4(0f,1f,0f,1f)); quadYZ.SetOutlineColor(new Vector4(1f,0f,0f,1f)); quadXY.SetColor(new Vector4(0f,0f,1f,1f)); quadXZ.SetColor(new Vector4(0f,1f,0f,1f)); quadYZ.SetColor(new Vector4(1f,0f,0f,1f)); } private void OpenGLControl_OpenGLInitialized(object sender, OpenGLRoutedEventArgs args) { OpenGL openGL = args.OpenGL; openGL.Enable(OpenGL.GL_DEPTH_TEST); openGL.Enable(OpenGL.GL_LINE_SMOOTH); var texture = new Texture(); using (var stream = new MemoryStream()) { /* // example for reading quad's texture from a file using (var image = new MagickImage(some_filepath)) { image.Format = MagickFormat.Bmp; //image.Alpha(AlphaOption.Opaque); //image.Alpha(AlphaOption.Transparent); image.Alpha(AlphaOption.Set); image.ReduceNoise(); //image.AutoLevel(); image.Level(new Percentage(20), new Percentage(100)); image.Normalize(); image.Trim(); image.Write(stream); } var bitmap = new System.Drawing.Bitmap(stream); texture.Create(openGL, bitmap); quadXY.SetTexture(texture); quadXZ.SetTexture(texture); quadYZ.SetTexture(texture); */ } } private void OpenGLControl_OpenGLDraw(object sender, OpenGLRoutedEventArgs args) { OpenGL openGL = args.OpenGL; openGL.ClearColor(0.1f, 0.1f, 0.1f, 1f); openGL.Clear(OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT); openGL.MatrixMode(OpenGL.GL_MODELVIEW); openGL.LoadIdentity(); camera.LookAt(openGL); openGL.Rotate(eulerAngle.X, eulerAngle.Y, eulerAngle.Z); foreach (var shape in renderShapes) shape.Render(openGL); foreach (var shape in userShapes) shape.Render(openGL); openGL.Flush(); sw.Stop(); // stop watch for delta time float currentFrame = sw.ElapsedMilliseconds / 1000f; time.deltaTime = currentFrame - time.lastFrame; time.lastFrame = currentFrame; sw.Start(); // start watch for delta time } private void OpenGLControl_Resized(object sender, OpenGLRoutedEventArgs args) { // update camera perspective when window gets resized camera.Persepective(args.OpenGL); } private void OnClick(object sender, RoutedEvent e) { SetPlane(Planes.YZ); } public void SetPlane(Planes plane) { currentPlane = plane; } public void SetMode(Modes mode) { currentMode = mode; } private (float deltaTime, float lastFrame) time; private (Point start, Point end) mouseLeftDelta; private (Point start, Point end) mouseRightDelta; private (Vector3 start, Vector3 end, bool isDragOn) mouseLeftAreaSelection; private void OnKeyDown(object sender, KeyEventArgs e) { switch (e.Key) { case Key.D1: currentMode = Modes.defaultSpectator; Console.WriteLine("entered to default spectator mode"); break; case Key.D3: currentMode = Modes.drawToPlane; Console.WriteLine("entered to draw on the plane mode"); break; case Key.D4: currentMode = Modes.editDrawnShapes; Console.WriteLine("entered to select drawn shapes mode"); break; case Key.Up: // next slide on plane CurrentPlaneQuad().incerease(0.1f); break; case Key.Down: // previous slide on plane CurrentPlaneQuad().incerease(-0.1f); break; case Key.Z: eulerAngle = new Vector3(0f,0f,0f); currentPlane = Planes.XY; break; case Key.X: eulerAngle = new Vector3(0f,90f,0f); currentPlane = Planes.YZ; break; case Key.C: eulerAngle = new Vector3(90f,0f,0f); currentPlane = Planes.XZ; break; case Key.V: eulerAngle = new Vector3(45f,45f,0f); break; case Key.Delete: if (0 <= selectedShape && selectedShape < userShapes.Count) { userShapes.RemoveAt(selectedShape); } selectedShape = -1; break; } } private void OnMouseDown(object sender, MouseButtonEventArgs e) { if (e.ChangedButton == MouseButton.Left) { // initialize camera start position mouseLeftDelta.start = e.GetPosition(this); if (currentMode == Modes.drawToPlane) { float shortestDistance = float.MaxValue; Vector3 closestHitpoint = new Vector3(); bool onQuad = false; if (quadXY.IntersectRay(camera.GetPosition(), camera.ScreenToRay(PointToVector2(e.GetPosition(this)), eulerAngle), out Vector3 hit, out float distance)) { onQuad = true; if (distance < shortestDistance) { shortestDistance = distance; closestHitpoint = hit; currentPlane = Planes.XY; } } if (quadXZ.IntersectRay(camera.GetPosition(), camera.ScreenToRay(PointToVector2(e.GetPosition(this)), eulerAngle), out hit, out distance)) { onQuad = true; if (distance < shortestDistance) { shortestDistance = distance; closestHitpoint = hit; currentPlane = Planes.XZ; } } if (quadYZ.IntersectRay(camera.GetPosition(), camera.ScreenToRay(PointToVector2(e.GetPosition(this)), eulerAngle), out hit, out distance)) { onQuad = true; if (distance < shortestDistance) { onQuad = true; shortestDistance = distance; closestHitpoint = hit; currentPlane = Planes.YZ; } } if (onQuad) { userShapes.Add(new UserShape(closestHitpoint, closestHitpoint, currentPlane)); // initialize mouse area selection mouseLeftAreaSelection.start = closestHitpoint; mouseLeftAreaSelection.isDragOn = true; } } if (currentMode == Modes.editDrawnShapes) { float shortestDistance = float.MaxValue; selectedShape = -1; for (int i = 0; i < userShapes.Count; i++) { if (userShapes[i].isRaycastable == false) continue; if (userShapes[i].IntersectRay(camera.GetPosition(), camera.ScreenToRay(PointToVector2(e.GetPosition(this)), eulerAngle), out Vector3 hit, out float distance)) { if (distance < shortestDistance) { selectedShape = i; } } } if (0 <= selectedShape && selectedShape <= userShapes.Count) { userShapes[selectedShape].SetColor(new Vector4(1f,0f,1f,1f)); } } } if (e.ChangedButton == MouseButton.Right) { // initialize camera start position mouseRightDelta.start = e.GetPosition(this); } } private void OnMouseMove(object sender, MouseEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed) { // initialize mouse end position mouseLeftDelta.end = e.GetPosition(this); float deltaX = (float)(mouseLeftDelta.end.X - mouseLeftDelta.start.X) * time.deltaTime; float deltaY = (float)(mouseLeftDelta.end.Y - mouseLeftDelta.start.Y) * time.deltaTime; // update mouse start position mouseLeftDelta.start = mouseLeftDelta.end; if (currentMode == Modes.drawToPlane && mouseLeftAreaSelection.isDragOn) { if (RayCast.IntersectPlane(camera.GetPosition(), camera.ScreenToRay(PointToVector2(e.GetPosition(this)), eulerAngle), Vector3.Zero, CurrentPlaneNormal(), out Vector3 hitPlane, out float distancePlane, true)) { if (CurrentPlaneQuad().IntersectRay(camera.GetPosition(), camera.ScreenToRay(PointToVector2(e.GetPosition(this)), eulerAngle), out Vector3 hitQuad, out float distanceQuad)) { // initialize mouse area selection mouseLeftAreaSelection.end = hitQuad; userShapes[userShapes.Count - 1].SetStartEnd(mouseLeftAreaSelection.start, mouseLeftAreaSelection.end); } else { // initialize mouse area selection mouseLeftAreaSelection.end = CurrentPlaneQuad().ClosestQuadPoint(hitPlane, currentPlane); userShapes[userShapes.Count - 1].SetStartEnd(mouseLeftAreaSelection.start, mouseLeftAreaSelection.end); } } else { mouseLeftAreaSelection.isDragOn = false; } } } if (e.RightButton == MouseButtonState.Pressed) { // initialize camera end position mouseRightDelta.end = e.GetPosition(this); float deltaX = (float)(mouseRightDelta.end.X - mouseRightDelta.start.X) * time.deltaTime; float deltaY = (float)(mouseRightDelta.end.Y - mouseRightDelta.start.Y) * time.deltaTime; // update camera start position mouseRightDelta.start = mouseRightDelta.end; eulerAngle += deltaY * camera.RightDirection(); eulerAngle += deltaX * camera.UpDirection(); } } private void OnMouseWheel(object sender, MouseWheelEventArgs e) { float deltaZ = e.Delta * time.deltaTime; camera.ZoomOrbitCamera(deltaZ); } private void OnMouseUp(object sender, MouseButtonEventArgs e) { if (e.ChangedButton == MouseButton.Left) { mouseLeftAreaSelection.isDragOn = false; } } private static Vector2 PointToVector2(Point point) { return new Vector2((float)point.X, (float)point.Y); } private Quad CurrentPlaneQuad() { if (currentPlane == Planes.XY) return quadXY; else if (currentPlane == Planes.XZ) return quadXZ; else return quadYZ; } private Vector3 CurrentPlaneNormal() { if (currentPlane == Planes.XY) return -Vector3.UnitZ; else if (currentPlane == Planes.XZ) return -Vector3.UnitY; else return -Vector3.UnitX; } } }