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