2023-06-30 02:10:24 +00:00
|
|
|
|
using OpenTK.Graphics.OpenGL4;
|
|
|
|
|
using OpenTK.Mathematics;
|
|
|
|
|
using OpenTK.Windowing.Common;
|
2023-07-26 14:00:46 +00:00
|
|
|
|
using OpenTK.Windowing.Common.Input;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
using OpenTK.Windowing.Desktop;
|
|
|
|
|
using OpenTK.Windowing.GraphicsLibraryFramework;
|
2023-07-08 03:41:32 +00:00
|
|
|
|
// https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Image.html
|
2023-06-30 02:10:24 +00:00
|
|
|
|
using Image = SixLabors.ImageSharp.Image;
|
2023-07-23 22:42:08 +00:00
|
|
|
|
using SixLabors.Fonts;
|
2023-07-16 23:13:08 +00:00
|
|
|
|
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
|
2023-07-23 22:42:08 +00:00
|
|
|
|
using SixLabors.ImageSharp.Drawing.Processing;
|
|
|
|
|
using SixLabors.ImageSharp.Drawing;
|
2023-07-26 02:19:18 +00:00
|
|
|
|
using System;
|
2023-07-26 01:44:22 +00:00
|
|
|
|
using System.Diagnostics;
|
2023-07-23 22:42:08 +00:00
|
|
|
|
using System.Runtime.CompilerServices;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
using System.Xml.Linq;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
|
|
|
|
namespace SemiColinGames;
|
|
|
|
|
|
2023-07-26 02:19:18 +00:00
|
|
|
|
public class FpsCounter {
|
2023-07-26 15:25:14 +00:00
|
|
|
|
private readonly int[] frameTimes = new int[30];
|
2023-07-26 02:19:18 +00:00
|
|
|
|
private double fps = 0;
|
|
|
|
|
private int idx = 0;
|
|
|
|
|
|
|
|
|
|
public int Fps {
|
|
|
|
|
get => (int) Math.Ceiling(fps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Update() {
|
|
|
|
|
var now = Environment.TickCount; // ms
|
|
|
|
|
if (frameTimes[idx] != 0) {
|
|
|
|
|
var timeElapsed = now - frameTimes[idx];
|
|
|
|
|
fps = 1000.0 * frameTimes.Length / timeElapsed;
|
|
|
|
|
}
|
|
|
|
|
frameTimes[idx] = now;
|
|
|
|
|
idx = (idx + 1) % frameTimes.Length;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-06 22:31:50 +00:00
|
|
|
|
public class CameraInfo {
|
|
|
|
|
public readonly Vector2i Resolution;
|
|
|
|
|
|
|
|
|
|
private CameraInfo(Vector2i resolution) {
|
|
|
|
|
Resolution = resolution;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-08 04:33:15 +00:00
|
|
|
|
public static readonly CameraInfo NIKON_D7000 = new(new Vector2i(4928, 3264));
|
2023-07-16 23:13:08 +00:00
|
|
|
|
public static readonly CameraInfo CANON_EOS_R6M2 = new(new Vector2i(6000, 4000));
|
2023-07-08 04:33:15 +00:00
|
|
|
|
public static readonly CameraInfo IPHONE_12_MINI = new(new Vector2i(4032, 3024));
|
2023-07-06 22:31:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 18:28:57 +00:00
|
|
|
|
// FIXME: switch to immediate mode??
|
2023-07-25 22:49:52 +00:00
|
|
|
|
// https://gamedev.stackexchange.com/questions/198805/opentk-immediate-mode-on-net-core-doesnt-work
|
|
|
|
|
// https://www.youtube.com/watch?v=Q23Kf9QEaO4
|
2023-06-30 02:10:24 +00:00
|
|
|
|
public class Shader : IDisposable {
|
2023-07-08 03:29:51 +00:00
|
|
|
|
public int Handle;
|
|
|
|
|
private bool init = false;
|
|
|
|
|
|
|
|
|
|
public Shader() {}
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
2023-07-08 03:29:51 +00:00
|
|
|
|
public void Init() {
|
|
|
|
|
init = true;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
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;
|
2023-07-07 03:14:55 +00:00
|
|
|
|
uniform vec4 color;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
|
|
|
|
void main() {
|
2023-07-07 03:14:55 +00:00
|
|
|
|
outputColor = texture(texture0, texCoord) * color;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
}";
|
|
|
|
|
|
|
|
|
|
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() {
|
2023-07-08 03:29:51 +00:00
|
|
|
|
if (!init) {
|
|
|
|
|
Console.WriteLine("Shader.Use(): must call Init() first");
|
|
|
|
|
}
|
2023-06-30 02:10:24 +00:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-17 06:44:34 +00:00
|
|
|
|
// FIXME: this should probably be IDisposable?
|
2023-07-16 23:23:03 +00:00
|
|
|
|
public class Photo {
|
2023-07-26 00:25:07 +00:00
|
|
|
|
public string Filename;
|
2023-07-24 20:07:17 +00:00
|
|
|
|
public bool Loaded = false;
|
2023-07-25 20:58:41 +00:00
|
|
|
|
public Vector2i Size;
|
2023-07-26 00:25:07 +00:00
|
|
|
|
public DateTime DateTimeOriginal;
|
2023-07-24 22:55:59 +00:00
|
|
|
|
public string CameraModel = "";
|
|
|
|
|
public string LensModel = "";
|
2023-07-24 20:39:01 +00:00
|
|
|
|
public string FocalLength = "<unk>";
|
2023-07-24 20:07:17 +00:00
|
|
|
|
public string FNumber = "<unk>";
|
|
|
|
|
public string ExposureTime = "<unk>";
|
|
|
|
|
public string IsoSpeed = "<unk>";
|
2023-07-25 13:41:13 +00:00
|
|
|
|
public int Rating = 0;
|
2023-07-25 20:43:11 +00:00
|
|
|
|
public ushort Orientation = 1;
|
2023-07-24 20:07:17 +00:00
|
|
|
|
|
2023-07-17 03:45:50 +00:00
|
|
|
|
private Texture texture;
|
|
|
|
|
private Texture placeholder;
|
2023-07-17 06:44:34 +00:00
|
|
|
|
private Image<Rgba32>? image = null;
|
2023-07-16 23:23:03 +00:00
|
|
|
|
|
2023-07-26 00:25:07 +00:00
|
|
|
|
public Photo(string filename, Texture placeholder) {
|
|
|
|
|
Filename = filename;
|
2023-07-17 03:45:50 +00:00
|
|
|
|
this.placeholder = placeholder;
|
|
|
|
|
texture = placeholder;
|
2023-07-25 20:58:41 +00:00
|
|
|
|
|
2023-07-26 00:25:07 +00:00
|
|
|
|
DateTime creationTime = File.GetCreationTime(filename); // Local time.
|
|
|
|
|
DateTimeOriginal = creationTime;
|
|
|
|
|
ImageInfo info = Image.Identify(filename);
|
2023-07-25 20:58:41 +00:00
|
|
|
|
Size = new(info.Size.Width, info.Size.Height);
|
|
|
|
|
ParseExif(info.Metadata.ExifProfile);
|
|
|
|
|
TryParseRating(info.Metadata.XmpProfile, out Rating);
|
2023-07-17 06:44:34 +00:00
|
|
|
|
}
|
2023-06-30 02:21:41 +00:00
|
|
|
|
|
2023-07-26 01:24:20 +00:00
|
|
|
|
public async void LoadAsync() {
|
2023-07-25 18:28:57 +00:00
|
|
|
|
// 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.
|
2023-07-26 00:25:07 +00:00
|
|
|
|
Image<Rgba32> tmp = await Image.LoadAsync<Rgba32>(Filename);
|
2023-07-25 21:10:51 +00:00
|
|
|
|
Util.RotateImageFromExif(tmp, Orientation);
|
|
|
|
|
image = tmp;
|
2023-07-17 00:06:37 +00:00
|
|
|
|
}
|
2023-07-17 03:45:50 +00:00
|
|
|
|
|
2023-07-26 02:00:43 +00:00
|
|
|
|
public async void UnloadAsync() {
|
|
|
|
|
Loaded = false;
|
2023-07-26 01:24:20 +00:00
|
|
|
|
if (texture != placeholder) {
|
2023-07-26 02:00:43 +00:00
|
|
|
|
await Task.Run( () => { texture.Dispose(); });
|
2023-07-26 01:24:20 +00:00
|
|
|
|
texture = placeholder;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 13:54:41 +00:00
|
|
|
|
private bool TryParseRating(XmpProfile? xmp, out int rating) {
|
|
|
|
|
rating = 0;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
if (xmp == null) {
|
2023-07-25 13:54:41 +00:00
|
|
|
|
return false;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
XDocument? doc = xmp.GetDocument();
|
|
|
|
|
if (doc == null) {
|
2023-07-25 13:54:41 +00:00
|
|
|
|
return false;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
XElement? root = doc.Root;
|
|
|
|
|
if (root == null) {
|
2023-07-25 13:54:41 +00:00
|
|
|
|
return false;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
foreach (XElement elt in root.Descendants()) {
|
|
|
|
|
if (elt.Name == "{http://ns.adobe.com/xap/1.0/}Rating") {
|
|
|
|
|
if (int.TryParse(elt.Value, out rating)) {
|
2023-07-25 13:54:41 +00:00
|
|
|
|
return true;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-25 13:54:41 +00:00
|
|
|
|
return false;
|
2023-07-25 13:41:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 00:25:07 +00:00
|
|
|
|
// Exif (and other image metadata) reference, from the now-defunct Metadata Working Group:
|
|
|
|
|
// https://web.archive.org/web/20180919181934/http://www.metadataworkinggroup.org/pdf/mwg_guidance.pdf
|
|
|
|
|
//
|
|
|
|
|
// Specifically:
|
|
|
|
|
//
|
|
|
|
|
// In general, date/time metadata is being used to describe the following scenarios:
|
|
|
|
|
// * Date/time original specifies when a photo was taken
|
|
|
|
|
// * Date/time digitized specifies when an image was digitized
|
|
|
|
|
// * Date/time modified specifies when a file was modified by the user
|
|
|
|
|
//
|
|
|
|
|
// Original Date/Time – Creation date of the intellectual content (e.g. the photograph), rather than the creation date of the content being shown
|
|
|
|
|
// Exif DateTimeOriginal (36867, 0x9003) and SubSecTimeOriginal (37521, 0x9291)
|
|
|
|
|
// IPTC DateCreated (IIM 2:55, 0x0237) and TimeCreated (IIM 2:60, 0x023C)
|
|
|
|
|
// XMP (photoshop:DateCreated)
|
|
|
|
|
//
|
|
|
|
|
// Digitized Date/Time – Creation date of the digital representation
|
|
|
|
|
// Exif DateTimeDigitized (36868, 0x9004) and SubSecTimeDigitized (37522, 0x9292)
|
|
|
|
|
// IPTC DigitalCreationDate (IIM 2:62, 0x023E) and DigitalCreationTime (IIM 2:63, 0x023F)
|
|
|
|
|
// XMP (xmp:CreateDate)
|
|
|
|
|
//
|
|
|
|
|
// Modification Date/Time – Modification date of the digital image file
|
|
|
|
|
// Exif DateTime (306, 0x132) and SubSecTime (37520, 0x9290)
|
|
|
|
|
// XMP (xmp:ModifyDate)
|
2023-07-25 20:43:11 +00:00
|
|
|
|
private void ParseExif(ExifProfile? exifs) {
|
|
|
|
|
if (exifs == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IExifValue<ushort>? orientation;
|
|
|
|
|
if (exifs.TryGetValue(ExifTag.Orientation, out orientation)) {
|
|
|
|
|
Orientation = 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]}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-26 00:25:07 +00:00
|
|
|
|
|
|
|
|
|
// FIXME: I think the iPhone stores time in UTC but other cameras report it in local time.
|
|
|
|
|
IExifValue<string>? dateTimeOriginal;
|
|
|
|
|
if (exifs.TryGetValue(ExifTag.DateTimeOriginal, out dateTimeOriginal)) {
|
|
|
|
|
DateTime date;
|
|
|
|
|
if (DateTime.TryParseExact(
|
|
|
|
|
dateTimeOriginal.Value ?? "",
|
|
|
|
|
"yyyy:MM:dd HH:mm:ss",
|
|
|
|
|
System.Globalization.CultureInfo.InvariantCulture,
|
|
|
|
|
System.Globalization.DateTimeStyles.AssumeLocal,
|
|
|
|
|
out date)) {
|
|
|
|
|
DateTimeOriginal = date;
|
|
|
|
|
} else {
|
|
|
|
|
Console.WriteLine($"*** WARNING: unexpected DateTimeOriginal value: {dateTimeOriginal.Value}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-25 20:43:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-17 03:45:50 +00:00
|
|
|
|
public Texture Texture() {
|
2023-07-23 23:29:47 +00:00
|
|
|
|
if (texture == placeholder && image != null) {
|
2023-07-25 22:26:46 +00:00
|
|
|
|
// The texture needs to be created on the GL thread, so we instantiate
|
|
|
|
|
// it here (since this is called from OnRenderFrame), as long as the
|
|
|
|
|
// image is ready to go.
|
2023-07-23 23:29:47 +00:00
|
|
|
|
texture = new Texture(image);
|
|
|
|
|
image.Dispose();
|
|
|
|
|
image = null;
|
2023-07-24 16:36:44 +00:00
|
|
|
|
Loaded = true;
|
2023-07-23 23:29:47 +00:00
|
|
|
|
}
|
|
|
|
|
return texture;
|
|
|
|
|
}
|
2023-07-24 20:07:17 +00:00
|
|
|
|
|
|
|
|
|
public string Description() {
|
2023-07-26 00:35:12 +00:00
|
|
|
|
string date = DateTimeOriginal.ToString("yyyy-MM-dd HH:mm:ss");
|
|
|
|
|
string shootingInfo = $"{date} {FocalLength}, {FNumber} at {ExposureTime}, {IsoSpeed}";
|
|
|
|
|
return String.Format("{0,-60} {1,-50} {2}", shootingInfo, $"{CameraModel} {LensModel}", Filename);
|
2023-07-24 20:07:17 +00:00
|
|
|
|
}
|
2023-07-17 00:06:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class Texture : IDisposable {
|
|
|
|
|
public int Handle;
|
|
|
|
|
public Vector2i Size;
|
|
|
|
|
|
|
|
|
|
public Texture(Image<Rgba32> image) {
|
|
|
|
|
Size = new Vector2i(image.Width, image.Height);
|
2023-07-08 03:41:32 +00:00
|
|
|
|
byte[] pixelBytes = new byte[Size.X * Size.Y * Unsafe.SizeOf<Rgba32>()];
|
2023-06-30 02:10:24 +00:00
|
|
|
|
image.CopyPixelDataTo(pixelBytes);
|
|
|
|
|
|
|
|
|
|
Handle = GL.GenTexture();
|
2023-07-24 16:13:13 +00:00
|
|
|
|
if (Handle > maxHandle) {
|
2023-07-26 01:44:22 +00:00
|
|
|
|
// Console.WriteLine("GL.GenTexture #" + Handle);
|
2023-07-24 16:13:13 +00:00
|
|
|
|
maxHandle = Handle;
|
|
|
|
|
}
|
2023-06-30 02:10:24 +00:00
|
|
|
|
GL.ActiveTexture(TextureUnit.Texture0);
|
|
|
|
|
GL.BindTexture(TextureTarget.Texture2D, Handle);
|
2023-07-08 03:41:32 +00:00
|
|
|
|
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, Size.X, Size.Y, 0, PixelFormat.Rgba, PixelType.UnsignedByte, pixelBytes);
|
2023-07-17 06:51:15 +00:00
|
|
|
|
//GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.LinearMipmapLinear);
|
|
|
|
|
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.Linear);
|
2023-06-30 02:10:24 +00:00
|
|
|
|
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);
|
2023-07-17 06:51:15 +00:00
|
|
|
|
// FIXME: should we use mipmaps?
|
|
|
|
|
//GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
|
2023-06-30 02:10:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 16:13:13 +00:00
|
|
|
|
private static int maxHandle = -1;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-07 18:24:43 +00:00
|
|
|
|
public class UiGeometry {
|
2023-07-08 04:33:15 +00:00
|
|
|
|
public static Vector2i MIN_WINDOW_SIZE = new(640, 480);
|
2023-07-16 23:13:08 +00:00
|
|
|
|
private static CameraInfo activeCamera = CameraInfo.CANON_EOS_R6M2;
|
2023-07-07 18:24:43 +00:00
|
|
|
|
|
|
|
|
|
public readonly Vector2i WindowSize;
|
2023-07-25 21:18:11 +00:00
|
|
|
|
public readonly Box2i ThumbnailBox;
|
2023-07-07 18:52:36 +00:00
|
|
|
|
public readonly List<Box2i> ThumbnailBoxes = new();
|
2023-07-07 20:00:43 +00:00
|
|
|
|
public readonly Box2i PhotoBox;
|
2023-07-24 17:14:07 +00:00
|
|
|
|
public readonly Box2i StatusBox;
|
2023-07-07 18:24:43 +00:00
|
|
|
|
|
|
|
|
|
public UiGeometry() : this(MIN_WINDOW_SIZE) {}
|
|
|
|
|
|
|
|
|
|
public UiGeometry(Vector2i windowSize) {
|
|
|
|
|
WindowSize = windowSize;
|
2023-07-07 18:52:36 +00:00
|
|
|
|
|
2023-07-26 15:38:04 +00:00
|
|
|
|
int numThumbnails = 15;
|
2023-07-08 04:04:45 +00:00
|
|
|
|
int thumbnailHeight = WindowSize.Y / numThumbnails;
|
2023-07-07 18:52:36 +00:00
|
|
|
|
int thumbnailWidth = (int) 1.0 * thumbnailHeight * activeCamera.Resolution.X / activeCamera.Resolution.Y;
|
2023-07-08 04:04:45 +00:00
|
|
|
|
for (int i = 0; i < numThumbnails; i++) {
|
2023-07-24 16:43:38 +00:00
|
|
|
|
Box2i box = Util.MakeBox(WindowSize.X - thumbnailWidth, i * thumbnailHeight, thumbnailWidth, thumbnailHeight);
|
2023-07-07 18:52:36 +00:00
|
|
|
|
ThumbnailBoxes.Add(box);
|
|
|
|
|
}
|
2023-07-07 20:00:43 +00:00
|
|
|
|
|
2023-07-24 17:14:07 +00:00
|
|
|
|
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);
|
2023-07-25 21:18:11 +00:00
|
|
|
|
ThumbnailBox = new Box2i(ThumbnailBoxes[0].Min.X, ThumbnailBoxes[0].Min.Y, WindowSize.X, WindowSize.Y);
|
2023-07-07 18:52:36 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static class Util {
|
2023-07-23 22:42:08 +00:00
|
|
|
|
public const float PI = (float) Math.PI;
|
|
|
|
|
|
2023-07-24 16:43:38 +00:00
|
|
|
|
public static Box2i MakeBox(int left, int top, int width, int height) {
|
2023-07-07 18:52:36 +00:00
|
|
|
|
return new Box2i(left, top, left + width, top + height);
|
2023-07-07 18:24:43 +00:00
|
|
|
|
}
|
2023-07-24 16:13:13 +00:00
|
|
|
|
|
2023-07-24 16:43:38 +00:00
|
|
|
|
public static Image<Rgba32> MakeImage(float width, float height) {
|
2023-07-24 16:36:44 +00:00
|
|
|
|
return new((int) Math.Ceiling(width), (int) Math.Ceiling(height));
|
2023-07-24 16:13:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 18:28:57 +00:00
|
|
|
|
// https://sirv.com/help/articles/rotate-photos-to-be-upright/
|
|
|
|
|
public static void RotateImageFromExif(Image<Rgba32> image, ushort orientation) {
|
2023-07-25 18:48:02 +00:00
|
|
|
|
if (orientation <= 1) {
|
2023-07-25 18:28:57 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
2023-07-25 19:00:30 +00:00
|
|
|
|
// 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.).
|
2023-07-25 18:48:02 +00:00
|
|
|
|
var operations = new Dictionary<ushort, (RotateMode, FlipMode)> {
|
|
|
|
|
{ 2, (RotateMode.None, FlipMode.Horizontal) },
|
|
|
|
|
{ 3, (RotateMode.Rotate180, FlipMode.None) },
|
2023-07-25 19:02:40 +00:00
|
|
|
|
{ 4, (RotateMode.None, FlipMode.Vertical) },
|
2023-07-25 18:48:02 +00:00
|
|
|
|
{ 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));
|
2023-07-25 18:28:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 16:43:38 +00:00
|
|
|
|
public static Texture RenderText(string text) {
|
|
|
|
|
return RenderText(text, 16);
|
2023-07-24 16:36:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 16:43:38 +00:00
|
|
|
|
public static Texture RenderText(string text, int size) {
|
2023-07-24 16:36:44 +00:00
|
|
|
|
Font font = SystemFonts.CreateFont("Consolas", size, FontStyle.Bold);
|
2023-07-24 16:13:13 +00:00
|
|
|
|
TextOptions options = new(font);
|
2023-07-24 16:36:44 +00:00
|
|
|
|
FontRectangle rect = TextMeasurer.Measure(text, new TextOptions(font));
|
2023-07-24 16:43:38 +00:00
|
|
|
|
Image<Rgba32> image = MakeImage(rect.Width, rect.Height);
|
2023-07-24 16:13:13 +00:00
|
|
|
|
IBrush brush = Brushes.Solid(Color.White);
|
2023-07-24 16:36:44 +00:00
|
|
|
|
// IPen pen = Pens.Solid(Color.Black, 1f);
|
|
|
|
|
// image.Mutate(x => x.DrawText(options, text, brush, pen));
|
|
|
|
|
image.Mutate(x => x.DrawText(options, text, brush));
|
2023-07-24 16:13:13 +00:00
|
|
|
|
Texture texture = new Texture(image);
|
|
|
|
|
image.Dispose();
|
|
|
|
|
return texture;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 15:24:56 +00:00
|
|
|
|
// FIXME: make a real icon stored as a PNG...
|
2023-07-26 14:00:46 +00:00
|
|
|
|
public static OpenTK.Windowing.Common.Input.Image[] RenderAppIcon() {
|
2023-07-26 15:11:06 +00:00
|
|
|
|
int size = 64;
|
2023-07-26 15:14:23 +00:00
|
|
|
|
Font font = SystemFonts.CreateFont("MS Mincho", size, FontStyle.Bold);
|
2023-07-26 14:00:46 +00:00
|
|
|
|
TextOptions options = new(font);
|
2023-07-26 15:11:06 +00:00
|
|
|
|
Image<Rgba32> image = MakeImage(size, size);
|
2023-07-26 14:00:46 +00:00
|
|
|
|
IBrush brush = Brushes.Solid(Color.Black);
|
|
|
|
|
image.Mutate(x => x.DrawText(options, "撮", brush));
|
2023-07-26 15:11:06 +00:00
|
|
|
|
byte[] pixelBytes = new byte[size * size * 4];
|
2023-07-26 14:00:46 +00:00
|
|
|
|
image.CopyPixelDataTo(pixelBytes);
|
2023-07-26 15:11:06 +00:00
|
|
|
|
image.Dispose();
|
|
|
|
|
OpenTK.Windowing.Common.Input.Image opentkImage = new(size, size, pixelBytes);
|
2023-07-26 14:00:46 +00:00
|
|
|
|
return new OpenTK.Windowing.Common.Input.Image[]{ opentkImage };
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 16:43:38 +00:00
|
|
|
|
public static Texture RenderStar(float radius) {
|
2023-07-24 16:13:13 +00:00
|
|
|
|
IPath path = new Star(x: radius, y: radius, prongs: 5, innerRadii: radius * 0.4f, outerRadii: radius, angle: Util.PI);
|
2023-07-24 16:43:38 +00:00
|
|
|
|
Image<Rgba32> image = MakeImage(path.Bounds.Width, path.Bounds.Height);
|
2023-07-24 16:13:13 +00:00
|
|
|
|
image.Mutate(x => x.Fill(Color.White, path));
|
|
|
|
|
Texture texture = new Texture(image);
|
|
|
|
|
image.Dispose();
|
|
|
|
|
return texture;
|
|
|
|
|
}
|
2023-07-07 18:24:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-30 02:10:24 +00:00
|
|
|
|
public class Game : GameWindow {
|
2023-07-08 04:20:23 +00:00
|
|
|
|
public Game(GameWindowSettings gwSettings, NativeWindowSettings nwSettings) : base(gwSettings, nwSettings) {}
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
2023-07-08 04:33:15 +00:00
|
|
|
|
private static Texture TEXTURE_WHITE = new(new Image<Rgba32>(1, 1, new Rgba32(255, 255, 255)));
|
2023-07-24 21:13:19 +00:00
|
|
|
|
private static Texture TEXTURE_BLACK = new(new Image<Rgba32>(1, 1, new Rgba32(0, 0, 0)));
|
2023-07-06 22:31:50 +00:00
|
|
|
|
|
2023-07-07 18:52:36 +00:00
|
|
|
|
UiGeometry geometry = new();
|
2023-07-26 02:19:18 +00:00
|
|
|
|
FpsCounter fpsCounter = new();
|
2023-07-07 18:24:43 +00:00
|
|
|
|
|
2023-07-16 23:13:08 +00:00
|
|
|
|
// Input handling.
|
|
|
|
|
long downTimer = Int64.MaxValue;
|
|
|
|
|
long upTimer = Int64.MaxValue;
|
|
|
|
|
|
2023-07-07 18:52:36 +00:00
|
|
|
|
// Four points, each consisting of (x, y, z, tex_x, tex_y).
|
2023-07-07 18:24:43 +00:00
|
|
|
|
float[] vertices = new float[20];
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
2023-07-07 18:52:36 +00:00
|
|
|
|
// Indices to draw a rectangle from two triangles.
|
2023-06-30 02:10:24 +00:00
|
|
|
|
uint[] indices = {
|
|
|
|
|
0, 1, 3, // first triangle
|
|
|
|
|
1, 2, 3 // second triangle
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
int VertexBufferObject;
|
|
|
|
|
int ElementBufferObject;
|
|
|
|
|
int VertexArrayObject;
|
2023-07-16 23:23:03 +00:00
|
|
|
|
List<Photo> photos = new();
|
2023-07-26 01:24:20 +00:00
|
|
|
|
HashSet<int> loadedImages = new();
|
2023-07-16 23:25:28 +00:00
|
|
|
|
int photoIndex = 0;
|
2023-07-25 22:26:46 +00:00
|
|
|
|
int ribbonIndex = 0;
|
2023-07-08 04:33:15 +00:00
|
|
|
|
Shader shader = new();
|
2023-06-30 02:10:24 +00:00
|
|
|
|
Matrix4 projection;
|
2023-07-18 05:42:59 +00:00
|
|
|
|
float zoomLevel = 0f;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
|
|
|
|
protected override void OnUpdateFrame(FrameEventArgs e) {
|
|
|
|
|
base.OnUpdateFrame(e);
|
2023-07-16 23:13:08 +00:00
|
|
|
|
long now = DateTime.Now.Ticks;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
|
|
|
|
KeyboardState input = KeyboardState;
|
|
|
|
|
|
2023-07-25 22:26:46 +00:00
|
|
|
|
// FIXME: add a confirm dialog before closing. (Also for the window-close button.)
|
2023-07-16 23:13:08 +00:00
|
|
|
|
// Close when Escape is pressed.
|
|
|
|
|
if (input.IsKeyDown(Keys.Escape)) {
|
|
|
|
|
Close();
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 22:38:21 +00:00
|
|
|
|
// Track keyboard repeat times for advancing up/down.
|
|
|
|
|
if (!input.IsKeyDown(Keys.Down)) {
|
|
|
|
|
downTimer = Int64.MaxValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!input.IsKeyDown(Keys.Up)) {
|
|
|
|
|
upTimer = Int64.MaxValue;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-16 23:13:08 +00:00
|
|
|
|
// Look for mouse clicks on thumbnails.
|
2023-07-25 22:38:21 +00:00
|
|
|
|
//
|
|
|
|
|
// Note that we don't bounds-check photoIndex until after all the possible
|
|
|
|
|
// inputs that might affect it. That simplifies this logic significantly.
|
|
|
|
|
if (MouseState.IsButtonPressed(MouseButton.Button1)) {
|
2023-07-08 04:18:31 +00:00
|
|
|
|
for (int i = 0; i < geometry.ThumbnailBoxes.Count; i++) {
|
|
|
|
|
Box2i box = geometry.ThumbnailBoxes[i];
|
|
|
|
|
if (box.ContainsInclusive((Vector2i) MouseState.Position)) {
|
2023-07-25 22:38:21 +00:00
|
|
|
|
photoIndex = ribbonIndex + i;
|
2023-07-08 04:18:31 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 22:38:21 +00:00
|
|
|
|
if (MouseState.IsButtonPressed(MouseButton.Button4)) {
|
|
|
|
|
photoIndex--;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 22:38:21 +00:00
|
|
|
|
if (MouseState.IsButtonPressed(MouseButton.Button5)) {
|
|
|
|
|
photoIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (MouseState.ScrollDelta.Y < 0) {
|
|
|
|
|
photoIndex++;
|
2023-07-16 23:13:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 22:38:21 +00:00
|
|
|
|
if (MouseState.ScrollDelta.Y > 0) {
|
|
|
|
|
photoIndex--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FIXME: make a proper Model class for tracking the state of the controls?
|
|
|
|
|
if (input.IsKeyPressed(Keys.Down) || now > downTimer) {
|
|
|
|
|
downTimer = now + 10000 * 200;
|
|
|
|
|
photoIndex++;
|
|
|
|
|
}
|
|
|
|
|
if (input.IsKeyPressed(Keys.Up) || now > upTimer) {
|
|
|
|
|
upTimer = now + 10000 * 200;
|
|
|
|
|
photoIndex--;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 01:44:22 +00:00
|
|
|
|
if (input.IsKeyPressed(Keys.Home)) {
|
|
|
|
|
photoIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.IsKeyPressed(Keys.End)) {
|
|
|
|
|
photoIndex = photos.Count - 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.IsKeyPressed(Keys.PageDown)) {
|
|
|
|
|
photoIndex += 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.IsKeyPressed(Keys.PageUp)) {
|
|
|
|
|
photoIndex -= 10;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 22:38:21 +00:00
|
|
|
|
// Make sure the photoIndex is actually valid.
|
|
|
|
|
photoIndex = Math.Clamp(photoIndex, 0, photos.Count - 1);
|
|
|
|
|
|
2023-07-23 21:34:42 +00:00
|
|
|
|
if (input.IsKeyDown(Keys.D0) || input.IsKeyDown(Keys.GraveAccent)) {
|
2023-07-18 05:42:59 +00:00
|
|
|
|
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;
|
|
|
|
|
}
|
2023-06-30 02:10:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 01:24:20 +00:00
|
|
|
|
protected override void OnLoad() {
|
2023-06-30 02:10:24 +00:00
|
|
|
|
base.OnLoad();
|
|
|
|
|
|
2023-07-07 05:44:51 +00:00
|
|
|
|
GL.ClearColor(0f, 0f, 0f, 1f);
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
2023-07-24 16:13:13 +00:00
|
|
|
|
GL.Enable(EnableCap.Blend);
|
|
|
|
|
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
|
|
|
|
|
2023-06-30 02:10:24 +00:00
|
|
|
|
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);
|
|
|
|
|
|
2023-07-08 03:29:51 +00:00
|
|
|
|
shader.Init();
|
2023-06-30 02:10:24 +00:00
|
|
|
|
shader.Use();
|
|
|
|
|
|
2023-07-07 18:52:36 +00:00
|
|
|
|
// Because there's 5 floats between the start of the first vertex and the start of the second,
|
|
|
|
|
// the stride is 5 * sizeof(float).
|
2023-06-30 02:10:24 +00:00
|
|
|
|
// 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));
|
|
|
|
|
|
2023-07-26 01:24:20 +00:00
|
|
|
|
// Load photos from a directory.
|
2023-07-25 13:41:13 +00:00
|
|
|
|
// string[] files = Directory.GetFiles(@"c:\users\colin\desktop\photos-test\");
|
2023-07-26 00:35:12 +00:00
|
|
|
|
// string[] files = Directory.GetFiles(@"c:\users\colin\pictures\photos\2023\07\14\");
|
2023-07-26 01:24:20 +00:00
|
|
|
|
// string[] files = Directory.GetFiles(@"G:\DCIM\100EOSR6\");
|
2023-07-25 18:28:57 +00:00
|
|
|
|
// string[] files = Directory.GetFiles(@"C:\Users\colin\Pictures\photos\2018\06\23");
|
2023-07-26 15:38:04 +00:00
|
|
|
|
string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\Germany all\104D7000");
|
2023-07-24 20:39:01 +00:00
|
|
|
|
// string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\many-birds\");
|
2023-07-17 03:45:50 +00:00
|
|
|
|
|
2023-07-17 00:06:37 +00:00
|
|
|
|
for (int i = 0; i < files.Count(); i++) {
|
|
|
|
|
string file = files[i];
|
2023-06-30 02:10:24 +00:00
|
|
|
|
if (file.ToLower().EndsWith(".jpg")) {
|
2023-07-24 21:13:19 +00:00
|
|
|
|
Photo photo = new Photo(file, TEXTURE_BLACK);
|
2023-07-17 06:44:34 +00:00
|
|
|
|
photos.Add(photo);
|
2023-06-30 02:10:24 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-25 20:58:41 +00:00
|
|
|
|
|
2023-07-26 00:25:07 +00:00
|
|
|
|
photos.Sort(ComparePhotosByDate);
|
2023-06-30 02:10:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 00:25:07 +00:00
|
|
|
|
private static int ComparePhotosByDate(Photo x, Photo y) {
|
|
|
|
|
int compare = x.DateTimeOriginal.CompareTo(y.DateTimeOriginal);
|
|
|
|
|
if (compare != 0) {
|
|
|
|
|
return compare;
|
|
|
|
|
}
|
|
|
|
|
// If the photos have the same seconds value, sort by filename
|
|
|
|
|
// (since cameras usually increment the filename for successive shots.)
|
|
|
|
|
return x.Filename.CompareTo(y.Filename);
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-30 02:10:24 +00:00
|
|
|
|
protected override void OnUnload() {
|
|
|
|
|
base.OnUnload();
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-26 01:24:20 +00:00
|
|
|
|
private async void LoadAndUnloadImagesAsync() {
|
2023-07-26 15:38:04 +00:00
|
|
|
|
int minUnloadedImage = Math.Max(0, photoIndex - 20);
|
|
|
|
|
int maxUnloadedImage = Math.Min(photoIndex + 30, photos.Count - 1);
|
|
|
|
|
int minLoadedImage = Math.Max(0, photoIndex - 10);
|
2023-07-26 01:24:20 +00:00
|
|
|
|
int maxLoadedImage = Math.Min(photoIndex + 20, photos.Count - 1);
|
|
|
|
|
// First, unload images that are far outside our window.
|
2023-07-26 01:44:22 +00:00
|
|
|
|
// FIXME: also cancel any in-progress loading tasks that have moved outside our window.
|
2023-07-26 02:00:43 +00:00
|
|
|
|
// FIXME: maybe use an LRU cache for evicting images?
|
|
|
|
|
// FIXME: keep around thumbnail-sized textures?
|
2023-07-26 01:24:20 +00:00
|
|
|
|
foreach (int i in loadedImages) {
|
|
|
|
|
if (i < minUnloadedImage || i > maxUnloadedImage) {
|
2023-07-26 01:44:22 +00:00
|
|
|
|
// Console.WriteLine("unloading " + i);
|
2023-07-26 02:00:43 +00:00
|
|
|
|
loadedImages.Remove(i);
|
|
|
|
|
photos[i].UnloadAsync();
|
2023-07-26 01:24:20 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-26 01:44:22 +00:00
|
|
|
|
|
2023-07-26 01:24:20 +00:00
|
|
|
|
// Then, start loading any images that aren't in our window.
|
|
|
|
|
for (int i = minLoadedImage; i <= maxLoadedImage; i++) {
|
|
|
|
|
if (!loadedImages.Contains(i)) {
|
2023-07-26 01:44:22 +00:00
|
|
|
|
// Console.WriteLine("loading " + i);
|
2023-07-26 01:24:20 +00:00
|
|
|
|
loadedImages.Add(i);
|
|
|
|
|
await Task.Run( () => { photos[i].LoadAsync(); });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-30 02:10:24 +00:00
|
|
|
|
protected override void OnRenderFrame(FrameEventArgs e) {
|
|
|
|
|
base.OnRenderFrame(e);
|
2023-07-26 02:19:18 +00:00
|
|
|
|
fpsCounter.Update();
|
2023-07-17 03:45:50 +00:00
|
|
|
|
|
2023-07-26 01:24:20 +00:00
|
|
|
|
LoadAndUnloadImagesAsync();
|
|
|
|
|
|
2023-06-30 02:10:24 +00:00
|
|
|
|
GL.Clear(ClearBufferMask.ColorBufferBit);
|
2023-07-26 01:24:20 +00:00
|
|
|
|
GL.BindBuffer(BufferTarget.ArrayBuffer, VertexBufferObject); HashSet<int> loadedImages = new();
|
2023-07-07 01:26:50 +00:00
|
|
|
|
GL.ActiveTexture(TextureUnit.Texture0);
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
2023-07-24 16:53:21 +00:00
|
|
|
|
Photo activePhoto = photos[photoIndex];
|
|
|
|
|
Texture active = activePhoto.Texture();
|
2023-06-30 02:52:52 +00:00
|
|
|
|
|
2023-07-07 20:00:43 +00:00
|
|
|
|
// FIXME: make a function for scaling & centering one box on another.
|
2023-07-08 04:30:05 +00:00
|
|
|
|
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);
|
2023-07-18 05:42:59 +00:00
|
|
|
|
if (zoomLevel > 0f) {
|
|
|
|
|
scale = zoomLevel;
|
|
|
|
|
}
|
2023-07-08 04:30:05 +00:00
|
|
|
|
|
|
|
|
|
Vector2i renderSize = (Vector2i) (((Vector2) active.Size) * scale);
|
|
|
|
|
Vector2i center = (Vector2i) geometry.PhotoBox.Center;
|
2023-07-24 16:43:38 +00:00
|
|
|
|
Box2i photoBox = Util.MakeBox(center.X - renderSize.X / 2, center.Y - renderSize.Y / 2, renderSize.X, renderSize.Y);
|
2023-07-07 20:00:43 +00:00
|
|
|
|
DrawTexture(active, photoBox);
|
2023-07-25 21:18:11 +00:00
|
|
|
|
|
|
|
|
|
// Draw thumbnail boxes.
|
2023-07-25 22:26:46 +00:00
|
|
|
|
ribbonIndex = Math.Clamp(photoIndex - (geometry.ThumbnailBoxes.Count - 1) / 2, 0, Math.Max(0, photos.Count - geometry.ThumbnailBoxes.Count));
|
2023-07-25 21:18:11 +00:00
|
|
|
|
DrawFilledBox(geometry.ThumbnailBox, Color4.Black);
|
2023-07-25 22:26:46 +00:00
|
|
|
|
for (int i = 0; i < geometry.ThumbnailBoxes.Count; i++) {
|
|
|
|
|
if (ribbonIndex + i >= photos.Count) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2023-07-07 18:52:36 +00:00
|
|
|
|
Box2i box = geometry.ThumbnailBoxes[i];
|
2023-07-25 22:26:46 +00:00
|
|
|
|
DrawTexture(photos[ribbonIndex + i].Texture(), box);
|
|
|
|
|
if (ribbonIndex + i == photoIndex) {
|
2023-07-07 06:33:01 +00:00
|
|
|
|
DrawBox(box, 5, Color4.Black);
|
|
|
|
|
DrawBox(box, 3, Color4.White);
|
2023-07-07 01:26:50 +00:00
|
|
|
|
}
|
2023-06-30 02:10:24 +00:00
|
|
|
|
}
|
2023-07-24 16:53:21 +00:00
|
|
|
|
|
2023-07-24 17:14:07 +00:00
|
|
|
|
// Draw status box.
|
2023-07-26 15:24:56 +00:00
|
|
|
|
int statusPadding = 2;
|
2023-07-25 13:45:40 +00:00
|
|
|
|
DrawFilledBox(geometry.StatusBox, Color4.Black);
|
2023-07-26 15:24:56 +00:00
|
|
|
|
DrawText(activePhoto.Description(), geometry.StatusBox.Min.X + 80, geometry.StatusBox.Min.Y + statusPadding);
|
|
|
|
|
DrawText($" FPS: {fpsCounter.Fps}", geometry.StatusBox.Max.X - 76, geometry.StatusBox.Min.Y + statusPadding);
|
2023-07-24 16:53:21 +00:00
|
|
|
|
if (activePhoto.Loaded) {
|
2023-07-26 15:24:56 +00:00
|
|
|
|
DrawText($"{(scale * 100):F1}%", geometry.StatusBox.Min.X, geometry.StatusBox.Min.Y + statusPadding);
|
2023-07-24 16:36:44 +00:00
|
|
|
|
}
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
|
|
|
|
SwapBuffers();
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-07 03:48:12 +00:00
|
|
|
|
void DrawTexture(Texture texture, Box2i box) {
|
2023-07-07 03:36:55 +00:00
|
|
|
|
DrawTexture(texture, box, Color4.White);
|
2023-07-07 03:14:55 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-07 03:48:12 +00:00
|
|
|
|
void DrawTexture(Texture texture, Box2i box, Color4 color) {
|
2023-07-07 05:07:58 +00:00
|
|
|
|
GL.Uniform4(shader.GetUniformLocation("color"), color);
|
2023-07-07 03:48:12 +00:00
|
|
|
|
SetVertices(box.Min.X, box.Min.Y, box.Size.X, box.Size.Y);
|
2023-07-07 01:26:50 +00:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-07 03:48:12 +00:00
|
|
|
|
void DrawBox(Box2i box, int thickness, Color4 color) {
|
2023-07-24 16:43:38 +00:00
|
|
|
|
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);
|
2023-07-07 01:26:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-25 13:45:40 +00:00
|
|
|
|
void DrawFilledBox(Box2i box, Color4 color) {
|
|
|
|
|
DrawTexture(TEXTURE_WHITE, Util.MakeBox(box.Min.X, box.Min.Y, box.Size.X, box.Size.Y), color);
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-24 16:53:21 +00:00
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-30 02:10:24 +00:00
|
|
|
|
protected override void OnResize(ResizeEventArgs e) {
|
|
|
|
|
base.OnResize(e);
|
|
|
|
|
Console.WriteLine($"OnResize: {e.Width}x{e.Height}");
|
|
|
|
|
|
2023-07-07 18:24:43 +00:00
|
|
|
|
geometry = new UiGeometry(e.Size);
|
|
|
|
|
|
|
|
|
|
projection = Matrix4.CreateOrthographicOffCenter(0f, e.Width, e.Height, 0f, -1f, 1f);
|
2023-06-30 02:10:24 +00:00
|
|
|
|
GL.UniformMatrix4(shader.GetUniformLocation("projection"), true, ref projection);
|
2023-07-07 18:24:43 +00:00
|
|
|
|
GL.Viewport(0, 0, e.Width, e.Height);
|
2023-06-30 02:10:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-30 02:57:38 +00:00
|
|
|
|
private void SetVertices(float left, float top, float width, float height) {
|
2023-06-30 02:10:24 +00:00
|
|
|
|
// top left
|
|
|
|
|
vertices[0] = left;
|
|
|
|
|
vertices[1] = top;
|
|
|
|
|
vertices[2] = 0f;
|
|
|
|
|
vertices[3] = 0f;
|
|
|
|
|
vertices[4] = 0f;
|
|
|
|
|
|
|
|
|
|
// top right
|
2023-06-30 02:57:38 +00:00
|
|
|
|
vertices[5] = left + width;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
vertices[6] = top;
|
|
|
|
|
vertices[7] = 0f;
|
|
|
|
|
vertices[8] = 1f;
|
|
|
|
|
vertices[9] = 0f;
|
|
|
|
|
|
|
|
|
|
// bottom right
|
2023-06-30 02:57:38 +00:00
|
|
|
|
vertices[10] = left + width;
|
|
|
|
|
vertices[11] = top + height;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
vertices[12] = 0f;
|
|
|
|
|
vertices[13] = 1f;
|
|
|
|
|
vertices[14] = 1f;
|
|
|
|
|
|
|
|
|
|
// bottom left
|
|
|
|
|
vertices[15] = left;
|
2023-06-30 02:57:38 +00:00
|
|
|
|
vertices[16] = top + height;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
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}");
|
2023-07-07 18:52:36 +00:00
|
|
|
|
GameWindowSettings gwSettings = new();
|
2023-07-26 02:19:18 +00:00
|
|
|
|
gwSettings.UpdateFrequency = 30.0;
|
|
|
|
|
gwSettings.RenderFrequency = 30.0;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
2023-07-07 18:52:36 +00:00
|
|
|
|
NativeWindowSettings nwSettings = new();
|
2023-06-30 02:10:24 +00:00
|
|
|
|
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);
|
2023-07-07 18:24:43 +00:00
|
|
|
|
nwSettings.MinimumSize = UiGeometry.MIN_WINDOW_SIZE;
|
2023-06-30 02:10:24 +00:00
|
|
|
|
nwSettings.Title = "Totte";
|
2023-07-26 14:00:46 +00:00
|
|
|
|
nwSettings.IsEventDriven = false;
|
|
|
|
|
nwSettings.Icon = new WindowIcon(Util.RenderAppIcon());
|
2023-06-30 02:10:24 +00:00
|
|
|
|
|
2023-07-08 04:33:15 +00:00
|
|
|
|
using (Game game = new(gwSettings, nwSettings)) {
|
2023-06-30 02:10:24 +00:00
|
|
|
|
game.Run();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|