godot
godot copied to clipboard
Sprite with shader that modifies COLOR ignores modulate property
Godot version
d696bd1
System information
W10
Issue description
When you add a simple shader that changes COLOR
, modulate property is ignored. It was fine in 3.x
Steps to reproduce
- Add Sprite2D node
- Apply this shader
shader_type canvas_item;
void fragment() {
COLOR = texture(TEXTURE, UV);
}
- Try changing modulate property
Minimal reproduction project
No response
EDIT: Reduz suggested that it might be intended, because the way it works has changed. In that case, it's a documentation issue about how it can be achieved now.
Modulation also seems to not work with polygon2d and line2d nodes, in most/all configurations that I can tell. For instance no change when textured (the tooltip says modulate applies to textures), untextured, or when using vertex colors.
Though the most sense of having this (aside from textured polygons) is for vertex colors due to their complexity, which would really benefit here for instance if you wanted something grayscale like a cloud to be recolored for different effects (potions, clouds, team colors, having a library to re-use simple art in different situations etc) which does work in 3.3.2.
So hopefully this isn't intended, at least without something to replace it (like a palette system so you can alter colors and they change all vertices with that index, especially if there was a way to add+control color mixing). With clip children
(when fixed+no blending, see 49198) it might be something that could be wrangled into something dynamic as-needed by someone clever... but that isn't quite as quick and easy as modulate is.
Particularly basic polygons/lines don't really benefit from modulate individually, but there might be situations where you have many nodes that could benefit from it. Although a simple 'block modulate' option might be useful, even better if there were some sort of tag/layer system.
Somewhat different, but issue #25120 is related. Because in previous versions (3.1 beta 1 and before) you could have vertex colors along with vertices that still use the color property instead. Which was useful in a similar-but-different way. Not that I think it was possible to have much control over which vertices had vertex color and which ones used the color property (but if your colors were vaguely in the same area, it might work).
Modulation also seems to not work with polygon2d and line2d nodes, in most/all configurations that I can tell. For instance no change when textured (the tooltip says modulate applies to textures), untextured, or when using vertex colors.
https://github.com/godotengine/godot/pull/49863 will likely fix this issue.
But the problem @KoBeWi has here is different, it is unlikely to be fixed by this PR.
Still occurring in Godot 4 alpha 3.
In Godot 3 multiplication with modulation color happened implicitly unless MODULATE
was used explicitly in shader code.
meanwhile, MODULATE
was removed from the fragment built-ins entirely - presumably intentionally as the docs already reflect the change.
Fragment built-ins in stable docs
Fragment built-ins in latest docs
Reduz suggested that it might be intended, because the way it works has changed. In that case, it's a documentation issue about how it can be achieved now.
I haven't been able to figure out "the new way" to get the modulation color to work with shaders. If anyone knows more, enlightenment would be appreciated.
it now works like this:
shader_type canvas_item;
void fragment() {
COLOR *= texture(TEXTURE, UV);
}
would be nice to hear a confirmation as for whether it is intended like that.
It works in the original overly-simplified example case, but for any other shader that actually does something with colors this will break.
Extra details on comment https://github.com/godotengine/godot/issues/64996#issuecomment-1229508497
I took a look into this today. Unfortunately it appears the current behaviour is intended and will be quite difficult to change.
Old behaviour
In Godot 3.x MODULATE was its own built in variable. It was passed into the shader as either a uniform or a vertex attribute. If it wasn't used then COLOR was multiplied by MODULATE in the fragment shader after the user fragment function runs. If MODULATE was used then it was up to the user to multiply it in.
Similarly COLOR represented the incoming color from the vertex function which was always the vertex color as modified by the vertex function (usually just passed through). If the user did not use COLOR in the fragment function then the fragment shader would multiply it by the texture's color (texture(TEXTURE, UV)
).
New Behaviour
MODULATE is gone and instead COLOR is multiplied by the modulate value on the CPU before passing it to the shader (unless vertex colors are used, then it is multiplied in the vertex shader before being exposed to the user). Basically it i done this way so that we can easily render sprites that have different MODULATE colors without changing much state (in GLES3 it allows us to batch items).
So now COLOR in the vertex shader represents COLOR * MODULATE and MODULATE is gone completely.
In the fragment shader COLOR is always multiplied by the texture's color (texture(TEXTURE, UV)
) before the user fragment function.
Discussion
This means we have two problems:
- You can't access MODULATE or COLOR on their own
- You can't even access MODULATE * COLOR on its own unless you create a varying.
Number 1 could be solved but the options come with tradeoffs:
option 1: Compress COLOR and MODULATE into vec2s and pass them in separately. So instead of vec4(COLOR * MODULATE) we would have vec4(vec2(COLOR), vec2(MODULATE)). In the shader they could be accessed as COLOR and MODULATE still they would just be compress to 16 bits. This would be fine for most colors as the default color picker only exposes 8 bits per channel colors anyway. But it may cause problems if users use "raw" colors that go well outside the 0-1 range.
edit: Of course it isn't this simple. primitive commands (lines, primitives) already compress their colors to 16 bits and so they don't have any way to compress further. Accordingly option 1 won't really work
option 2: We could change the way batching works and pass some data in on a per-batch basis (right now all necessary data is packed into 128 bytes per item). This would require implementing batching in the Vulkan renderer.
The downside to both option 1 and option 2 is that they will incur a few extra calculations in the vertex shader (unpacking color and modulate) and they will waste a varying. Mobile devices have limited numbers of varyings so users will be more limited in what they can use.
Number 2 can be solved by doing what was done in 3.x. I.e. only multiply in the texture value if COLOR is unused in the fragment function. This behaviour was very confusing for beginners as they often expected COLOR to contain the texture color.
@clayjohn Thanks a lot for this explanation! Just ran into a problem today where I would not be able to animate the modulate property to make a sprite fade in/out that had a mask shader.
After reading your explanation I just had to change the shader to do:
COLOR.a *= vmask.a;
As a workaround to make the shader behave the same way it used to in Godot 3.x, as in our case we were not using the MODULATE built in variable.
yea I've also been able to restore 3.x functionality in all of my affected shaders by doing something similar than eh-jogos (as I've mentioned a couple of months back). most of the time, it's fade effects using the alpha channel of modulate. so from my perspective all that's missing here is proper documentation.
it would be nice to have separate access to modulate in some way. For example, one of the shaders I'm using maps the rgb channels of the texture onto 3 arbitrary colors. I can of course use TEXTURE to get the source colors, but it would be nice to be able to modulate the resulting colors as well. (Obviously I can't just use COLOR here because it will create unexpected results- instead of tinting the colors, it will just interfere with the mapping.)
it would be nice to have separate access to modulate in some way.
I'm running into the same. I needed a way to get a per-alternative-tile attribute into the shader for a TileMap I'm rendering, and the "modulate" property (which ends up in the vertex color when rendering a tile) seemed the only choice (without creating a new material for every variation).
However, COLOR
didn't give me access to that, as I expected. It's already multiplied with the texture. I created a workaround by adding a varying that tracks the COLOR
that comes into the vertex shader. I think this means it's doing duplicate work as that value has to be around somewhere.
What is the reason this is considered 4.x
rather than 4.0
, considering that this worked fine in 3.x
? I read this comment by @clayjohn and I understand that architecturally from an implementation standpoint this is "by design". However, from a usability perspective, it looks like a bug to me.
One workaround for 4.0
could be to show a warning next to the modulate colour picker in case a shader is configured to warn the user about the impact. (I spent 3 hours yesterday trying to figure out why the modulate does not apply only to realise it is unique to Godot 4)
Literally what you said in the second sentence, it's by design. 4.0 is a completely different renderer that need not comply to the same rules 3.x did
@Zireael07 there is a difference between "renderer design" and "UX design" - from a user perspective it is broken. The user does not understand necessarily the internals of a renderer, at least Godot currently does not make it clear to the user that there is a problem. If the renderer does not support something but the user still tries to configure the modulate property without it having any effect, at least there should be some sort of warning to the user that they are doing something wrong.
Phrased differently: what is the purpose of having the modulate
property when a shader is configured and it won't have any effect?
Knowing how it works in 4.0, I adjusted my old shaders and was able to successfully fix most of them. However in one shader I absolutely needed the modulate to be separate, so what I did was just passing it as a color parameter. Is this the intended way now? Maybe MODULATE could be available as a built-in parameter? (and would only be passed to the shader if used)
Phrased differently: what is the purpose of having the modulate property when a shader is configured and it won't have any effect?
The modulate
does have effect. It's part of the COLOR. The shader from your original issue could probably work like this:
COLOR = vec4(texture(TEXTURE, UV).rgb, COLOR.a);
The modulate does have effect. It's part of the COLOR.
Thank you for clarifying this.
Maybe MODULATE could be available as a built-in parameter? (and would only be passed to the shader if used)
I would very much like that. The problem I'm currently facing is, that I want to offset the UV coordinate in the fragment shader with help of 2D noise, followed by a texture(TEXTURE, UV + noise)
lookup to create a 'wobble' effect. But at the same time, a tween can change the alpha component of modulate to fade the sprite in and out.
Because COLOR contains the original texture alpha multiplied by modulate alpha, I can't separate them. But I need modulate alpha alone, to multiply it with the new value from the lookup above to get a correct value.
I could pass modulate in as another uniform, but that's such a terrible hack.
Totally agreed. Having a modulate built in parameter would allow you to simply just multiply color by modulate, instead of passing in modulate every time it is changed
In the fragment shader COLOR is always multiplied by the texture's color (
texture(TEXTURE, UV)
) before the user fragment function.
Always? Is this not a complete waste of a texture read if the user doesn't need that particular value?
Also got this issue today with my custom shader to increase brightness.
I found the following workaround (besides adding a custom parameter "modulate", assuming the point is to benefit from the integrated hierarchical modulate system):
Since COLOR = MODULATE * texture(TEXTURE, UV), we should re-divide it by texture(TEXTURE, UV) to get MODULATE:
vec4 tex = texture(TEXTURE, UV);
float modulate = COLOR / tex;
# Work with tex to compute wanted color `col`, then at the end of fragment():
COLOR = col * modulate;
At first I was worried by 0 / 0 on RGB and A, but since modulate works by multiplication, a value of 0 will stay at 0 eventually so the result was correct. I don't know if this will work with all shaders though.
I found a "workaround", by making the parent a canvas group, you can change the modulation of the object by adjusting the modulation of that canvas group.
The shader didn't seem to like this but it did point me in the right direction, this works for rgba:
void fragment() {
vec4 tex = texture(TEXTURE,UV);
vec4 modulate = COLOR - tex;
//color changing code on tex goes here
COLOR = tex + modulate;
}
If this is more efficient than adding MODULATE back in I don't see the problem as long as it's clear methods like this exist. Would be nice if a warning popped up if you have both a shader and a changed modulation for example.
OK, so I found an issue with my division by tex, as I feared when some value reaches 0. I only tested simple mixes of colors like (1, 1, 0) and not darker shaders with a 0, so I didn't spot it at first.
For instance, this character has dark purple pixels (Green = 0) which still show striking bright purple as brightness = 1, instead of white:
I managed to avoid division 0 by clamping the tex. But to fix the issue completely at brightness 1 I had to clamp the COLOR too:
vec4 modulate = max(COLOR, 1e-3) / max(tex, 1e-3);
but as soon as I reduce the Green below 1 in the modulate, the reverse issue appears:
Unfortunately, there is not much to do about it, at least when working at fragment level: since the dark purple has a Green = 0, both tex.g and COLOR.g are 0, and we literally lost information about modulate, making it impossible to retrieve modulate.g.
I tried various other formulas including the -/+ suggested by @oxixxixo (which gave different results that tend to show more variations of the original texture; but not like the original modulate), but because of this core issue, nothing could properly apply the missing information.
So I'll be going with a custom uniform vec4 "modulate" parameter for now. Unfortunately, the inspector is not aware that my vec4 is a color, so it displays it as 4 values instead of showing the color picker. It won't affect runtime code setting the parameter, but it would be nice to be able to tag said vec4 as a color for color picking when debugging the shader live in the editor (anybody knows a way to do this before I post a proposal?)
EDIT: I found it! Add type hint source_color
after the parameter name:
uniform vec4 modulate : source_color = vec4(1.0);
This will make the color picker appear, and also recognize the default value. Default value is ignored when it's a mere vec4, but I think it's a bug.
I don't know shaders that well. In Godot 3.x I used a shader to change a sprite to white.
void fragment() {
COLOR = texture(TEXTURE, UV);
COLOR.rgb = vec3(1,1,1);
}
Then used the inspector modulate property to change it's color. Is this no longer possible?
If we now have to use a shader parameter to do this, it effects all material instances, forcing to duplicate the material.
If we now have to use a shader parameter to do this, it effects all material instances, forcing to duplicate the material.
Indeed, I'm checking Local to Scene on my character scene so each inherited scene and instance has its own material properties.
I would also love if MODULATE was a built in variable passed to the shader.
Although the solution suggested by oxixxixo works, in the case you are adding a border or outline, the outline does not set it's alpha to the desired one, being fully opaque.
Having a MODULATE built in would let us set it's alpha as the outline color alpha, thus, fixing the issue.
Edit: And would also just let us recreate the Godot 3 behavior by just ending the shader with a multiplication by MODULATE after using the original TEXTURE instead of COLOR.
I found that you can get the vertex's modulate without the texture color by doing this:
varying flat vec4 modulate;
void vertex() {
modulate = COLOR;
}
Then you can access modulate
in the fragment function.
EDIT: flat
is not necessary but interpolation is usually not needed in 2D and I am guessing it might be faster.
As a beginner in shaders, I must admit that I don't fully grasp all the intricate implications of this concept. Nevertheless, I can say that the previous version, 3x, was considerably more user-friendly in this aspect. It allowed me to effortlessly apply shaders to any node and reliably adjust or manipulate them. I also found myself perplexed when modulation ceased to function properly after applying a shader in the current version. It seems that the process is no longer as straightforward as it used to be.
Is this issue in limbo? If something is going to change again, it would be nice to know the "final" syntax as soon as possible.
@djrain this issue most likely would need to be reworded by @KoBeWi - rather it being an actual bug, the behaviour changed in Godot 4 (due to the way how materials work now), meaning that this is not a bug but a feature. All it requires is an update in documentation (and possible UX improvements in future to avoid confusion for people coming from Godot 3.x)