Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add outputs_srgb example (Showing difference between sRGB and linear RGB) #2098

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

faern
Copy link
Contributor

@faern faern commented Feb 16, 2024

I have been struggling a bit with rendering correct colors lately. As part of investigating this, I wanted to get a better understanding of what color spaces the shaders were expected to output. It was not intuitive to me how to change the sRGB configuration in glium, and there are not really any docs around it that I could find. All I found was #2069, talking about making it more intuitive. But that issue/PR did not add any docs or examples. It basically just changed the default value.

So I decided to create an example to show to myself how to do it and what difference it makes (gamma in this case). Figured I cold provide it upstream with these two intended outcomes:

  • Get some feedback that improves my own own understanding of this topic
  • Help others in the future

Ping @jasoneveleth and @justincredible, You might be interested in this since you worked on the previous sRGB issue/PR.

@justincredible
Copy link
Contributor

justincredible commented Feb 17, 2024

Jason's compilation comprises the context, but #2059 per se is a sufficient summary.

The outputs_srgb flag attests the shader program is responsible for outputting sRGB values (if necessary). Otherwise, GL_FRAMEBUFFER_SRGB is enabled and the shader output is automatically converted when writing an sRGB target.

This is certainly easier to explain with a visualization, but the comments at the top of the file may be a bit confused. glium merely toggles the OpenGL colorspace correction and the result is due to glutin's provided default framebuffer.

EDIT:
The left column writes to a Texture2d with both programs, the right column writes to an SrgbTexture2d. The bottom row enables color correction while writing to the default framebuffer, the top row disables color correction.
srgb_example

diff
diff --git a/examples/srgb_shader.rs b/examples/srgb_shader.rs
index cd42171..a2587ae 100644
--- a/examples/srgb_shader.rs
+++ b/examples/srgb_shader.rs
@@ -69,6 +69,8 @@ struct Application {
     pub vertex_buffer: glium::VertexBuffer<Vertex>,
     pub linear_rgb_program: glium::Program,
     pub srgb_program: glium::Program,
+    pub texture_2d: glium::Texture2d,
+    pub srgb_texture_2d: glium::texture::SrgbTexture2d,
 }
 
 impl ApplicationContext for Application {
@@ -92,16 +94,19 @@ impl ApplicationContext for Application {
         }
         let vertex_buffer = { glium::VertexBuffer::new(display, &vertices).unwrap() };
 
+        let texture_2d = glium::Texture2d::empty(display, 800, 600).unwrap();
+        let srgb_texture_2d = glium::texture::SrgbTexture2d::empty(display, 800, 600).unwrap();
+
         Self {
             vertex_buffer,
             linear_rgb_program: create_program(display, false),
             srgb_program: create_program(display, true),
+            texture_2d,
+            srgb_texture_2d,
         }
     }
 
     fn draw_frame(&mut self, display: &Display<WindowSurface>) {
-        let mut frame = display.draw();
-
         // Draw band of linear RGB gradient at the top of the window
         let linear_rgb_uniforms = uniform! {
             matrix: [
@@ -121,27 +126,107 @@ impl ApplicationContext for Application {
             ]
         };
 
-        // Clear the window with some non-grey color, to make the gradients stand out a bit
-        frame.clear_color(0.1, 0.3, 0.1, 1.0);
-        
-        frame
-            .draw(
-                &self.vertex_buffer,
-                NoIndices(PrimitiveType::TriangleStrip),
-                &self.linear_rgb_program,
-                &linear_rgb_uniforms,
-                &Default::default(),
-            )
-            .unwrap();
-        frame
-            .draw(
-                &self.vertex_buffer,
-                NoIndices(PrimitiveType::TriangleStrip),
-                &self.srgb_program,
-                &srgb_uniforms,
-                &Default::default(),
-            )
-            .unwrap();
+        // Draw the gradient with each program on a non-sRGB texture
+        let mut framebuffer_linear = glium::framebuffer::SimpleFrameBuffer::new(
+            display,
+            &self.texture_2d
+        )
+        .unwrap();
+        framebuffer_linear.clear(None, Some((0.1, 0.3, 0.1, 1.0)), false, None, None); // clear_color
+        framebuffer_linear.draw(
+            &self.vertex_buffer,
+            NoIndices(PrimitiveType::TriangleStrip),
+            &self.linear_rgb_program,
+            &linear_rgb_uniforms,
+            &Default::default(),
+        )
+        .unwrap();
+        framebuffer_linear.draw(
+            &self.vertex_buffer,
+            NoIndices(PrimitiveType::TriangleStrip),
+            &self.srgb_program,
+            &srgb_uniforms,
+            &Default::default(),
+        )
+        .unwrap();
+        let mut framebuffer_srgb = glium::framebuffer::SimpleFrameBuffer::new(
+            display,
+            &self.srgb_texture_2d
+        )
+        .unwrap();
+        // Draw the gradients again on an sRGB texture
+        framebuffer_srgb.clear(None, Some((0.1, 0.3, 0.1, 1.0)), true, None, None); // clear_color_srgb
+        framebuffer_srgb.draw(
+            &self.vertex_buffer,
+            NoIndices(PrimitiveType::TriangleStrip),
+            &self.linear_rgb_program,
+            &linear_rgb_uniforms,
+            &Default::default(),
+        )
+        .unwrap();
+        framebuffer_srgb.draw(
+            &self.vertex_buffer,
+            NoIndices(PrimitiveType::TriangleStrip),
+            &self.srgb_program,
+            &srgb_uniforms,
+            &Default::default(),
+        )
+        .unwrap();
+
+        // Blit both textures twice, toggling color correction
+        let mut frame = display.draw();
+        frame.clear(None, None, false, None, None);
+        let rect = glium::Rect {
+            left: 0,
+            bottom: 0,
+            width: 800,
+            height: 600,
+        };
+        frame.blit_from_simple_framebuffer(
+            &framebuffer_linear,
+            &rect,
+            &glium::BlitTarget {
+                left: 0,
+                bottom: 0,
+                width: 400,
+                height: 300,
+            },
+            glium::uniforms::MagnifySamplerFilter::Linear,
+        );
+        frame.blit_from_simple_framebuffer(
+            &framebuffer_srgb,
+            &rect,
+            &glium::BlitTarget {
+                left: 400,
+                bottom: 0,
+                width: 400,
+                height: 300,
+            },
+            glium::uniforms::MagnifySamplerFilter::Linear,
+        );
+        frame.clear(None, None, true, None, None);
+        frame.blit_from_simple_framebuffer(
+            &framebuffer_linear,
+            &rect,
+            &glium::BlitTarget {
+                left: 0,
+                bottom: 300,
+                width: 400,
+                height: 300,
+            },
+            glium::uniforms::MagnifySamplerFilter::Linear,
+        );
+        frame.blit_from_simple_framebuffer(
+            &framebuffer_srgb,
+            &rect,
+            &glium::BlitTarget {
+                left: 400,
+                bottom: 300,
+                width: 400,
+                height: 300,
+            },
+            glium::uniforms::MagnifySamplerFilter::Linear,
+        );
         frame.finish().unwrap();
     }
 }

