Search code examples
c#openglcamerasharpgl

SharpGL (OpenGL in C#) Solar System - Camera position is Calculated Wrong


I am currently learning OpenGL and I am trying to write simple solar system application similar to the one in tutorial, yet for some reason camera behavior is really weird and I am not sure what is causing this. I want my camera to look at the sun, but all I am getting are some really weird angles of planets or nothing at all, it might not even be pure camera problem. Can somebody tell me what exactly I am doing wrong? I would appreciate some code. If someone is willing to help and prefer to check application instead of reading code here, link is added below.

The camera here will be as close to the tutorial as possible (FPS), but I got also dragging/scrolling system instead of this.

public class Camera
{
    private static float eyeX, eyeY, eyeZ;
    private static float centerX, centerY, centerZ;
    private const float movingSpeed = 0.3f;
    private const float rotationSpeed = 0.25f;
    private static double i, j, k;

    public static float Height { get; set; }
    public static float Slope { get; set; }

    public void InitCamera()
    {
        eyeX = 0f;
        eyeY = 15f;
        eyeZ = 25f;
        centerX = 0;
        centerY = 2;
        centerZ = 0;
        Look();
    }

    public void Look()
    {
        Gl.MatrixMode(OpenGL.GL_MODELVIEW);
        Gl.LoadIdentity();
        Gl.LookAt(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, 0, 1, 0);
    }

    public void UpdateDirVector()
    {
        i = -Math.Sin(((double)Slope).ToRadians()); 
        j = Math.Sin(((double)Height).ToRadians());  
        k = Math.Cos(((double)Slope).ToRadians());     

        centerX = eyeX - (float)i;
        centerY = eyeY - (float)j;
        centerZ = eyeZ - (float)k;
    }

    public static void CenterMouse()
    {
        if (GlCenter == null)
            return;
        var pos = (Point) GlCenter;
        WinApi.SetCursorPos((int)Math.Round(pos.X), (int)Math.Round(pos.Y));
    }

    public void Update(int pressedButton)
    {
        if (GlCenter == null)
            return;
        var pos = (Point)GlCenter;
        var halfHeight = GlHeight / 2;
        var halfWidth = GlWidth / 2;

        var position = new Pointer();
        WinApi.GetCursorPos(ref position);

        var diffX = (float)pos.X - position.x;
        var diffY = (float)pos.Y - position.y;

        if (position.y < halfHeight)
            Height -= rotationSpeed * diffY;
        else if (position.y > halfHeight)
            Height += rotationSpeed * -diffY;
        if (position.x < halfWidth)
            Slope += rotationSpeed * -diffX;
        else if (position.x > halfWidth)
            Slope -= rotationSpeed * diffX;
        UpdateDirVector();
        CenterMouse();

        if (pressedButton == 1) // LPM
        {
            eyeX -= (float)i * movingSpeed;
            eyeY -= (float)j * movingSpeed;
            eyeZ -= (float)k * movingSpeed;
        }
        else if (pressedButton == -1) // PPM
        {
            eyeX += (float)i * movingSpeed;
            eyeY += (float)j * movingSpeed;
            eyeZ += (float)k * movingSpeed;
        }

        Look();
    }
}

Planet.cs:

public class Planet
{
    private readonly PlanetTypes _planetType;
    private readonly Position _position;
    private float _orbitAngle;
    private readonly float _sizeRadius;
    private readonly float _velocity;
    private readonly string _texturePath;

    private uint _list;
    private float _rotationAngle;

    public Planet(float radius, PlanetTypes planetType, Position position, string texturePath, bool hasMoon)
    {
        _sizeRadius = radius;
        _planetType = planetType;
        _position = position;
        _orbitAngle = Rng.Next(360);
        _velocity = (float)Rng.NextDouble() * 0.3f;
        _texturePath = texturePath;
    }

    public void Create()
    {
        var quadric = Gl.NewQuadric(); 
        Gl.QuadricNormals(quadric, OpenGL.GLU_SMOOTH);
        Gl.QuadricTexture(quadric, (int) OpenGL.GL_TRUE);

        _list = Gl.GenLists(1);
        Gl.NewList(_list, OpenGL.GL_COMPILE);
        Gl.PushMatrix();
        Gl.Rotate(270, 1, 0, 0);
        Gl.Sphere(quadric, _sizeRadius, 32, 32); 
        Gl.PopMatrix();
        Gl.EndList();
    }

    public void DrawOrbit()
    {
        Gl.Begin(OpenGL.GL_LINE_STRIP);
        for (var i = 0; i <= 360; i++)
            Gl.Vertex(_position.X * (float)Math.Sin(i * Math.PI / 180), 0, _position.X * (float)Math.Cos(i * Math.PI / 180));
        Gl.End(); 
    }

    public void Draw()
    {
        DrawOrbit(); 

        LoadTexture($"{_texturesPath}{_texturePath}");

        Gl.PushMatrix();
        _orbitAngle += _velocity;
        _rotationAngle += 0.6f;
        Gl.Rotate(_orbitAngle, 0, 1, 0);
        Gl.Translate(-_position.X, -_position.Y, -_position.Z);

        Gl.Rotate(_rotationAngle, 0, 1, 0);

        Gl.CallList(_list);

        Gl.PopMatrix();
    } 
}

Sun.cs

public class Sun
{
    private uint _list;
    private float _rotation;
    private readonly string _texturePath;

    public Sun(string texturePath)
    {
        _texturePath = texturePath;
    }

    public void Create()
    {
        var quadratic = Gl.NewQuadric();
        Gl.QuadricNormals(quadratic, OpenGL.GLU_SMOOTH);
        Gl.QuadricTexture(quadratic, (int)OpenGL.GL_TRUE);

        _list = Gl.GenLists(1);
        Gl.NewList(_list, OpenGL.GL_COMPILE);
        Gl.PushMatrix();
        Gl.Rotate(90, 1, 0, 0);
        Gl.Sphere(quadratic, 3, 32, 32);
        Gl.PopMatrix();
        Gl.EndList();
    }

    public void Draw()
    {
        LoadTexture($"{_texturesPath}{_texturePath}");
        Gl.PushMatrix();
        _rotation += 0.05f;
        Gl.Rotate(_rotation, 0, 1, 0);
        Gl.CallList(_list);
        Gl.PopMatrix();
    } 
}

Stars.cs

public class Stars
{
    private readonly List<Position> starPositions = new List<Position>();

    public void CreateStars(int amount)
    {
        var count = 0;
        while (count <= amount)
        {
            var p = default(Position);
            p.X = Rng.Next(110) * (float)Math.Pow(-1, Rng.Next());
            p.Z = Rng.Next(110) * (float)Math.Pow(-1, Rng.Next());
            p.Y = Rng.Next(110) * (float)Math.Pow(-1, Rng.Next());

            if (!(Math.Pow(Math.Pow(p.X, 2) + Math.Pow(p.Y, 2) + Math.Pow(p.Z, 2), 1 / 3f) > 15))
                continue;
            starPositions.Add(p);
            count++;
        }
    }

    public void Draw()
    {
        Gl.Begin(OpenGL.GL_POINTS);
        Gl.Color(1, 1, 1);
        Gl.PointSize(3); 
        foreach (var starPos in starPositions)
            Gl.Vertex(starPos.X, starPos.Y, starPos.Z);   
        Gl.End(); 
    }
}

SolarSystem.cs (basically a collection of everything above, enums aren't necessary, I left them cause they might be useful in the future)

public class SolarSystem
{
    public static Random Rng { get; } = new Random();
    private readonly Stars _stars;
    private readonly Sun _sun;
    private readonly List<Planet> _planets;

    public SolarSystem()
    {
        Camera = new Camera();
        _stars = new Stars();
        _sun = new Sun("sun.bmp");
        _planets = new List<Planet>();
    }

    public void CreateScene()
    {
        _planets.Add(new Planet(0.5f, PlanetTypes.Mercury, new Position(5, 0, 0), "mercury.bmp", false)); // tylko tutaj pliki, wszedzie indziej przekształcone na .bmp
        _planets.Add(new Planet(0.7f, PlanetTypes.Venus, new Position(11, 0, 0), "venus.bmp", false));
        _planets.Add(new Planet(1, PlanetTypes.Earth, new Position(15, 0, 0), "earth.bmp", true));
        _planets.Add(new Planet(1, PlanetTypes.Mars, new Position(22, 0, 0), "mars.bmp", false));
        _planets.Add(new Planet(1.5f, PlanetTypes.Jupiter, new Position(28, 0, 0), "jupiter.bmp", false));
        _planets.Add(new Planet(1.2f, PlanetTypes.Saturn, new Position(35, 0, 0), "saturn.bmp", false));
        _planets.Add(new Planet(1.2f, PlanetTypes.Uranus, new Position(41, 0, 0), "uranus.bmp", false));
        _planets.Add(new Planet(1.2f, PlanetTypes.Neptune, new Position(51, 0, 0), "neptune.bmp", false));
        _planets.Add(new Planet(1.2f, PlanetTypes.Pluto, new Position(60, 0, 0), "pluto.bmp", false));
        _stars.CreateStars(500);
        _sun.Create();
        foreach (var planet in _planets)
            planet.Create();  
    }

    public Camera Camera { get; }

    public void DrawScene()
    {  
        _stars.Draw();
        _sun.Draw();
        foreach (var planet in _planets)
            planet.Draw();
    }

    public enum PlanetTypes
    {
        Mercury,
        Venus,
        Earth,
        Mars,
        Jupiter,
        Saturn,
        Neptune,
        Uranus,
        Pluto
    }
}

API (including pointer position translation, because only this way I was able to center cursor, it will be removed with dragging camera system)):

public static class WinApi
{
    [DllImport("GDI32.dll")]
    public static extern void SwapBuffers(uint hdc);
    [DllImport("user32.dll")]
    public static extern void SetCursorPos(int x, int y);
    [DllImport("user32.dll")]
    public static extern void GetCursorPos(ref Pointer point);
    [DllImport("user32.dll")]
    public static extern IntPtr GetWindowDC(IntPtr hWnd);

    [StructLayout(LayoutKind.Sequential)]
    private struct POINT
    {
        public int X;
        public int Y;
    }

    [DllImport("User32", EntryPoint = "ClientToScreen", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool ClientToScreen(
        IntPtr hWnd,
        ref POINT pt);

    [EnvironmentPermission(SecurityAction.LinkDemand, Unrestricted = true)]
    public static Point? TransformToScreen(
        Point point,
        Visual relativeTo)
    {
        var hwndSource = PresentationSource.FromVisual(relativeTo) as HwndSource;
        if (hwndSource == null)
            return null;
        var root = hwndSource.RootVisual;

        // Transform the point from the root to client coordinates.
        var transformToRoot = relativeTo.TransformToAncestor(root);
        var pointRoot = transformToRoot.Transform(point);
        var m = Matrix.Identity;
        var transform = VisualTreeHelper.GetTransform(root);
        if (transform != null)
        {
            m = Matrix.Multiply(m, transform.Value);
        }

        var offset = VisualTreeHelper.GetOffset(root);
        m.Translate(offset.X, offset.Y);
        var pointClient = m.Transform(pointRoot);
        pointClient = hwndSource.CompositionTarget.TransformToDevice.Transform(pointClient);

        var pointClientPixels = new POINT();
        pointClientPixels.X = (0 < pointClient.X)
            ? (int)(pointClient.X + 0.5)
            : (int)(pointClient.X - 0.5);
        pointClientPixels.Y = (0 < pointClient.Y)
            ? (int)(pointClient.Y + 0.5)
            : (int)(pointClient.Y - 0.5);

        var pointScreenPixels = pointClientPixels;

        if (ClientToScreen(
            hwndSource.Handle,
            ref pointScreenPixels))
        {
            return new Point(
                pointScreenPixels.X,
                pointScreenPixels.Y);
        }
        return new Point();
    }
}

Extensions.cs

public static class Extensions
{
    public static double ToDegrees(this double radians)
    {
        return radians * (180.0 / Math.PI);
    }

    public static double ToRadians(this double degrees)
    {
        return Math.PI * degrees / 180.0;
    }
}

MainWindow.cs

public partial class MainWindow
{
    private DispatcherTimer _dispatcherTimer;
    private uint _hdc;
    public static string _texturesPath = @"Data\Textures\";
    private SolarSystem _solarSystem;
    private int _movement;

    public static OpenGL Gl { get; private set; }
    public static Point? GlCenter { get; private set; }
    public static int GlHeight { get; private set; }
    public static int GlWidth { get; private set; }

    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
        ContentRendered += MainWindow_ContentRendered;
    }

    private void MainWindow_ContentRendered(object sender, EventArgs e)
    {
        Gl = openGLControl.OpenGL;

        var source = (HwndSource)PresentationSource.FromVisual(openGLControl);
        var hWnd = source?.Handle;
        if (hWnd != null) _hdc = (uint)hWnd;

        _solarSystem = new SolarSystem();
        _solarSystem.Camera.InitCamera();

        float[] materialAmbient = { 0.5f, 0.5f, 0.5f, 1.0f };
        float[] materialDiffuse = { 1f, 1f, 1f, 1.0f };
        float[] materialShininess = { 10.0f };
        float[] lightPosition = { 0f, 0f, 0f, 1.0f };
        float[] lightAmbient = { 0.85f, 0.85f, 0.85f, 0.0f };

        Gl.Light(OpenGL.GL_LIGHT0, OpenGL.GL_AMBIENT, lightAmbient);
        Gl.Light(OpenGL.GL_LIGHT0, OpenGL.GL_POSITION, lightPosition);
        Gl.Material(OpenGL.GL_FRONT_AND_BACK, OpenGL.GL_SHININESS, materialShininess);
        Gl.Material(OpenGL.GL_FRONT_AND_BACK, OpenGL.GL_DIFFUSE, materialDiffuse);
        Gl.Material(OpenGL.GL_FRONT_AND_BACK, OpenGL.GL_AMBIENT, materialAmbient);

        Gl.Enable(OpenGL.GL_LIGHTING);
        Gl.Enable(OpenGL.GL_LIGHT0);
        Gl.Enable(OpenGL.GL_DEPTH_TEST);

        _solarSystem.CreateScene();

        Gl.ClearColor(0, 0, 0, 1);


        GlCenter = WinApi.TransformToScreen(new Point(openGLControl.Width / 2, openGLControl.Height / 2), openGLControl);

        GlHeight = (int)openGLControl.Height;
        GlWidth = (int)openGLControl.Width;

        Camera.CenterMouse();

        _dispatcherTimer = new DispatcherTimer();
        _dispatcherTimer.Tick += DispatcherTimer_Tick;
        _dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 1);
        _dispatcherTimer.Start();
    }

    private void DispatcherTimer_Tick(object sender, EventArgs e)
    {
        Gl.Clear(OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT);
        _solarSystem.Camera.Update(_movement);
        _solarSystem.DrawScene();

        WinApi.SwapBuffers(_hdc);
        Gl.Flush();
    }

    private void OpenGLControl_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Left)
            _movement = 1;
        else
            _movement = -1;
    }

    private void OpenGLControl_MouseUp(object sender, MouseButtonEventArgs e)
    {
        _movement = 0;
    }

    private void Window_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.C)
        {
            GlCenter = WinApi.TransformToScreen(new Point(openGLControl.Width / 2, openGLControl.Height / 2), openGLControl); // new Point((int)(Left), (int)(Top));
        }
    }

    public static uint LoadTexture(string filename)
    {
        if (string.IsNullOrEmpty(filename))
            throw new ArgumentException(filename);

        Gl.Enable(OpenGL.GL_TEXTURE_2D);
        var texture = new uint[1];
        var id = texture[0];
        Gl.GenTextures(1, texture);
        Gl.BindTexture(OpenGL.GL_TEXTURE_2D, id);

        var bmp = new Bitmap(filename);
        var bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);

        Gl.TexImage2D(OpenGL.GL_TEXTURE_2D, 0, 3, bmpData.Width, bmpData.Height, 0, OpenGL.GL_BGR, OpenGL.GL_UNSIGNED_BYTE, bmpData.Scan0);

        Gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_MIN_FILTER, OpenGL.GL_LINEAR);
        Gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_MAG_FILTER, OpenGL.GL_LINEAR);

        bmp.UnlockBits(bmpData);

        return id;
    }
}

