godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.17k stars 98 forks source link

Better _draw() functionality for gradient text #10999

Open choboy46 opened 1 month ago

choboy46 commented 1 month ago

Describe the project you are working on

A 2D sprite based game in Godot 4.3, with a dialogue system that's supposed to be able to display colored gradient text (makes use of the _draw() function).

Describe the problem or limitation you are having in your project

One of the previous engines I've used allowed for freely drawing with built in functions, so it delighted me to learn that Godot had its own _draw() function. However, I soon came to realize that it lacked an inherent ability to draw text with a gradient effect. Sure, I was able to figure out how to create such an effect using shaders, but that only solved part of the problem. I wanted to be able to draw multiple texts of varying colors which all had the gradient effect applied, but the way I was doing it could only allow for one gradient color per node. Moreover, only one shader effect can be applied per node at a time, and using multiple nodes to achieve my desired effect seems to be impractical.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

A possible solution without needing to change how shaders work is to add a draw function that enables users to draw a string/char with a gradient color. Users should be able to choose the direction which the gradient is drawn in. This way, you can draw multiple different colored gradient texts from a single _draw() function. Alternatively, provide a way to change shader properties in the middle of the _draw() function so that different shader properties are able to be applied to different draws.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

new draw function example: draw_string_gradient(font, pos, "Gradient Text", gradient1, font_size) draw_string_gradient(font, pos, "Gradient Text2", gradient2, font_size) With a function such as this, users have a simpler way to draw text with different gradient effects applied without other draws in the same function being affected, removing the necessity to create excessive nodes.

If this enhancement will not be used often, can it be worked around with a few lines of script?

As I've mentioned before, you could hypothetically achieve this by creating multiple nodes and give them each a shader with different uniform variables, but this would not be very practical in the context of a dialogue system.

Is there a reason why this should be core and not an add-on in the asset library?

This could improve accessibility for people who are new to Godot, especially for those who are used to working with other engines.

Calinou commented 1 month ago

Are you referring to a gradient changing the letters' colors vertically (i.e. all letters have the same gradient applied) or horizontally (i.e. each letter has a slightly different color compared to the previous one)? If this is about drawing a gradient on each letter, there's already a proposal tracking this: https://github.com/godotengine/godot-proposals/issues/2564

If you want to color each letter individually, this can already be achieved using RichTextLabel's [color] tag (it could be scripted so you don't have to enter them manually) or by writing a custom RichTextEffect. Either way, I don't think this should be implemented in _draw() as it's generally only used in low-level functionality (such as performance-critical scenarios). Higher-level GUI functionality is generally implemented in nodes instead.

choboy46 commented 1 month ago

Vertically right now, but horizontally is another thing I look forward to. The problem that I am facing here is how font modulation works with the shaders. Normally, I would be able to change the text color mid string using a single node. However, shaders do not account for modulation, which is a problem because in order to achieve what I wanted to achieve I would need several separate duplicates of the node drawing the text in order to display different colors with the same gradient effect applied.

bruvzg commented 1 month ago

RichTextEffect only allow setting a single color for each glyph, so it won't work with gradient.

You can do text with gradient in various ways, but it will require at least one node per string/effect:

Both work, but can't be directly used with complex controls like RTL.

draw_string_gradient seems a bit too specific, but draw_string_with_texture or/and an option to apply texture in the RichTextEffect might be useful.

choboy46 commented 1 month ago

Small update here, I figured out that you can access the modulation color from the vertex function by saving it in a varying vec4 variable. However, I still face issues with maintaining the same gradient across all characters due to the inconsistent UVs.

bruvzg commented 1 month ago

However, I still face issues with maintaining the same gradient across all characters due to the inconsistent UVs.

In case of text, UV is for the glyph texture atlas, you can use SCREEN_UV or VERTEX position instead:

shader_type canvas_item;

uniform sampler2D gradient_texture;
uniform vec2 text_size;

varying vec2 vertex_coord;

void vertex() {
    vertex_coord = VERTEX / text_size;
}

void fragment() {
    COLOR = textureLod(gradient_texture, vertex_coord, 0.0) * COLOR;
}
Screenshot 2024-10-21 at 07 49 12
choboy46 commented 1 month ago

Was actually making use of the mix() function but thanks for the advice! Anyway, I think its worth mentioning here that this code also creates a sort of a text shadow as well.

shader_type canvas_item;

uniform bool enable = false;
uniform float fnt_ht;
uniform float vspace = 32;
uniform float mult;

const vec4 white = vec4(1.0, 1.0, 1.0, 1.0);
const vec4 dkgray = vec4(64.0 / 255.0, 64.0 / 255.0, 64.0 / 255.0 , 1.0);
const vec4 navy = vec4(0.0, 0.0, 128.0 / 255.0, 1.0);
const vec4 black = vec4(0.0, 0.0, 0.0, 1.0);

varying float vertex_mix;
varying vec4 vertex_color;
varying vec2 vertex_pass;

void vertex() {
    if (enable) {
        float vm = VERTEX.y / (fnt_ht * mult);
        float m = vspace / fnt_ht;
        vertex_mix = mod(vm, m);
        vertex_color = COLOR;
        vertex_pass = VERTEX;
    }
}

void fragment() {
    if (enable) {
        vec4 c = texture(TEXTURE, UV);
        if (c.a != 0.0) {
            vec4 gradient_color = mix(white, vertex_color, vertex_mix);
            COLOR = gradient_color;
        }
        else {
            vec2 tex_size = vec2(textureSize(TEXTURE, 0));
            if (UV.x > 0.0 && UV.y > 0.0 && texture(TEXTURE, UV - vec2(1.0 / tex_size.x, 1.0 / tex_size.y)).a != 0.0) {
                if (vertex_color == white) {
                    COLOR = mix(dkgray, navy, vertex_mix);
                }
                else {
                    COLOR = mix(vertex_color, black, 0.7);
                }
            }
            else {
                COLOR = vec4(0.0, 0.0, 0.0, 0.0);
            }
        }
    }
}