@faern
Copy link
Contributor Author

faern commented Feb 18, 2024

glium merely toggles the OpenGL ...

But I don't know that part of OpenGL and was hoping I could learn it through the way I'm consuming it (glium). Especially since it has some built in magic in between me and OpenGL that is not 100% obvious to me.

I'm open to suggestions how to better word the description for the example! Or other ways of implementing it that makes it easier to understand.

The outputs_srgb flag ... Otherwise, GL_FRAMEBUFFER_SRGB is enabled

This was not really obvious to me. And the documentation for SpirvProgram::outputs_srgb states: "See SourceCode::outputs_srgb.". But SourceCode has no member of that name. Updating this documentation to be more verbose would be amazing. I would do it myself if I knew what to write, but I think you have a much better understanding of that.

The best would of course be to be able to visualize the pipelines how color values travel from wherever they start to the actual physical monitor. Where gamma correction is added/reversed and what more steps there are. Personally I don't care too much about textures. I have color values in XYZ-format that I get from various colorimetry tools. And I want to write software that renders these values as correct as possible (I'm aware that anything outside the sRGB gamut won't be possible to display of course).

@faern
Copy link
Contributor Author

faern commented Feb 18, 2024

@justincredible Do you have the source code for your four examples available? When you say you enable and disable color correction, what are you referring to exactly?

@justincredible
Copy link
Contributor

There is a git-diff below the image, but I've also provided the altered source here. There is another level to explore with how the background is cleared for both types of textures.

The SpirvProgram docs are referring to these docs. I have not used SPIR-V myself.
The sRGB behaviour is discussed in the texture docs.
The other point of control (besides shader programs) is the Surface::clear function with the color_srgb flag. Most users enable the GL_FRAMEBUFFER_SRGB color correction by calling clear_color.

I'm relatively new to this library and the practicalities of sRGB, so I may be ignorant of some wider context.

@faern
Copy link
Contributor Author

faern commented Feb 19, 2024

The SpirvProgram docs are referring to these docs. I have not used SPIR-V myself.

Aaah. I did not find those. If you search for outputs_srgb in the rustdoc they turn up in a very unhelpful order. The first one is the SpirvProgram::outputs_srgb build method, which has no valuable docs. Second hit is the SpirvProgram::outputs_srgb member, which up until #2099 was not clickable. My separate documentation PR aims to hopefully slightly improve on this :)

The sRGB behaviour is discussed in the texture docs.

Those docs must surely be outdated, no? Because they say:

By default, glium enables the GL_FRAMEBUFFER_SRGB trigger, which expects the output of your fragment shader to be in linear RGB

While the outputs_srgb flag says the flag is enabled by default, and that GL_FRAMEBUFFER_SRGB is enabled only when outputs_srgb is disabled. So they contradict each other right now.

@faern
Copy link
Contributor Author

faern commented Feb 20, 2024

For my own understanding, what's the correct/simplest way to just render linear RGB values directly to the window framebuffer? I don't have any textures. I just have linear RGB data that I want to render as simple but correct as possible. I can encode them to srgb also, but if I can avoid that step and have OpenGL do whatever color correction is needed that's just simpler.

My understanding is that I should then do clear_color for clearing the framebuffer and set outputs_srgb: false in my shader program?

@justincredible
Copy link
Contributor

clear_color and outputs_srgb: false each enable color correction for their operation, but this only affects sRGB targets.

clear_color_srgb and outputs_srgb: true are what you want for the window framebuffer.

@faern
Copy link
Contributor Author

faern commented Feb 21, 2024

Given that my shader writes srgb values, no? What if my shader writes linear rgb to the display framebuffer?

Edit: is the display framebuffer always linear, never an srgb target?

EDIT2: No, they differ. The display framebuffers I get when using glium via the gtk4 crate are not sRGB images, but the targets set up by the examples in this repository are sRGB images. So in my gtk4 program it does not matter what I set anything to, no color correction is applied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants