using OpenTK.Graphics.OpenGL4; using OpenTK.Mathematics; using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; using OpenTK.Windowing.GraphicsLibraryFramework; // https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Image.html using SixLabors.ImageSharp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using System.IO; using System.Reflection.Metadata; using System.Runtime.CompilerServices; using static System.Runtime.InteropServices.JavaScript.JSType; using System.Xml.Linq; using Image = SixLabors.ImageSharp.Image; namespace SemiColinGames; public class CameraInfo { public readonly Vector2i Resolution; private CameraInfo(Vector2i resolution) { Resolution = resolution; } public static readonly CameraInfo NIKON_D7000 = new CameraInfo(new Vector2i(4928, 3264)); public static readonly CameraInfo IPHONE_12_MINI = new CameraInfo(new Vector2i(4032, 3024)); } public class Shader : IDisposable { int Handle; public Shader() { int VertexShader; int FragmentShader; string VertexShaderSource = @" #version 330 layout(location = 0) in vec3 aPosition; layout(location = 1) in vec2 aTexCoord; out vec2 texCoord; uniform mat4 projection; void main(void) { texCoord = aTexCoord; gl_Position = vec4(aPosition, 1.0) * projection; }"; string FragmentShaderSource = @" #version 330 out vec4 outputColor; in vec2 texCoord; uniform sampler2D texture0; void main() { outputColor = texture(texture0, texCoord); }"; VertexShader = GL.CreateShader(ShaderType.VertexShader); GL.ShaderSource(VertexShader, VertexShaderSource); FragmentShader = GL.CreateShader(ShaderType.FragmentShader); GL.ShaderSource(FragmentShader, FragmentShaderSource); GL.CompileShader(VertexShader); int success; GL.GetShader(VertexShader, ShaderParameter.CompileStatus, out success); if (success == 0) { string infoLog = GL.GetShaderInfoLog(VertexShader); Console.WriteLine(infoLog); } GL.CompileShader(FragmentShader); GL.GetShader(FragmentShader, ShaderParameter.CompileStatus, out success); if (success == 0) { string infoLog = GL.GetShaderInfoLog(FragmentShader); Console.WriteLine(infoLog); } Handle = GL.CreateProgram(); GL.AttachShader(Handle, VertexShader); GL.AttachShader(Handle, FragmentShader); GL.LinkProgram(Handle); GL.GetProgram(Handle, GetProgramParameterName.LinkStatus, out success); if (success == 0) { string infoLog = GL.GetProgramInfoLog(Handle); Console.WriteLine(infoLog); } GL.DetachShader(Handle, VertexShader); GL.DetachShader(Handle, FragmentShader); GL.DeleteShader(FragmentShader); GL.DeleteShader(VertexShader); } public void Use() { GL.UseProgram(Handle); } private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { GL.DeleteProgram(Handle); disposedValue = true; } } ~Shader() { if (disposedValue == false) { Console.WriteLine("~Shader(): resource leak? Dispose() should be called manually."); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public int GetAttribLocation(string name) { return GL.GetAttribLocation(Handle, name); } public int GetUniformLocation(string name) { return GL.GetUniformLocation(Handle, name); } } public class Texture : IDisposable { public int Handle; public int Width; public int Height; public Texture(Image image) { Width = image.Width; Height = image.Height; Console.WriteLine($"image loaded: {Width}x{Height}"); //foreach (IExifValue exif in image.Metadata.ExifProfile.Values) { // Console.WriteLine($"{exif.Tag} : {exif.GetValue()}"); //} byte[] pixelBytes = new byte[Width * Height * Unsafe.SizeOf()]; image.CopyPixelDataTo(pixelBytes); image.Dispose(); Handle = GL.GenTexture(); GL.ActiveTexture(TextureUnit.Texture0); GL.BindTexture(TextureTarget.Texture2D, Handle); GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, image.Width, image.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, pixelBytes); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.LinearMipmapLinear); // FIXME: is this right? GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) TextureMagFilter.Nearest); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int) TextureWrapMode.ClampToBorder); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int) TextureWrapMode.ClampToBorder); float[] borderColor = { 0.0f, 0.0f, 0.0f, 1.0f }; GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBorderColor, borderColor); GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); } private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { GL.DeleteTexture(Handle); disposedValue = true; } } ~Texture() { if (!disposedValue) { Console.WriteLine("~Texture(): resource leak? Dispose() should be called manually."); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } public class Game : GameWindow { public Game(GameWindowSettings gwSettings, NativeWindowSettings nwSettings) : base(gwSettings, nwSettings) { } static CameraInfo activeCamera = CameraInfo.NIKON_D7000; static int thumbnailHeight = 150; static int thumbnailWidth = (int) 1.0 * thumbnailHeight * activeCamera.Resolution.X / activeCamera.Resolution.Y; static Texture TEXTURE_WHITE; int windowWidth; int windowHeight; float[] vertices = { // Position Texture coordinates 0f, 0f, 0.0f, 0.0f, 0.0f, // top left 2560f, 0f, 0.0f, 1.0f, 0.0f, // top right 2560f, 1440f, 0.0f, 1.0f, 1.0f, // bottom right 0f, 1440f, 0.0f, 0.0f, 1.0f, // bottom left }; uint[] indices = { 0, 1, 3, // first triangle 1, 2, 3 // second triangle }; int VertexBufferObject; int ElementBufferObject; int VertexArrayObject; List textures; int textureIndex = 0; Shader shader; Matrix4 projection; protected override void OnUpdateFrame(FrameEventArgs e) { base.OnUpdateFrame(e); KeyboardState input = KeyboardState; if (input.IsKeyDown(Keys.Escape)) { Close(); } if (input.IsKeyPressed(Keys.Down)) { if (textureIndex < textures.Count - 1) { textureIndex++; } } if (input.IsKeyPressed(Keys.Up)) { if (textureIndex > 0) { textureIndex--; } } } protected override void OnLoad() { base.OnLoad(); Console.WriteLine($"thumbnail size: {thumbnailWidth}x{thumbnailHeight}"); GL.ClearColor(0.0f, 0.0f, 0.05f, 1.0f); VertexArrayObject = GL.GenVertexArray(); GL.BindVertexArray(VertexArrayObject); VertexBufferObject = GL.GenBuffer(); ElementBufferObject = GL.GenBuffer(); GL.BindBuffer(BufferTarget.ArrayBuffer, VertexBufferObject); GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices, BufferUsageHint.DynamicDraw); GL.BindBuffer(BufferTarget.ElementArrayBuffer, ElementBufferObject); GL.BufferData(BufferTarget.ElementArrayBuffer, indices.Length * sizeof(uint), indices, BufferUsageHint.DynamicDraw); shader = new Shader(); shader.Use(); // Because there's now 5 floats between the start of the first vertex and the start of the second, // we modify the stride from 3 * sizeof(float) to 5 * sizeof(float). // This will now pass the new vertex array to the buffer. var vertexLocation = shader.GetAttribLocation("aPosition"); GL.EnableVertexAttribArray(vertexLocation); GL.VertexAttribPointer(vertexLocation, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), 0); // Next, we also setup texture coordinates. It works in much the same way. // We add an offset of 3, since the texture coordinates comes after the position data. // We also change the amount of data to 2 because there's only 2 floats for texture coordinates. var texCoordLocation = shader.GetAttribLocation("aTexCoord"); GL.EnableVertexAttribArray(texCoordLocation); GL.VertexAttribPointer(texCoordLocation, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), 3 * sizeof(float)); // Load blank white texture. Image white1x1 = new Image(1, 1, new Rgba32(255, 255, 255)); TEXTURE_WHITE = new Texture(white1x1); // Load textures from JPEGs. string[] files = Directory.GetFiles(@"c:\users\colin\pictures\photos\2023\06\27\"); textures = new List(); foreach (string file in files) { if (file.ToLower().EndsWith(".jpg")) { Image image = Image.Load(file); textures.Add(new Texture(image)); } } } protected override void OnUnload() { base.OnUnload(); } protected override void OnRenderFrame(FrameEventArgs e) { base.OnRenderFrame(e); GL.Clear(ClearBufferMask.ColorBufferBit); GL.BindBuffer(BufferTarget.ArrayBuffer, VertexBufferObject); GL.ActiveTexture(TextureUnit.Texture0); int maxPhotoWidth = windowWidth - thumbnailWidth; Texture active = textures[textureIndex]; // TODO: handle the case where we need to letterbox vertically instead. // TODO: pull these geometry calculations out into an object. int photoWidth = (int) (1.0 * windowHeight / active.Height * active.Width); int letterboxWidth = (maxPhotoWidth - photoWidth) / 2; DrawTexture(active, letterboxWidth, 0, photoWidth, windowHeight); for (int i = 0; i < textures.Count; i++) { Rectangle box = new Rectangle(windowWidth - thumbnailWidth, i * thumbnailHeight, thumbnailWidth, thumbnailHeight); DrawTexture(textures[i], box); if (i == textureIndex) { DrawBox(box, 3); } } SwapBuffers(); } void DrawTexture(Texture texture, Rectangle box) { DrawTexture(texture, box.Left, box.Top, box.Width, box.Height); } void DrawTexture(Texture texture, int left, int top, int width, int height) { SetVertices(left, top, width, height); GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices, BufferUsageHint.DynamicDraw); GL.BindTexture(TextureTarget.Texture2D, texture.Handle); GL.DrawElements(PrimitiveType.Triangles, indices.Length, DrawElementsType.UnsignedInt, 0); } void DrawBox(Rectangle box, int thickness) { DrawBox(box.Left, box.Top, box.Width, box.Height, thickness); } void DrawBox(int left, int top, int width, int height, int thickness) { DrawTexture(TEXTURE_WHITE, left, top, width, thickness); DrawTexture(TEXTURE_WHITE, left, top, thickness, height); DrawTexture(TEXTURE_WHITE, left, top + height - thickness, width, thickness); DrawTexture(TEXTURE_WHITE, left + width - thickness, top, thickness, height); } protected override void OnResize(ResizeEventArgs e) { base.OnResize(e); Console.WriteLine($"OnResize: {e.Width}x{e.Height}"); windowWidth = e.Width; windowHeight = e.Height; projection = Matrix4.CreateOrthographicOffCenter(0f, windowWidth, windowHeight, 0f, -1f, 1f); GL.UniformMatrix4(shader.GetUniformLocation("projection"), true, ref projection); GL.Viewport(0, 0, windowWidth, windowHeight); } private void SetVertices(float left, float top, float width, float height) { // top left vertices[0] = left; vertices[1] = top; vertices[2] = 0f; vertices[3] = 0f; vertices[4] = 0f; // top right vertices[5] = left + width; vertices[6] = top; vertices[7] = 0f; vertices[8] = 1f; vertices[9] = 0f; // bottom right vertices[10] = left + width; vertices[11] = top + height; vertices[12] = 0f; vertices[13] = 1f; vertices[14] = 1f; // bottom left vertices[15] = left; vertices[16] = top + height; vertices[17] = 0f; vertices[18] = 0f; vertices[19] = 1f; } } static class Program { static void Main(string[] args) { List monitors = Monitors.GetMonitors(); MonitorInfo bestMonitor = monitors[0]; int bestResolution = bestMonitor.HorizontalResolution * bestMonitor.VerticalResolution; for (int i = 1; i < monitors.Count; i++) { MonitorInfo monitor = monitors[i]; int resolution = monitor.HorizontalResolution * monitor.VerticalResolution; if (resolution > bestResolution) { bestResolution = resolution; bestMonitor = monitor; } } Console.WriteLine($"best monitor: {bestMonitor.HorizontalResolution}x{bestMonitor.VerticalResolution}"); GameWindowSettings gwSettings = new GameWindowSettings(); gwSettings.RenderFrequency = 60.0; NativeWindowSettings nwSettings = new NativeWindowSettings(); nwSettings.WindowState = WindowState.Normal; nwSettings.CurrentMonitor = bestMonitor.Handle; nwSettings.Location = new Vector2i(bestMonitor.WorkArea.Min.X + 1, bestMonitor.WorkArea.Min.Y + 31); nwSettings.Size = new Vector2i(bestMonitor.WorkArea.Size.X - 2, bestMonitor.WorkArea.Size.Y - 32); nwSettings.MinimumSize = new Vector2i(640, 480); nwSettings.Title = "Totte"; // FIXME: nwSettings.Icon = ... using (Game game = new Game(gwSettings, nwSettings)) { game.Run(); } } }