gdshader in depth — vertex, fragment, light
Spatial shader structure, built-in functions, uniforms, varyings, optimization steps.
In the rendering chapter we saw a basic ShaderMaterial. Here, in more detail: the structure of a 3D shader, what the vertex/fragment/light functions are, and how to write your own lighting.
Full structure of a spatial shader
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
// Uniforms — material parameters available from the Inspector
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap;
uniform vec4 tint : source_color = vec4(1.0);
uniform float roughness : hint_range(0.0, 1.0) = 0.5;
// Varyings — data passed from vertex to fragment
varying vec3 world_pos;
varying float vertical_factor;
void vertex() {
// Here we modify vertex positions and pass data downstream
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
vertical_factor = clamp(VERTEX.y * 0.1, 0.0, 1.0);
// Simple wave: sine displacement based on world time
VERTEX.y += sin(TIME + world_pos.x) * 0.1;
}
void fragment() {
// Here we compute the color of each pixel
vec4 tex = texture(albedo_tex, UV);
ALBEDO = tex.rgb * tint.rgb;
ALPHA = tex.a * tint.a;
ROUGHNESS = roughness;
METALLIC = 0.0;
EMISSION = vec3(0.0, 0.5 * vertical_factor, 0.0); // green gradient by height
}
void light() {
// (optional) custom lighting, replaces the built-in one
// LIGHT — final color, added to ALBEDO
DIFFUSE_LIGHT += LIGHT_COLOR * dot(NORMAL, LIGHT) * ATTENUATION;
}
What’s inside:
shader_type spatial;— this is for a 3D mesh. For 2D usecanvas_item, for a skybox usesky, and so on.render_mode— flags:blend_*(transparency),cull_*(culling),depth_*(Z-buffer),diffuse_*(diffuse lighting model),specular_*(specular),unshaded(no lighting).uniform— a parameter visible in the material Inspector and accessible from code viaset_shader_parameter.varying— a variable passed from vertex to fragment (interpolated across the triangle).vertex()— modifies vertices before rasterization.fragment()— computes surface properties (ALBEDO, NORMAL, ROUGHNESS, METALLIC, EMISSION, ALPHA).light()— custom lighting function (if you want your own non-PBR look).
Built-in variables
The main in/out variables:
In vertex():
VERTEX— vertex position in object space (in/out).NORMAL— normal (in/out).TANGENT,BINORMAL— tangents.UV,UV2— texture coordinates.COLOR— vertex color.MODEL_MATRIX,VIEW_MATRIX,PROJECTION_MATRIX— standard matrices.TIME— game time in seconds.INSTANCE_ID,VERTEX_ID— indices (for MultiMesh).
In fragment():
ALBEDO— diffuse color (out, vec3).ALPHA— transparency (out, float).METALLIC,ROUGHNESS,SPECULAR— PBR parameters (out).EMISSION— self-illumination (out, vec3).NORMAL,NORMAL_MAP— normals (out).AO,AO_LIGHT_AFFECT— ambient occlusion (out).RIM,RIM_TINT— rim light (out).CLEARCOAT,CLEARCOAT_GLOSS.UV,FRAGCOORD,VIEW,VERTEX— input.DEPTH— out (you can write to the depth buffer).
In light():
LIGHT— direction toward the light source (in, vec3).LIGHT_COLOR— color with intensity (in, vec3).ATTENUATION— attenuation (in, float).DIFFUSE_LIGHT,SPECULAR_LIGHT— final results (out, vec3).NORMAL,VIEW— for calculations.
Hints for uniforms
// Source: color texture (applies inverse gamma — sRGB → linear automatically)
uniform sampler2D albedo : source_color, filter_linear_mipmap, repeat_enable;
// Source: normal map (unpacked correctly)
uniform sampler2D normal_map : hint_normal;
// Range slider in the Inspector
uniform float intensity : hint_range(0.0, 5.0, 0.01) = 1.0;
// Color picker
uniform vec4 emission_color : source_color = vec4(1.0, 0.5, 0.0, 1.0);
// Cubemap for skybox / reflection
uniform samplerCube environment : source_color, filter_linear_mipmap;
The main hints:
source_color— for color textures (sRGB conversion).hint_normal— for a normal map.hint_roughness_r/_g/_b/_gray— channel pack for PBR.hint_range(min, max, step)— slider.filter_*— filtering:nearest/linear/linear_mipmap/linear_mipmap_anisotropic.repeat_*— wrap:enable/disable.
Example: dissolve
The classic “object dissolves into a noise pattern” effect:
shader_type spatial;
uniform sampler2D albedo : source_color, filter_linear_mipmap;
uniform sampler2D noise : filter_linear, repeat_enable;
uniform float threshold : hint_range(0.0, 1.0) = 0.0;
uniform vec3 edge_color : source_color = vec3(1.0, 0.5, 0.0);
uniform float edge_width : hint_range(0.0, 0.1) = 0.03;
void fragment() {
vec4 tex = texture(albedo, UV);
float n = texture(noise, UV).r;
if (n < threshold) {
discard; // discard this pixel
}
ALBEDO = tex.rgb;
if (n < threshold + edge_width) {
EMISSION = edge_color * 3.0;
ALBEDO = edge_color;
}
}
Usage:
var mat = $Mesh.material_override as ShaderMaterial
var tween = create_tween()
tween.tween_property(mat, "shader_parameter/threshold", 1.0, 2.0)
tween.tween_callback(queue_free)
Sub-pass: Cull back and transparency
Transparent objects often require manual tuning:
shader_type spatial;
render_mode blend_mix, depth_draw_always, cull_disabled;
uniform vec4 color : source_color = vec4(1.0, 0.5, 0.5, 0.4);
void fragment() {
ALBEDO = color.rgb;
ALPHA = color.a;
}
blend_mix— standard alpha blending.depth_draw_always— always write to the Z-buffer (for correct intersections).cull_disabled— draw both sides (for glass, foliage).
Your own light() — toon shading
shader_type spatial;
render_mode unshaded; // fully disable built-in lighting
uniform sampler2D albedo : source_color, filter_linear_mipmap;
uniform float bands : hint_range(2, 8, 1) = 3;
varying vec3 world_normal;
void vertex() {
world_normal = (MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz;
}
void light() {
float diffuse = dot(normalize(world_normal), normalize(LIGHT));
float quantized = floor(diffuse * bands) / bands;
DIFFUSE_LIGHT += LIGHT_COLOR * max(quantized, 0.0);
}
void fragment() {
ALBEDO = texture(albedo, UV).rgb;
}
render_mode unshaded disables the built-in PBR lighting; we write our own in light().
if (n < threshold) { discard; } is a branch in the fragment shader. It’s cheap on its own, but
massive branching destroys performance. Usually smoothstep or a mask via step() is better —
without ifs.
Particle shader
Particles use shader_type particles; with their own semantics:
shader_type particles;
render_mode keep_data;
uniform vec3 attract_center;
uniform float attract_strength : hint_range(0.0, 10.0) = 1.0;
void process() {
vec3 to_center = attract_center - TRANSFORM[3].xyz;
float dist = length(to_center);
VELOCITY += normalize(to_center) * attract_strength * DELTA;
TRANSFORM[3].xyz += VELOCITY * DELTA;
}
Applied as process_material on GPUParticles3D (replaces ParticleProcessMaterial).
What not to do in shaders
- Heavy computations in fragment(). Every pixel invokes the fragment shader. One extra texture sample at 1080p is 2 million samples per frame.
- Large loops. The GPU handles divergent branching poorly. A loop
for (int i = 0; i < 100; i++)is acceptable, butfor (int i = 0; i < N; i++) if (cond[i]) ...is not. - Pixel-perfect logic in the fragment shader. If it can be done in vertex (N times fewer calls), do it there.
dynamictexture sampling via a variable index — the GPU dislikes it. One atlas is better than an array of textures.
Further reading
- godotshaders.com — a community site with ready-made shaders.
- Documentation — the
Shading referencein the Godot Docs (docs.godotengine.org/en/stable/tutorials/shaders/). - Sources of the built-in shaders — in the Godot repository at
servers/rendering/renderer_rd/shaders/.
The next chapter is a glossary.