If you've been clicking nodes in Godot 4's VisualShader editor and wondering why the output never quite matches what you imagined, the answer is almost always the same: you need to write the shader yourself. Godot's `ShaderMaterial` with hand-written GLSL gives you complete control over every vertex, every fragment, and every uniform — and it's far less intimidating than it looks.
Why Write GLSL Shaders Directly in Godot 4?
VisualShader is a great prototyping tool, but it generates verbose GLSL under the hood, hides the underlying math, and makes it hard to share or version-control your work. Raw shader code is just a `.gdshader` text file — it diffs cleanly in Git, runs without conversion overhead, and exposes the exact properties Godot's rendering pipeline expects.
Godot 4 uses a custom GLSL dialect built on top of OpenGL ES 3.0/Vulkan. You declare a `shader_type`, then write `vertex()` and `fragment()` functions using Godot's built-in varying variables. The renderer handles the matrix math boilerplate; you focus on the creative logic. The performance case is equally strong: a hand-written shader uses exactly the instructions it needs, whereas VisualShader graphs often produce redundant nodes, extra texture samples, and intermediate variables a skilled artist would never write by hand.
Setting Up Your First ShaderMaterial
Start with a `MeshInstance3D` in your scene. In the Inspector, expand Surface Material Override, click the slot, and choose New ShaderMaterial. Click the new material, then click Shader → New Shader. Godot opens an inline editor at the bottom of the viewport.
The minimal shader is four lines: `shader_type spatial;` at the top, then `void fragment() { ALBEDO = vec3(1.0, 0.0, 0.5); ROUGHNESS = 0.4; }`. Save — the mesh turns pink immediately. Godot injects built-in output variables (`ALBEDO`, `ROUGHNESS`, `METALLIC`, `EMISSION`, `NORMAL`, `ALPHA`) automatically; you don't declare them. `shader_type spatial` targets the 3D PBR renderer (alternatives: `canvas_item` for 2D, `particles`). Every save recompiles and hot-reloads.
![]()
Vertex Shaders — Animated Displacement and Wind Effects
The `vertex()` function runs once per mesh vertex on the GPU and lets you offset position, animate UV coordinates, or compute custom normals before rasterization. A classic use case is foliage wind sway: a sine-wave displacement driven by `TIME`. Multiplying displacement by `UV.y` anchors the base of the plant while the tips sway — simple but convincing for grass, crops, or tree leaves. Export `wind_strength` and `wind_speed` as `uniform` values so level designers can tune them without touching shader code.
```glsl
shader_type spatial;
uniform float wind_strength : hint_range(0.0, 1.0) = 0.1;
uniform float wind_speed : hint_range(0.1, 5.0) = 1.5;
void vertex() {
float wave = sin(TIME * wind_speed + VERTEX.x * 2.0) * wind_strength;
VERTEX.x += wave * UV.y; // UV.y = 0 at base, 1 at tip — anchors the roots
VERTEX.z += wave * 0.5 * UV.y;
}
void fragment() {
ALBEDO = vec3(0.15, 0.55, 0.1);
ROUGHNESS = 0.85;
}
```
Key built-in vertex variables: `VERTEX` (local position, vec3, writable), `NORMAL` (vec3, writable), `UV` / `UV2` (vec2, read-only), `COLOR` (vertex color, vec4), `TIME` (elapsed seconds), `MODEL_MATRIX` (object-to-world, mat4).
Fragment Shaders — PBR Properties, Rim Light, and Emission
The `fragment()` function determines the final appearance of each pixel. Godot's spatial renderer is physically-based, so the outputs map directly to PBR: `ALBEDO` is base color, `ROUGHNESS` and `METALLIC` control specular response, and `EMISSION` adds self-illuminating color that ignores scene lighting.
Here's a sci-fi panel material combining sampled PBR textures with procedural rim light. `VIEW` points from the surface toward the camera; when `dot(NORMAL, VIEW)` is near zero the surface is edge-on — that's where rim light appears. Raising it to a power tightens the halo to a crisp edge rather than a diffuse glow.
```glsl
shader_type spatial;
uniform sampler2D albedo_tex : source_color, hint_default_white;
uniform sampler2D orm_tex : hint_default_white; // R=AO, G=Roughness, B=Metallic
uniform vec4 emission_color : source_color = vec4(0.0, 0.8, 1.0, 1.0);
uniform float emission_strength : hint_range(0.0, 8.0) = 2.0;
uniform float rim_strength : hint_range(0.0, 1.0) = 0.4;
void fragment() {
ALBEDO = texture(albedo_tex, UV).rgb;
vec3 orm = texture(orm_tex, UV).rgb;
AO = orm.r; ROUGHNESS = orm.g; METALLIC = orm.b;
float rim = 1.0 - clamp(dot(NORMAL, VIEW), 0.0, 1.0);
EMISSION = emission_color.rgb * pow(rim, 3.0) * rim_strength * emission_strength;
}
```
![]()
For assets from BitSoul's marketplace, ORM-packed textures (Occlusion in R, Roughness in G, Metallic in B) are the standard format. One texture sample covers three PBR channels, saving two texture fetches per pixel.
Shader Uniforms and GDScript Integration
Uniforms declared in a shader automatically appear as editable properties on the `ShaderMaterial` in the Inspector. Drive them from GDScript at runtime for state-driven effects like damage flashes or energy charge-ups. Use `set_shader_parameter(name, value)` — the Godot 4 API; `set_shader_param()` was removed in Godot 4 and will silently fail. Cache the material reference in `_ready()` rather than calling `get_surface_override_material()` every frame.
```gdscript
var _mat: ShaderMaterial
var _charge: float = 0.0
@export var charge_speed: float = 2.0
func _ready() -> void:
_mat = get_surface_override_material(0) as ShaderMaterial
func _process(delta: float) -> void:
_charge = clamp(_charge + delta * charge_speed, 0.0, 1.0)
_mat.set_shader_parameter("emission_strength", _charge * 8.0)
```
The name string must match the `uniform` identifier exactly, including case. You can pass `float`, `int`, `bool`, `Vector2`, `Vector3`, `Color`, `Transform3D`, and `Texture2D` objects directly.
Performance Tips and When to Use VisualShader Instead
Hand-written shaders are not always the right tool. Use VisualShader when prototyping a blend effect, when a technical artist without GLSL experience owns the asset, or when you need to visually debug intermediate values using the Preview node.
Stick to GLSL when you need version control and code review on shader logic, when the shader will be generated or templated programmatically, or when performance profiling shows the VisualShader output is doing redundant work.
Godot 4's shader compiler runs on first use, which can cause a stutter. Warm it at load time by calling `RenderingServer.shader_compile_spirv_from_glsl()` off the main thread, or enable Godot's built-in PSO caching in Project Settings → Rendering → Shaders → Shader Compilation Mode.
Texture lookups are the main GPU cost in fragment shaders. For game assets on BitSoul's marketplace, check whether the asset already includes channel-packed ORM textures before writing custom sampling logic — one sample is always faster than three.
---
*This post is part of the Ultimate Guide to Free 3D Game Assets — BitSoul's complete reference for formats, texturing, rigging, optimization, and engine integration.*