godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.48k stars 21.26k forks source link

GPUParticles2D colors are being rendered darker than expected #80025

Open georgwacker opened 1 year ago

georgwacker commented 1 year ago

Godot version

v4.2.dev.custom_build [75f9c97de]

System information

Windows 11 - Godot 4.1.1 / master [75f9c97de] - AMD RX6800, Vulkan Forward/Mobile

Issue description

particle_color

Particles from GPUParticles2D are being rendered darker than expected with the Vulkan backend, for both forward and mobile.

I noticed this with Godot 4.1.1 and the issue is still present on 75f9c97de.

Steps to reproduce

Create GPUParticles2D with default ParticlesMaterial, change color, set scale, compare to ColorRect with same RGB value.

Minimal reproduction project

N/A

Calinou commented 1 year ago

This is likely a sRGB-to-linear conversion issue (or vice versa). To confirm this, set the color to the result Color.linear_to_srgb() (or try Color.srgb_to_linear()).

georgwacker commented 1 year ago

I've set the color via:

process_material.set("color", Color("cb70ff").linear_to_srgb())

and it is now showing the correct color.

Color.srgb_to_linear() on the other hand is showing an even darker color.

In order of the scene list: colors

clayjohn commented 1 year ago

This comes from an interesting limitation in the Particles process shaders.

When passing uniform colors to shaders, we have some logic to automatically convert the color to linear if it is being used in a shader where colors should be in linear space (spatial shader, sky shader, etc.) However, Particle process shaders can be either linear space (for 3D shaders) or sRGB space (for 2D shaders). During the actual particles process shader, there is no way to tell if you are going to use the particles in 2D or in 3D.

One option would be to always do the processing in sRGB space, then force 3D users to set the "vertex_is_srgb" flag in their material. But that would break compatibility, so isn't really a good option.

We should probably investigate giving the material some awareness of where it is used. But even that is tricky as process material uniforms are updated independently from the particles. We might be able to figure something out using a shader variation though. As the process update step has knowledge of the particles node and whether it is 2D or 3D.

I guess what could work is the following:

  1. Create a shader variant for 2D and 3D (same as what we already do for the particles copy shader)
  2. Add a wrapper define like ENSURE_COLOR_SPACE() that does a linear to sRGB conversion when 2D is defined, otherwise it does nothing
  3. In the shader compiler code, we detect if the user is accessing a variable with "source_color" and is in a particles shader. If so, we wrap the variable with ENSURE_COLOR_SPACE(). We do something similar for uv coordinates when accessing screen textures and using Multiview
clayjohn commented 1 year ago

I had some more ideas while working on https://github.com/godotengine/godot/pull/80215

I think I have a solution in mind that should be ideal:

  1. Cache an sRGB version of uniform set (as we do for 2D materials in https://github.com/godotengine/godot/pull/80215)
  2. At run time, choose the sRGB version if using a 2D shader, choose the linear version if 3D (this means the particles shader will be in sRGB space for 2D and linear space for 3D
  3. When drawing the particles, convert back to linear if using a linear viewport (feature added in https://github.com/godotengine/godot/pull/80215)

This should make everything "just work" and won't be a lot of work to implement. But it relies on https://github.com/godotengine/godot/pull/80215 being merged first

QbieShay commented 1 year ago

@clayjohn could be also something that we can tie to disable_z flag, since that's mostly used for 2D (a bit hacky tho)

vybr commented 9 months ago

Any update on this @clayjohn? Not well-versed on this stuff but enabling 2D HDR also makes the particle colour ramps display colours incorrectly, but oddly the particle colour is displayed correctly only when it is enabled.

Fire Colour: image

Smoke Colour: image

HDR off (fire particles are correct, smoke is darker than it should be): image

HDR on (fire incorrect, smoke is correct colour): image

Novark commented 8 months ago

@vybr Out of curiosity, do any of your colour ramps contain alpha channel values below 1.0 (i.e., particle transparency)? Your second colour ramp for smoke looks like it might have a white center, with a bit of a feathered (lower alpha?) near the edges, but I can't tell from your image if this is a decrease in alpha, or just a darker grey RGB colour.

My understanding is that the HDR changes will result in normal RGB values appearing slightly different, as they are converted from sRGB gamma-corrected colour space to linear RGB space. I'm still trying to determine if alpha channel values receives this same conversion treatment with Godot's forward renderer, and if so - whether that's the correct way to handle alpha channel colour space conversion. Check #80868 for some related discussion.

Proggle commented 6 days ago

Just as a note, the documentation says that the Color property is multiplied by the texture colors. If the intended behavior is to do something other than an element-by-element multiplication (such as a nonlinear conversion), I think the documentation needs adjustment to describe what the intended behavior is.

Color color = Color(1, 1, 1, 1)

void set_color(value: Color)

Color get_color()

Each particle's initial color. If the GPUParticles2D's texture is defined, it will be multiplied by this color.

Note: color multiplies the particle mesh's vertex colors.

As is, I'm trying to sample pixel colors from a texture to dynamically break it apart into particles, and it's very unexpected to not multiply a (1,1,1,1) white texture by my sampled colors and get those exact sampled colors back out.