/* * ReFuel.StbImage.Viewer - A simple image viewer demo for ReFuel.StbImage. * ------------------------------------------------------------------------ * * Pass an image file as path (or drag and drop over the executable or window) to view * of the file formats stb_image supports. Otherwise the default embedded image will be * shown. * * The demo uses OpenGL2.1 for brevity sake - I did not feel like writing a shader program * for this demo. It is very easy to port this demo to modern OpenGL if desired. Just * replace the FFP calls with the equivalent programmable pipeline calls (glVertexAttribPointer, * glUseShader and its friends). */ using System.Reflection; using System.Runtime.InteropServices; using OpenTK.Graphics.OpenGL; using OpenTK.Mathematics; using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; using ReFuel.Stb; NativeWindow window = new NativeWindow(new NativeWindowSettings() { Profile = ContextProfile.Any, APIVersion = new Version(2, 1), Title = "ReFuel StbImage Viewer", AutoLoadBindings = true, Flags = ContextFlags.Default, }); bool quit = false; int texture = 0; int imageWidth = 0, imageHeight = 0; // This flag is important for OpenGL users, as texture coordinate systems // are the opposite of what most image formats use. Y is up not down. // Does not matter for DX, for example. StbImage.FlipVerticallyOnLoad = true; Vertex[] vertices = new Vertex[] { new Vertex(-1, -1, 0, 0), new Vertex(1, -1, 1, 0), new Vertex(1, 1, 1, 1), new Vertex(-1, -1, 0, 0), new Vertex(1, 1, 1, 1), new Vertex(-1, 1, 0, 1), }; LoadImage(args.Length > 0 ? args[0] : null); GL.ClearColor(Color4.Black); GL.Enable(EnableCap.Texture2D); GL.Enable(EnableCap.Blend); GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); // conventional blending function. GL.EnableClientState(ArrayCap.VertexArray); GL.EnableClientState(ArrayCap.TextureCoordArray); GL.EnableClientState(ArrayCap.ColorArray); window.Closing += (_) => quit = true; window.FramebufferResize += (_) => { ResizeImage(); Paint(); }; window.FileDrop += (args) => LoadImage( args.FileNames .Select(x => new FileInfo(x)) .FirstOrDefault(x => x.Exists)?.FullName); window.WindowState = WindowState.Normal; while (!quit) { NativeWindow.ProcessWindowEvents(true); Paint(); }; void Paint() { GL.Clear(ClearBufferMask.ColorBufferBit); GL.Viewport(0, 0, window.FramebufferSize.X, window.FramebufferSize.Y); GL.BindTexture(TextureTarget.Texture2D, texture); unsafe { fixed (Vertex* pvert = vertices) { // We have to do it this way because the garbage collector may // move the vertex data at any time. The OpenGL client will // handle streaming the data at the glDrawArrays call, unlike // the modern method. GL.VertexPointer(2, VertexPointerType.Float, 32, (nint)(&pvert->X)); GL.TexCoordPointer(2, TexCoordPointerType.Float, 32, (nint)(&pvert->U)); GL.ColorPointer(4, ColorPointerType.Float, 32, (nint)(&pvert->Color)); GL.DrawArrays(PrimitiveType.Triangles, 0, vertices.Length); } } window.Context.SwapBuffers(); } void ResizeImage() { // Regenerates the vertex positions to maintain the aspect ratio and fill the window. float windowAspect = (float)window.FramebufferSize.X / window.FramebufferSize.Y; float imageAspect = (float)imageWidth / imageHeight; float ratio = imageAspect / windowAspect; float widthNDC, heightNDC; if (ratio > 1) { widthNDC = 2.0f; heightNDC = 2.0f / ratio; } else { heightNDC = 2.0f; widthNDC = 2.0f * ratio; } vertices[0].X = -widthNDC / 2; vertices[0].Y = -heightNDC / 2; vertices[1].X = widthNDC / 2; vertices[1].Y = -heightNDC / 2; vertices[2].X = widthNDC / 2; vertices[2].Y = heightNDC / 2; vertices[3].X = -widthNDC / 2; vertices[3].Y = -heightNDC / 2; vertices[4].X = widthNDC / 2; vertices[4].Y = heightNDC / 2; vertices[5].X = -widthNDC / 2; vertices[5].Y = heightNDC / 2; } void LoadImage(string? path) { // Load the given image, or the default if the path is null, or any other error happens. StbImage? image = null; if (path != null && File.Exists(path)) { try { using Stream str = File.OpenRead(path); StbImage.TryLoad(out image, str); } catch { // Ignore } } if (image == null) { using Stream str = Assembly.GetExecutingAssembly().GetManifestResourceStream("default.png")!; image = StbImage.Load(str); } // Boilerplate code for creating a new OpenGL texture. GL.DeleteTexture(texture); texture = GL.GenTexture(); GL.BindTexture(TextureTarget.Texture2D, texture); GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, image.Width, image.Height, 0, image.Format switch { StbiImageFormat.Grey => PixelFormat.Red, StbiImageFormat.GreyAlpha => PixelFormat.Rg, StbiImageFormat.Rgb => PixelFormat.Rgb, StbiImageFormat.Rgba => PixelFormat.Rgba, _ => throw new Exception() }, image.IsFloat ? PixelType.Float : PixelType.UnsignedByte, image.ImagePointer); // Generate mipmaps for better minification. GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); // Enable them. GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); // Set texture wrap mode to clamp to border to prevent bleeding on the image edges. GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToBorder); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToBorder); // For R or RA format images, we need to set the swizzle mask. switch (image.Format) { case StbiImageFormat.Grey: GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int)TextureSwizzle.Red); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int)TextureSwizzle.Red); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int)TextureSwizzle.Red); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int)TextureSwizzle.One); break; case StbiImageFormat.GreyAlpha: GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleR, (int)TextureSwizzle.Red); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleG, (int)TextureSwizzle.Red); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleA, (int)TextureSwizzle.Green); // Yes the last channel is green, we uploaded the texture with the Rg color format. GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureSwizzleB, (int)TextureSwizzle.Red); break; } imageWidth = image.Width; imageHeight = image.Height; ResizeImage(); image.Dispose(); } // Vertex struct for convenience. Padded to 32 bytes for memory alignment. [StructLayout(LayoutKind.Sequential, Size = 32)] struct Vertex(float x, float y, float u, float v) { public float X = x, Y = y, U = u, V = v; public Color4 Color = Color4.White; }