You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

750 lines
24 KiB

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 Image = SixLabors.ImageSharp.Image;
using SixLabors.Fonts;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Drawing;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
namespace SemiColinGames;
public class CameraInfo {
public readonly Vector2i Resolution;
private CameraInfo(Vector2i resolution) {
Resolution = resolution;
}
public static readonly CameraInfo NIKON_D7000 = new(new Vector2i(4928, 3264));
public static readonly CameraInfo CANON_EOS_R6M2 = new(new Vector2i(6000, 4000));
public static readonly CameraInfo IPHONE_12_MINI = new(new Vector2i(4032, 3024));
}
// FIXME: switch to immediate mode??
public class Shader : IDisposable {
public int Handle;
private bool init = false;
public Shader() {}
public void Init() {
init = true;
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;
uniform vec4 color;
void main() {
outputColor = texture(texture0, texCoord) * color;
}";
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() {
if (!init) {
Console.WriteLine("Shader.Use(): must call Init() first");
}
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);
}
}
// FIXME: this should probably be IDisposable?
public class Photo {
public string File;
public bool Loaded = false;
public string CameraModel = "";
public string LensModel = "";
public string FocalLength = "<unk>";
public string FNumber = "<unk>";
public string ExposureTime = "<unk>";
public string IsoSpeed = "<unk>";
public int Rating = 0;
private Texture texture;
private Texture placeholder;
private Image<Rgba32>? image = null;
public Photo(string file, Texture placeholder) {
File = file;
this.placeholder = placeholder;
texture = placeholder;
}
public async void Load() {
// We don't assign to this.image until Load() is done, because we might
// edit the image due to rotation (etc) and don't want to try generating
// a texture for it until that's already happened.
Image<Rgba32> tmpImage = await Image.LoadAsync<Rgba32>(File);
TryParseRating(tmpImage.Metadata.XmpProfile, out Rating);
ExifProfile? exifs = tmpImage.Metadata.ExifProfile;
if (exifs != null) {
// FIXME: when we write out images, we'll want to correct the Exif Orientation to 1.
// FIXME: handle date shot / edited (and sort by shot date?)
// FIXME: PixelXDimension & PixelYDimension hold the image geometry in Exif.
IExifValue<ushort>? orientation;
if (exifs.TryGetValue(ExifTag.Orientation, out orientation)) {
Util.RotateImageFromExif(tmpImage, orientation.Value);
}
IExifValue<string>? model;
if (exifs.TryGetValue(ExifTag.Model, out model)) {
CameraModel = model.Value ?? "";
}
IExifValue<string>? lensModel;
if (exifs.TryGetValue(ExifTag.LensModel, out lensModel)) {
LensModel = lensModel.Value ?? "";
}
IExifValue<Rational>? focalLength;
if (exifs.TryGetValue(ExifTag.FocalLength, out focalLength)) {
Rational r = focalLength.Value;
FocalLength = $"{r.Numerator / r.Denominator}mm";
}
IExifValue<Rational>? fNumber;
if (exifs.TryGetValue(ExifTag.FNumber, out fNumber)) {
Rational r = fNumber.Value;
if (r.Denominator == 1) {
FNumber = $"f/{r.Numerator}";
} else {
if (r.Denominator != 10) {
Console.WriteLine($"*** WARNING: unexpected FNumber denominator: {r.Denominator}");
}
if (r.Numerator % 10 == 0) {
FNumber = $"f/{r.Numerator / 10}";
} else {
FNumber= $"f/{r.Numerator / 10}.{r.Numerator % 10}";
}
}
}
IExifValue<Rational>? exposureTime;
if (exifs.TryGetValue(ExifTag.ExposureTime, out exposureTime)) {
Rational r = exposureTime.Value;
if (r.Numerator == 1) {
ExposureTime = $"1/{r.Denominator}";
} else if (r.Numerator == 10) {
ExposureTime = $"1/{r.Denominator / 10}";
} else if (r.Denominator == 1) {
ExposureTime = $"{r.Numerator }\"";
} else if (r.Denominator == 10) {
ExposureTime = $"{r.Numerator / 10}.{r.Numerator % 10}\"";
} else {
Console.WriteLine($"*** WARNING: unexpected ExposureTime: {r.Numerator}/{r.Denominator}");
ExposureTime = r.ToString();
}
}
IExifValue<ushort[]>? isoSpeed;
if (exifs.TryGetValue(ExifTag.ISOSpeedRatings, out isoSpeed)) {
ushort[]? iso = isoSpeed.Value;
if (iso != null) {
if (iso.Length != 1) {
Console.WriteLine($"*** WARNING: unexpected ISOSpeedRatings array length: {iso.Length}");
}
if (iso.Length >= 1) {
IsoSpeed = $"ISO {iso[0]}";
}
}
}
// foreach (IExifValue exif in exifs.Values) {
// Console.WriteLine(exif.Tag.ToString() + " " + exif.GetValue().ToString());
// }
image = tmpImage;
}
}
private bool TryParseRating(XmpProfile? xmp, out int rating) {
rating = 0;
if (xmp == null) {
return false;
}
XDocument? doc = xmp.GetDocument();
if (doc == null) {
return false;
}
XElement? root = doc.Root;
if (root == null) {
return false;
}
foreach (XElement elt in root.Descendants()) {
if (elt.Name == "{http://ns.adobe.com/xap/1.0/}Rating") {
if (int.TryParse(elt.Value, out rating)) {
return true;
}
}
}
return false;
}
public Texture Texture() {
if (texture == placeholder && image != null) {
texture = new Texture(image);
image.Dispose();
image = null;
Loaded = true;
}
return texture;
}
public string Description() {
if (Loaded) {
string shootingInfo = $"{FocalLength}, {FNumber} at {ExposureTime}, {IsoSpeed}";
return String.Format("{0,-40} {1,-50} {2}", shootingInfo, $"{CameraModel} {LensModel}", File);
} else {
return String.Format("{0,-90} {1}", "", File);
}
}
}
public class Texture : IDisposable {
public int Handle;
public Vector2i Size;
public Texture(Image<Rgba32> image) {
Size = new Vector2i(image.Width, image.Height);
byte[] pixelBytes = new byte[Size.X * Size.Y * Unsafe.SizeOf<Rgba32>()];
image.CopyPixelDataTo(pixelBytes);
Handle = GL.GenTexture();
if (Handle > maxHandle) {
Console.WriteLine("GL.GenTexture #" + Handle);
maxHandle = Handle;
}
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, Handle);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, Size.X, Size.Y, 0, PixelFormat.Rgba, PixelType.UnsignedByte, pixelBytes);
//GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.LinearMipmapLinear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.Linear);
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);
// FIXME: should we use mipmaps?
//GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
}
private static int maxHandle = -1;
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 UiGeometry {
public static Vector2i MIN_WINDOW_SIZE = new(640, 480);
private static CameraInfo activeCamera = CameraInfo.CANON_EOS_R6M2;
public readonly Vector2i WindowSize;
public readonly List<Box2i> ThumbnailBoxes = new();
public readonly Box2i PhotoBox;
public readonly Box2i StatusBox;
public UiGeometry() : this(MIN_WINDOW_SIZE) {}
public UiGeometry(Vector2i windowSize) {
WindowSize = windowSize;
int numThumbnails = 13;
int thumbnailHeight = WindowSize.Y / numThumbnails;
int thumbnailWidth = (int) 1.0 * thumbnailHeight * activeCamera.Resolution.X / activeCamera.Resolution.Y;
for (int i = 0; i < numThumbnails; i++) {
Box2i box = Util.MakeBox(WindowSize.X - thumbnailWidth, i * thumbnailHeight, thumbnailWidth, thumbnailHeight);
ThumbnailBoxes.Add(box);
}
int statusBoxHeight = 20;
int statusBoxPadding = 4;
PhotoBox = new Box2i(0, 0, WindowSize.X - thumbnailWidth, WindowSize.Y - statusBoxHeight - statusBoxPadding);
StatusBox = new Box2i(0, WindowSize.Y - statusBoxHeight, WindowSize.X - thumbnailWidth, WindowSize.Y);
}
}
public static class Util {
public const float PI = (float) Math.PI;
public static Box2i MakeBox(int left, int top, int width, int height) {
return new Box2i(left, top, left + width, top + height);
}
public static Image<Rgba32> MakeImage(float width, float height) {
return new((int) Math.Ceiling(width), (int) Math.Ceiling(height));
}
// https://sirv.com/help/articles/rotate-photos-to-be-upright/
public static void RotateImageFromExif(Image<Rgba32> image, ushort orientation) {
if (orientation <= 1) {
return;
}
// FIXME: I'm not convinced that all of these are correct, especially the
// cases that involve flipping (because whether you're flipping before or
// after rotation matters.).
var operations = new Dictionary<ushort, (RotateMode, FlipMode)> {
{ 2, (RotateMode.None, FlipMode.Horizontal) },
{ 3, (RotateMode.Rotate180, FlipMode.None) },
{ 4, (RotateMode.None, FlipMode.Vertical) },
{ 5, (RotateMode.Rotate90, FlipMode.Vertical) },
{ 6, (RotateMode.Rotate90, FlipMode.None) },
{ 7, (RotateMode.Rotate270, FlipMode.Vertical) },
{ 8, (RotateMode.Rotate270, FlipMode.None) },
};
var (rotate, flip) = operations[orientation];
image.Mutate(x => x.RotateFlip(rotate, flip));
}
public static Texture RenderText(string text) {
return RenderText(text, 16);
}
public static Texture RenderText(string text, int size) {
Font font = SystemFonts.CreateFont("Consolas", size, FontStyle.Bold);
TextOptions options = new(font);
FontRectangle rect = TextMeasurer.Measure(text, new TextOptions(font));
Image<Rgba32> image = MakeImage(rect.Width, rect.Height);
IBrush brush = Brushes.Solid(Color.White);
// IPen pen = Pens.Solid(Color.Black, 1f);
// image.Mutate(x => x.DrawText(options, text, brush, pen));
image.Mutate(x => x.DrawText(options, text, brush));
Texture texture = new Texture(image);
image.Dispose();
return texture;
}
public static Texture RenderStar(float radius) {
IPath path = new Star(x: radius, y: radius, prongs: 5, innerRadii: radius * 0.4f, outerRadii: radius, angle: Util.PI);
Image<Rgba32> image = MakeImage(path.Bounds.Width, path.Bounds.Height);
image.Mutate(x => x.Fill(Color.White, path));
Texture texture = new Texture(image);
image.Dispose();
return texture;
}
}
public class Game : GameWindow {
public Game(GameWindowSettings gwSettings, NativeWindowSettings nwSettings) : base(gwSettings, nwSettings) {}
private static Texture TEXTURE_WHITE = new(new Image<Rgba32>(1, 1, new Rgba32(255, 255, 255)));
private static Texture TEXTURE_BLACK = new(new Image<Rgba32>(1, 1, new Rgba32(0, 0, 0)));
UiGeometry geometry = new();
// Input handling.
long downTimer = Int64.MaxValue;
long upTimer = Int64.MaxValue;
// Four points, each consisting of (x, y, z, tex_x, tex_y).
float[] vertices = new float[20];
// Indices to draw a rectangle from two triangles.
uint[] indices = {
0, 1, 3, // first triangle
1, 2, 3 // second triangle
};
int VertexBufferObject;
int ElementBufferObject;
int VertexArrayObject;
List<Photo> photos = new();
int photoIndex = 0;
Shader shader = new();
Matrix4 projection;
float zoomLevel = 0f;
protected override void OnUpdateFrame(FrameEventArgs e) {
base.OnUpdateFrame(e);
long now = DateTime.Now.Ticks;
KeyboardState input = KeyboardState;
// Close when Escape is pressed.
if (input.IsKeyDown(Keys.Escape)) {
Close();
}
// Look for mouse clicks on thumbnails.
if (MouseState.IsButtonPressed(0)) {
for (int i = 0; i < geometry.ThumbnailBoxes.Count; i++) {
Box2i box = geometry.ThumbnailBoxes[i];
if (box.ContainsInclusive((Vector2i) MouseState.Position)) {
if (0 <= i && i < photos.Count) {
photoIndex = i;
}
}
}
}
// Track keyboard repeat times for advancing up/down.
if (!input.IsKeyDown(Keys.Down)) {
downTimer = Int64.MaxValue;
}
if (!input.IsKeyDown(Keys.Up)) {
upTimer = Int64.MaxValue;
}
if (input.IsKeyDown(Keys.D0) || input.IsKeyDown(Keys.GraveAccent)) {
zoomLevel = 0f;
}
if (input.IsKeyDown(Keys.D1)) {
zoomLevel = 1f;
}
if (input.IsKeyDown(Keys.D2)) {
zoomLevel = 2f;
}
if (input.IsKeyDown(Keys.D3)) {
zoomLevel = 4f;
}
if (input.IsKeyDown(Keys.D4)) {
zoomLevel = 8f;
}
if (input.IsKeyDown(Keys.D5)) {
zoomLevel = 16f;
}
// FIXME: make a proper Model class for tracking the state of the controls?
if (input.IsKeyPressed(Keys.Down) || now > downTimer) {
if (photoIndex < photos.Count - 1) {
downTimer = now + 10000 * 200;
photoIndex++;
}
}
if (input.IsKeyPressed(Keys.Up) || now > upTimer) {
if (photoIndex > 0) {
upTimer = now + 10000 * 200;
photoIndex--;
}
}
}
protected override async void OnLoad() {
base.OnLoad();
GL.ClearColor(0f, 0f, 0f, 1f);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
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.Init();
shader.Use();
// Because there's 5 floats between the start of the first vertex and the start of the second,
// the stride is 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 textures from JPEGs.
// string[] files = Directory.GetFiles(@"c:\users\colin\desktop\photos-test\");
// string[] files = Directory.GetFiles(@"c:\users\colin\pictures\photos\2023\07\14\");
string[] files = Directory.GetFiles(@"G:\DCIM\100EOSR6\");
// string[] files = Directory.GetFiles(@"C:\Users\colin\Pictures\photos\2018\06\23");
// string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\Germany all\104D7000");
// string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\many-birds\");
for (int i = 0; i < files.Count(); i++) {
string file = files[i];
if (file.ToLower().EndsWith(".jpg")) {
Photo photo = new Photo(file, TEXTURE_BLACK);
photos.Add(photo);
await Task.Run( () => { photo.Load(); });
if (photos.Count == 100) {
break;
}
}
}
}
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);
Photo activePhoto = photos[photoIndex];
Texture active = activePhoto.Texture();
// FIXME: make a function for scaling & centering one box on another.
float scaleX = 1f * geometry.PhotoBox.Size.X / active.Size.X;
float scaleY = 1f * geometry.PhotoBox.Size.Y / active.Size.Y;
float scale = Math.Min(scaleX, scaleY);
if (zoomLevel > 0f) {
scale = zoomLevel;
}
Vector2i renderSize = (Vector2i) (((Vector2) active.Size) * scale);
// FIXME: clip by the actual PhotoBox size -- this bleeds over UI elements when zoomed in.
// This doesn't work -- boo.
// if (renderSize.X > geometry.PhotoBox.Size.X) {
// renderSize.X = geometry.PhotoBox.Size.X;
// }
// if (renderSize.Y > geometry.PhotoBox.Size.Y) {
// renderSize.Y = geometry.PhotoBox.Size.Y;
// }
Vector2i center = (Vector2i) geometry.PhotoBox.Center;
Box2i photoBox = Util.MakeBox(center.X - renderSize.X / 2, center.Y - renderSize.Y / 2, renderSize.X, renderSize.Y);
DrawTexture(active, photoBox);
for (int i = 0; i < photos.Count && i < geometry.ThumbnailBoxes.Count(); i++) {
Box2i box = geometry.ThumbnailBoxes[i];
DrawTexture(photos[i].Texture(), box);
if (i == photoIndex) {
DrawBox(box, 5, Color4.Black);
DrawBox(box, 3, Color4.White);
}
}
// Draw status box.
DrawFilledBox(geometry.StatusBox, Color4.Black);
DrawText(activePhoto.Description(), geometry.StatusBox.Min.X + 80, geometry.StatusBox.Min.Y);
if (activePhoto.Loaded) {
DrawText($"{(scale * 100):F1}%", geometry.StatusBox.Min.X, geometry.StatusBox.Min.Y);
}
SwapBuffers();
}
void DrawTexture(Texture texture, Box2i box) {
DrawTexture(texture, box, Color4.White);
}
void DrawTexture(Texture texture, Box2i box, Color4 color) {
GL.Uniform4(shader.GetUniformLocation("color"), color);
SetVertices(box.Min.X, box.Min.Y, box.Size.X, box.Size.Y);
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(Box2i box, int thickness, Color4 color) {
DrawTexture(TEXTURE_WHITE, Util.MakeBox(box.Min.X, box.Min.Y, box.Size.X, thickness), color);
DrawTexture(TEXTURE_WHITE, Util.MakeBox(box.Min.X, box.Min.Y, thickness, box.Size.Y), color);
DrawTexture(TEXTURE_WHITE, Util.MakeBox(box.Min.X, box.Max.Y - thickness, box.Size.X, thickness), color);
DrawTexture(TEXTURE_WHITE, Util.MakeBox(box.Max.X - thickness, box.Min.Y, thickness, box.Size.Y), color);
}
void DrawFilledBox(Box2i box, Color4 color) {
DrawTexture(TEXTURE_WHITE, Util.MakeBox(box.Min.X, box.Min.Y, box.Size.X, box.Size.Y), color);
}
void DrawText(string text, int x, int y) {
Texture label = Util.RenderText(text);
DrawTexture(label, Util.MakeBox(x, y, label.Size.X, label.Size.Y));
label.Dispose();
}
protected override void OnResize(ResizeEventArgs e) {
base.OnResize(e);
Console.WriteLine($"OnResize: {e.Width}x{e.Height}");
geometry = new UiGeometry(e.Size);
projection = Matrix4.CreateOrthographicOffCenter(0f, e.Width, e.Height, 0f, -1f, 1f);
GL.UniformMatrix4(shader.GetUniformLocation("projection"), true, ref projection);
GL.Viewport(0, 0, e.Width, e.Height);
}
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<MonitorInfo> 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();
gwSettings.RenderFrequency = 60.0;
NativeWindowSettings nwSettings = new();
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 = UiGeometry.MIN_WINDOW_SIZE;
nwSettings.Title = "Totte";
// FIXME: nwSettings.Icon = ...
using (Game game = new(gwSettings, nwSettings)) {
game.Run();
}
}
}