Link to whole Application:

https://www.dropbox.com/sh/uhfyeayxn8l7q9y/AAA8tFda5-ZLAjTUzJcwKUm6a?dl=0

UPDATE 1:

I have changed Look() function to:

public void Look()
{
    Gl.MatrixMode(OpenGL.GL_PROJECTION);
    Gl.LoadIdentity();
    Gl.Viewport(0, 0, GlWidth, GlHeight);
    Gl.Perspective(45.0f, GlWidth / (double) GlHeight, 1, 200.0);
    Gl.LookAt(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, 0, 1, 0);
    Gl.MatrixMode(OpenGL.GL_MODELVIEW);
}

And now it works.

Now I can see that my application is loading incorrect textures for some reason. I guess thats because method LoadTextures() I wrote, is using Gl.GenTextures(1, texture). (There is no Gen(Single)Texture method in sharpgl or I am getting it all wrong).

UPDATE 2:

So basically most of my textures doesn't work because they are not power of two, but from what I have read, they don't have to be anymore. SO my current question is: How to force sharpGL to display NPOT textures?

UPDATE 3:

Turns out I can load them like this, but well, they are upside down :).

_texture = new Texture();
...
Gl.Enable(OpenGL.GL_TEXTURE_2D);
_texture.Create(Gl, $"{_texturesPath}{_texturePath}");
_texture.Bind(Gl);

UPDATE 4:

I can flip texture to display it properly, but question is why this is happening?

Gl.Enable(OpenGL.GL_TEXTURE_2D);
var bmp = new Bitmap($"{_texturesPath}{_texturePath}");
bmp.RotateFlip(RotateFlipType.RotateNoneFlipY);
_texture.Create(Gl, bmp);
_texture.Bind(Gl);

UPDATE 5:

While question from Update 4 still stands, I got last question: How can I recalculate Camera so it is not limited to look up / down only to -90/90 degree?


Solution

  • You don't appear to be setting up your viewing frustrum correctly, and are using it in an uninitialized state. You have code for it, but it is commented out in MainWindow.xaml.cs:123.

    You must setup the frustrum. At least once before drawing. It can be either perspective or orthographic.

    gl.MatrixMode(OpenGL.GL_PROJECTION);
    gl.LoadIdentity();
    gl.Perspective(60.0f, (double)Width / (double)Height, 0.01, 100.0);