From 2ab0878f88b8a4f70381cd99927da1ff127945c3 Mon Sep 17 00:00:00 2001 From: John-Mark Allen Date: Mon, 5 Jul 2021 22:53:23 +0100 Subject: [PATCH] Fix sRGB support and comments Fix sRGB support, now when initialising a Renderer you can explicitly choose whether to output colors in linear or sRGB color spaces. Fix examples to show how to render these properly. Fix comments in examples --- imgui-glow-renderer/examples/01_basic.rs | 4 +- imgui-glow-renderer/examples/02_triangle.rs | 20 +-- .../examples/03_triangle_gles.rs | 19 ++- .../examples/04_custom_textures.rs | 16 +- imgui-glow-renderer/examples/utils/mod.rs | 22 +-- imgui-glow-renderer/src/lib.rs | 140 ++++++++++++++---- 6 files changed, 145 insertions(+), 76 deletions(-) diff --git a/imgui-glow-renderer/examples/01_basic.rs b/imgui-glow-renderer/examples/01_basic.rs index aea797e..485397d 100644 --- a/imgui-glow-renderer/examples/01_basic.rs +++ b/imgui-glow-renderer/examples/01_basic.rs @@ -13,15 +13,13 @@ type Window = WindowedContext; fn main() { // Common setup for creating a winit window and imgui context, not specifc - // to this renderer at all ecept that glutin is used to create the window + // to this renderer at all except that glutin is used to create the window // since it will give us access to a GL context let (event_loop, window) = create_window(); let (mut winit_platform, mut imgui_context) = imgui_init(&window); // OpenGL context from glow let gl = glow_context(&window); - // Outputting to screen, we want an sRGB framebuffer - unsafe { gl.enable(glow::FRAMEBUFFER_SRGB) }; // OpenGL renderer from this crate let mut ig_renderer = imgui_glow_renderer::AutoRenderer::initialize(gl, &mut imgui_context) diff --git a/imgui-glow-renderer/examples/02_triangle.rs b/imgui-glow-renderer/examples/02_triangle.rs index 0ad1a44..283b011 100644 --- a/imgui-glow-renderer/examples/02_triangle.rs +++ b/imgui-glow-renderer/examples/02_triangle.rs @@ -1,8 +1,4 @@ -//! A basic example showing imgui rendering together with some custom rendering. -//! -//! Note this example uses `RendererBuilder` rather than `auto_renderer` and -//! (because we're using the default "trivial" `ContextStateManager`) -//! therefore does not attempt to backup/restore OpenGL state. +//! A basic example showing imgui rendering on top of a simple custom scene. use std::time::Instant; @@ -40,21 +36,17 @@ fn main() { window.window().request_redraw(); } glutin::event::Event::RedrawRequested(_) => { - { - let gl = ig_renderer.gl_context(); - // This is required because, without the `StateBackupCsm` - // (which is provided by `auto_renderer` but not - // `RendererBuilder` by default), the OpenGL context is left - // in an arbitrary, dirty state - unsafe { gl.disable(glow::SCISSOR_TEST) }; - tri_renderer.render(gl); - } + // Render your custom scene, note we need to borrow the OpenGL + // context from the `AutoRenderer`, which takes ownership of it. + tri_renderer.render(ig_renderer.gl_context()); let ui = imgui_context.frame(); ui.show_demo_window(&mut true); winit_platform.prepare_render(&ui, window.window()); let draw_data = ui.render(); + + // Render imgui on top of it ig_renderer .render(&draw_data) .expect("error rendering imgui"); diff --git a/imgui-glow-renderer/examples/03_triangle_gles.rs b/imgui-glow-renderer/examples/03_triangle_gles.rs index 32bcb7c..104e6ac 100644 --- a/imgui-glow-renderer/examples/03_triangle_gles.rs +++ b/imgui-glow-renderer/examples/03_triangle_gles.rs @@ -1,7 +1,7 @@ -//! A basic example showing imgui rendering together with some custom rendering +//! A basic example showing imgui rendering on top of a simple custom scene //! using OpenGL ES, rather than full-fat OpenGL. //! -//! Note this example uses `Renderer` rather than `OwningRenderer` and +//! Note this example uses `Renderer` rather than `AutoRenderer` and //! therefore requries more lifetime-management of the OpenGL context. use std::time::Instant; @@ -18,15 +18,17 @@ fn main() { let (mut winit_platform, mut imgui_context) = utils::imgui_init(&window); let gl = utils::glow_context(&window); + // When using `Renderer`, we need to create a texture map let mut texture_map = imgui_glow_renderer::SimpleTextureMap::default(); + + // When using `Renderer`, we specify whether or not to output sRGB colors. + // Since we're drawing to screen and using OpenGL ES (which doesn't support + // `GL_FRAMEBUFFER_SRGB`) then we do need to convert to sRGB in the shader. let mut ig_renderer = - imgui_glow_renderer::Renderer::initialize(&gl, &mut imgui_context, &mut texture_map) + imgui_glow_renderer::Renderer::initialize(&gl, &mut imgui_context, &mut texture_map, true) .expect("failed to create renderer"); // Note the shader header now needs a precision specifier - let tri_renderer = Triangler::new( - &gl, - "#version 300 es\nprecision mediump float;\n#define IS_GLES", - ); + let tri_renderer = Triangler::new(&gl, "#version 300 es\nprecision mediump float;"); let mut last_frame = Instant::now(); event_loop.run(move |event, _, control_flow| { @@ -47,6 +49,7 @@ fn main() { window.window().request_redraw(); } glutin::event::Event::RedrawRequested(_) => { + // Draw custom scene tri_renderer.render(&gl); let ui = imgui_context.frame(); @@ -54,6 +57,8 @@ fn main() { winit_platform.prepare_render(&ui, window.window()); let draw_data = ui.render(); + + // Render imgui on top ig_renderer .render(&gl, &texture_map, &draw_data) .expect("error rendering imgui"); diff --git a/imgui-glow-renderer/examples/04_custom_textures.rs b/imgui-glow-renderer/examples/04_custom_textures.rs index 1f98e6f..9bf4403 100644 --- a/imgui-glow-renderer/examples/04_custom_textures.rs +++ b/imgui-glow-renderer/examples/04_custom_textures.rs @@ -1,5 +1,9 @@ //! Example showing the same functionality as //! `imgui-examples/examples/custom_textures.rs` +//! +//! Not that the texture uses the internal format `glow::SRGB`, so that +//! OpenGL automatically converts colors to linear space before the shaders. +//! The renderer assumes you set this internal format correctly like this. use std::{io::Cursor, time::Instant}; @@ -18,9 +22,15 @@ fn main() { let (event_loop, window) = utils::create_window("Custom textures", glutin::GlRequest::Latest); let (mut winit_platform, mut imgui_context) = utils::imgui_init(&window); let gl = utils::glow_context(&window); + // This time, we tell OpenGL this is an sRGB framebuffer and OpenGL will + // do the conversion to sSGB space for us after the fragment shader. + unsafe { gl.enable(glow::FRAMEBUFFER_SRGB) }; let mut textures = imgui::Textures::::default(); - let mut ig_renderer = Renderer::initialize(&gl, &mut imgui_context, &mut textures) + // Note that `output_srgb` is `false`. This is because we set + // `glow::FRAMEBUFFER_SRGB` so we don't have to manually do the conversion + // in the shader. + let mut ig_renderer = Renderer::initialize(&gl, &mut imgui_context, &mut textures, false) .expect("failed to create renderer"); let textures_ui = TexturesUi::new(&gl, &mut textures); @@ -120,7 +130,7 @@ impl TexturesUi { gl.tex_image_2d( glow::TEXTURE_2D, 0, - glow::RGB as _, + glow::RGB as _, // When generating a texture like this, you're probably working in linear color space WIDTH as _, HEIGHT as _, 0, @@ -262,7 +272,7 @@ impl Lenna { gl.tex_image_2d( glow::TEXTURE_2D, 0, - glow::SRGB as _, + glow::SRGB as _, // image file has sRGB encoded colors width as _, height as _, 0, diff --git a/imgui-glow-renderer/examples/utils/mod.rs b/imgui-glow-renderer/examples/utils/mod.rs index c33bf53..f5c0287 100644 --- a/imgui-glow-renderer/examples/utils/mod.rs +++ b/imgui-glow-renderer/examples/utils/mod.rs @@ -23,11 +23,7 @@ pub fn create_window(title: &str, gl_request: GlRequest) -> (EventLoop<()>, Wind } pub fn glow_context(window: &Window) -> glow::Context { - unsafe { - let gl = glow::Context::from_loader_function(|s| window.get_proc_address(s).cast()); - gl.enable(glow::FRAMEBUFFER_SRGB); - gl - } + unsafe { glow::Context::from_loader_function(|s| window.get_proc_address(s).cast()) } } pub fn imgui_init(window: &Window) -> (WinitPlatform, imgui::Context) { @@ -53,7 +49,6 @@ pub fn imgui_init(window: &Window) -> (WinitPlatform, imgui::Context) { pub struct Triangler { pub program: ::Program, pub vertex_array: ::VertexArray, - is_gles: bool, } impl Triangler { @@ -93,7 +88,6 @@ in vec4 color; out vec4 frag_color; vec4 linear_to_srgb(vec4 linear_color) { -#ifdef IS_GLES vec3 linear = linear_color.rgb; vec3 selector = ceil(linear - 0.0031308); vec3 less_than_branch = linear * 12.92; @@ -102,10 +96,6 @@ vec4 linear_to_srgb(vec4 linear_color) { mix(less_than_branch, greater_than_branch, selector), linear_color.a ); -#else - // For non-GLES, GL_FRAMEBUFFER_SRGB handles this for free - return linear_color; -#endif } void main() { @@ -149,21 +139,13 @@ void main() { Self { program, vertex_array, - is_gles: imgui_glow_renderer::versions::GlVersion::read(gl).is_gles, } } } pub fn render(&self, gl: &glow::Context) { unsafe { - if self.is_gles { - // Specify clear color in sRGB space, since GL_FRAMEBUFFER_SRGB - // is not supported - gl.clear_color(0.05, 0.05, 0.1, 1.0); - } else { - // Specify clear color in linear space - gl.clear_color(0.004, 0.004, 0.01, 1.0); - } + gl.clear_color(0.05, 0.05, 0.1, 1.0); gl.clear(glow::COLOR_BUFFER_BIT); gl.use_program(Some(self.program)); gl.bind_vertex_array(Some(self.vertex_array)); diff --git a/imgui-glow-renderer/src/lib.rs b/imgui-glow-renderer/src/lib.rs index d3c879e..393c16c 100644 --- a/imgui-glow-renderer/src/lib.rs +++ b/imgui-glow-renderer/src/lib.rs @@ -1,3 +1,45 @@ +//! Renderer for `[imgui-rs]` using the `[glow]` library for OpenGL. +//! +//! This is heavily influenced by the +//! [example from upstream](https://github.com/ocornut/imgui/blob/fe245914114588f272b0924538fdd43f6c127a26/backends/imgui_impl_opengl3.cpp). +//! +//! # Basic usage +//! +//! A few code examples are provided in the source. +//! +//! In short, create either an `[AutoRenderer]` (for basic usage) or `[Renderer]` +//! (for slightly more customizable operation), then call the `render(...)` +//! method with draw data from `imgui-rs`. +//! +//! # OpenGL (ES) versions +//! +//! This renderer is expected to work with OpenGL version 3.3 and above, and +//! OpenGL ES version 3.0 or above. This should cover the vast majority of even +//! fairly dated hardware. Please submit an issue for any incompatibilities +//! found with these OpenGL versions, pull requests to extend support to earlier +//! versions are welcomed. +//! +//! # Scope +//! +//! Consider this an example renderer. It is intended to be sufficent for simple +//! applications running imgui-rs as the final rendering step. If your application +//! has more specific needs, it's probably best to write your own renderer, in +//! which case this can be a useful starting point. +//! +//! # sRGB +//! +//! When outputting colors to a screen, colors need to be converted from a +//! linear color space to a non-linear space matching the monitor (e.g. sRGB). +//! When using the `[AutoRenderer]`, this library will convert colors to sRGB +//! as a step in the shader. When initialising a `[Renderer]`, you can choose +//! whether or not to include this step in the shader or not when calling +//! `[Renderer::initialize]`. +//! +//! This library also assumes that textures have their internal format +//! set appropriately when uploaded to OpenGL. That is, assuming your texture +//! is sRGB (if you don't know, it probably is) the `internal_format` is +//! one of the `SRGB*` values. + use std::{borrow::Cow, error::Error, fmt::Display, mem::size_of}; use imgui::internal::RawWrapper; @@ -38,9 +80,10 @@ impl< { } -/// Renderer which owns the OpenGL context and handles textures itself. -/// Useful for simple applications, but more complicated applications may prefer -/// to use `[Renderer]`. +/// Renderer which owns the OpenGL context and handles textures itself. Also +/// converts all output colors to sRGB for display. Useful for simple applications, +/// but more complicated applications may prefer to use `[Renderer]`, or even +/// write their own renderer based on this code. /// /// OpenGL context is still available to the rest of the application through /// the `[gl_context]` method. @@ -56,7 +99,7 @@ impl AutoRenderer { /// result in an error. pub fn initialize(gl: G, imgui_context: &mut imgui::Context) -> Result { let mut texture_map = SimpleTextureMap::default(); - let renderer = Renderer::initialize(&gl, imgui_context, &mut texture_map)?; + let renderer = Renderer::initialize(&gl, imgui_context, &mut texture_map, true)?; Ok(Self { gl, texture_map, @@ -101,6 +144,8 @@ impl Drop for AutoRenderer { } } +/// Main renderer. Borrows the OpenGL context and [texture map](TextureMap) +/// when required. pub struct Renderer { shaders: Shaders, state_backup: GlStateBackup, @@ -115,6 +160,22 @@ pub struct Renderer { } impl Renderer { + /// Create the renderer, initialising OpenGL objects and shaders. + /// + /// `output_srgb` controls whether the shader outputs sRGB colors, or linear + /// RGB colors. In short: + /// - If you're outputting to the screen and haven't specified the framebuffer + /// is sRGB (e.g. with `gl.enable(glow::FRAMEBUFFER_SRGB)`), then you probably + /// want `output_srgb=true`. + /// - If you're outputting to a screen with an sRGB framebuffer (e.g. with + /// `gl.enable(glow::FRAMEBUFFER_SRGB)`), then you probably want + /// `output_srgb=false`, as OpenGL will convert to sRGB itself. + /// - If you're not outputting to some intermediate framebuffer, then you + /// probably want `output_srgb=false` to keep the colours in linear + /// color space, and then convert them to sRGB at some later stage. + /// - OpenGL ES doesn't support sRGB framebuffers, so you almost always + /// want `output_srgb=true`. + /// /// # Errors /// Any error initialising the OpenGL objects (including shaders) will /// result in an error. @@ -122,6 +183,7 @@ impl Renderer { gl: &G, imgui_context: &mut imgui::Context, texture_map: &mut T, + output_srgb: bool, ) -> Result { #![allow( clippy::similar_names, @@ -157,7 +219,7 @@ impl Renderer { let font_atlas_texture = prepare_font_atlas(gl, imgui_context.fonts(), texture_map)?; - let shaders = Shaders::new(gl, gl_version)?; + let shaders = Shaders::new(gl, gl_version, output_srgb)?; let vbo_handle = unsafe { gl.create_buffer() }.map_err(InitError::CreateBufferObject)?; let ebo_handle = unsafe { gl.create_buffer() }.map_err(InitError::CreateBufferObject)?; @@ -183,6 +245,8 @@ impl Renderer { Ok(out) } + /// This must be called before being dropped to properly free OpenGL + /// resources. pub fn destroy(&mut self, gl: &G) { if self.is_destroyed { return; @@ -487,38 +551,46 @@ impl Renderer { } } +/// Trait for mapping imgui texture IDs to OpenGL textures. +/// +/// `[register]` should be called after uploading a texture to OpenGL to get a +/// `[imgui::TextureId]` corresponding to it. +/// +/// Then `[gl_texture]` can be called to find the OpenGL texture corresponding to +/// that `[imgui::TextureId]`. pub trait TextureMap { - fn gl_texture(&self, imgui_texture: imgui::TextureId) -> Option; - fn register(&mut self, gl_texture: glow::Texture) -> Option; + + fn gl_texture(&self, imgui_texture: imgui::TextureId) -> Option; } -/// Texture map where the imgui texture ID is simply the OpenGL texture ID +/// Texture map where the imgui texture ID is simply numerically equal to the +/// OpenGL texture ID. #[derive(Default)] pub struct SimpleTextureMap(); impl TextureMap for SimpleTextureMap { + #[inline(always)] + fn register(&mut self, gl_texture: glow::Texture) -> Option { + Some(imgui::TextureId::new(gl_texture as _)) + } + #[inline(always)] fn gl_texture(&self, imgui_texture: imgui::TextureId) -> Option { #[allow(clippy::cast_possible_truncation)] Some(imgui_texture.id() as _) } - - #[inline(always)] - fn register(&mut self, gl_texture: glow::Texture) -> Option { - Some(imgui::TextureId::new(gl_texture as _)) - } } -/// `Textures` from the `imgui` crate is a simple choice for a texture map +/// `[imgui::Textures]` is a simple choice for a texture map. impl TextureMap for imgui::Textures { - fn gl_texture(&self, imgui_texture: imgui::TextureId) -> Option { - self.get(imgui_texture).copied() - } - fn register(&mut self, gl_texture: glow::Texture) -> Option { Some(self.insert(gl_texture)) } + + fn gl_texture(&self, imgui_texture: imgui::TextureId) -> Option { + self.get(imgui_texture).copied() + } } /// This OpenGL state backup is based on the upstream OpenGL example from @@ -718,8 +790,9 @@ struct Shaders { } impl Shaders { - fn new(gl: &G, gl_version: GlVersion) -> Result { - let (vertex_source, fragment_source) = Self::get_shader_sources(gl, gl_version)?; + fn new(gl: &G, gl_version: GlVersion, output_srgb: bool) -> Result { + let (vertex_source, fragment_source) = + Self::get_shader_sources(gl, gl_version, output_srgb)?; let vertex_shader = unsafe { gl.create_shader(glow::VERTEX_SHADER) }.map_err(ShaderError::CreateShader)?; @@ -783,7 +856,11 @@ impl Shaders { }) } - fn get_shader_sources(gl: &G, gl_version: GlVersion) -> Result<(String, String), ShaderError> { + fn get_shader_sources( + gl: &G, + gl_version: GlVersion, + output_srgb: bool, + ) -> Result<(String, String), ShaderError> { const VERTEX_BODY: &str = r#" layout (location = 0) in vec2 position; layout (location = 1) in vec2 uv; @@ -820,7 +897,6 @@ uniform sampler2D tex; layout (location = 0) out vec4 out_color; vec4 linear_to_srgb(vec4 linear_color) { -#ifdef IS_GLES vec3 linear = linear_color.rgb; vec3 selector = ceil(linear - 0.0031308); vec3 less_than_branch = linear * 12.92; @@ -829,14 +905,15 @@ vec4 linear_to_srgb(vec4 linear_color) { mix(less_than_branch, greater_than_branch, selector), linear_color.a ); -#else - // For non-GLES, GL_FRAMEBUFFER_SRGB handles this for free - return linear_color; -#endif } void main() { - out_color = linear_to_srgb(fragment_color * texture(tex, fragment_uv.st)); + vec4 linear_color = fragment_color * texture(tex, fragment_uv.st); +#ifdef OUTPUT_SRGB + out_color = linear_to_srgb(linear_color); +#else + out_color = linear_color; +#endif } "#; @@ -879,11 +956,16 @@ void main() { body = VERTEX_BODY, ); let fragment_source = format!( - "#version {major}{minor}{es_extras}\n{body}", + "#version {major}{minor}{es_extras}{defines}\n{body}", major = major, minor = minor * 10, es_extras = if is_gles { - " es\nprecision mediump float;\n#define IS_GLES" + " es\nprecision mediump float;" + } else { + "" + }, + defines = if output_srgb { + "\n#define OUTPUT_SRGB" } else { "" },