Godot Version
4.6
Question
Hi i do not know if this is an incredibly stupid question but i would like to make a font looking like it is carved (the opposite of embossed). I do not mean that the font should have the same color as the background, the font can have whatever is the color of the theme or of the object. But it should have some kind of shadow at the top left and highlights at the bottom right that make look as it is carved (light coming from the top left corner).
I guess this should be doable if the font supports sdf (signace distance field) but i literally have no idea on how to practically do that:
- Even if somehow i can create a shader that does what i have in mind, the shader is applied to a node (a button, a tab, a label) not only to the text attached to it
- Also i really would like something that is “set once and forget” and i should not have to set for every different object/type of object
I am justing asking for some “directions” because i do not know where to start…
thanks for any help
Hi,
2D Text has an option to set a shader material in the CanvasItem Section.
You might be able to achieve this effect with the Outline and Shadow properties, as you can change them both to whatever you want, including color and alpha.
You can do it with a shader, even without sdf.
Sample the texture 3 times, once without offset, two times with + and - offset. Use the first sample as a mask to draw differences between the non-offset and offset samples in dark and light color. The amount of “depth” you can have while still looking acceptable will depend on the font but should be fine to create an illusion of a typical engraved look with most standard fonts.
Here’s a quick demo. Note that UV offset may be screen space dependent due to the way Godot dynamically handles font atlases, but since it’s reasonable to assume there will be no zooming of gui elements at runtime - it should be fine once the depth is adjusted for a specific view.
shader_type canvas_item;
uniform float depth = 0.005;
uniform vec4 dark_color: source_color = vec4(0.0, 0.0, 0.0, 1.0);
uniform vec4 light_color: source_color = vec4(1.0, 1.0, 1.0, 1.0);
uniform float darken: hint_range(0.0, 0.1) = 0.05;
void fragment() {
if (REGION_RECT.z < 1.0) { // cheap trick to make sure we're rendering the font
float a0 = texture(TEXTURE, UV).a;
float a1 = texture(TEXTURE, UV - vec2(depth)).a;
float a2 = texture(TEXTURE, UV + vec2(depth)).a;
COLOR.a = (a0 - a1) * dark_color.a + (a0 - a2) * light_color.a;
COLOR.rgb = mix(dark_color.rgb, light_color.rgb, (a0 - a2));
COLOR.a += darken * a0; // darken the interior sligltly for better feel
}
}
Thanks for your help @normalized. I will check your answer as a solution since you spent a lot of effort in trying to help me. Unfortunately i have not been able to get results not even close to your images (with your method or others) so i guess the problem is on my end.
Not sure why you couldn’t get the results. The whole things is quite straightforward. Can you describe what exactly have you tried?
This is basically what i get:
The first image is without shader, the other 2 are at the same exact zoom with depth 0.001 and 0.005 i tried with a couple of fonts and i get the same result…
What happens if you use the default font?
Pretty much the same, i am simply unable to replicate your results. I literally copy pasted the shader from your answer and failed miserably to get the same result.
What happens when you zoom in?
This is what i get zooming in or out…
Can you post a minimal reproduction project?
This is the minimum project file that i was able to do. On my pc it behaves like the image i showed you
Minimum file example
Try disabling 2d hdr in project settings, or clamp the final alpha to 0-1 range.
Thanks clamping the alpha works. You were really helpful. I spent some time trying to make it “zoom independent” I am not sure it is 100% zoom independent but it changes “less” so i am posting that change as a “thank you” @normalized
shader_type canvas_item;
uniform float depth = 0.5;
uniform vec4 dark_color: source_color = vec4(0.0, 0.0, 0.0, 1.0);
uniform vec4 light_color: source_color = vec4(1.0, 1.0, 1.0, 1.0);
uniform float darken: hint_range(0.0, 0.1) = 0.05;
// Create a ‘varying’ variable to bridge the data between vertex and fragment
varying float true_scale;
void vertex() {
// 1. Calculate the true scale inside the vertex function where the matrices actually exist
true_scale = length(CANVAS_MATRIX[0].xy) * length(MODEL_MATRIX[0].xy);
}
void fragment() {
if (REGION_RECT.z < 1.0) {
// 2. Multiply by the varying 'true_scale'.
vec2 offset = (depth * TEXTURE_PIXEL_SIZE) * true_scale;
float a0 = texture(TEXTURE, UV).a;
float a1 = texture(TEXTURE, UV - offset).a;
float a2 = texture(TEXTURE, UV + offset).a;
float raw_alpha = (a0 - a1) * dark_color.a + (a0 - a2) * light_color.a + (darken * a0);
COLOR.a = clamp(raw_alpha, 0.0, 1.0);
vec3 raw_rgb = mix(dark_color.rgb, light_color.rgb, (a0 - a2));
COLOR.rgb = max(vec3(0.0), raw_rgb);
}
}