← Back to Blog 3d-modeling

Godot 4 Shaders: Write Custom PBR Materials from Scratch

By BitSoul Team4/26/2026Updated 8/2/20266 min read351 views
Godot 4 Shaders: Write Custom PBR Materials from Scratch

Godot's built-in StandardMaterial3D covers 90% of cases—but the moment you need a dissolve edge, a tri-planar blend, or a custom rim-light pass, you hit a wall. Writing spatial shaders by hand gives you full control, and Godot 4's shader language is close enough to GLSL 3.0 that the learning curve is short if you know your PBR fundamentals.

This guide takes you from an empty shader file to a fully functioning PBR material with emission and rim lighting. All code is Godot 4.x compatible.

Understanding Godot 4's Spatial Shader Model

Godot 4 uses a fragment-shader pipeline for 3D (spatial) materials. Unlike Unity's ShaderGraph or Unreal's Material editor, Godot exposes a set of built-in output variables you write to directly. There is no node graph sitting between your code and the renderer—what you set is what the engine uses.

Every spatial shader has three optional functions:

For 99% of custom PBR materials you only need `fragment()`. The engine handles light accumulation, shadow sampling, ambient occlusion, and environment reflections automatically once you write to the standard output variables.

The shader type declaration is required at the top of every file:

```glsl
shader_type spatial;
```

Without it Godot will not know which pipeline to compile for and the script will error on import.

Setting Up Your First PBR Shader

Setting Up Your First PBR Shader — illustrated

Create a new `ShaderMaterial` in Godot 4, assign it to a MeshInstance3D, and click the shader field to open a new `.gdshader` file. Start with this skeleton:

```glsl
shader_type spatial;

uniform sampler2D albedo_texture : source_color, hint_default_white;
uniform sampler2D normal_map : hint_roughness_normal;
uniform sampler2D orm_map : hint_default_white; // R=AO, G=Roughness, B=Metallic

uniform vec4 albedo_tint : source_color = vec4(1.0);
uniform float metallic : hint_range(0.0, 1.0) = 0.0;
uniform float roughness : hint_range(0.0, 1.0) = 0.5;

void fragment() {
vec4 base = texture(albedo_texture, UV) * albedo_tint;
ALBEDO = base.rgb;
ALPHA = base.a;

vec3 orm = texture(orm_map, UV).rgb;
AO = orm.r;
ROUGHNESS = orm.g * roughness;
METALLIC = orm.b * metallic;

NORMAL_MAP = texture(normal_map, UV).rgb;
}
```

The `source_color` hint tells Godot to apply gamma correction when sampling—always use it for albedo and any texture that stores colour data. The `hint_roughness_normal` hint marks the normal map for tangent-space unpacking. The ORM layout (Ambient Occlusion in R, Roughness in G, Metallic in B) matches what Substance Painter, Blender's Principled BSDF baker, and most marketplace assets ship by default, so one sampler covers all three channels.

Once this compiles, drag your textures into the shader parameters in the Inspector. You now have a fully functional PBR material with correct gamma handling and normal mapping—no node graph required.

Albedo, Metallic, Roughness, and Normal Maps in Code

Understanding exactly which built-in variable does what prevents the most common shader bugs:

| Variable | Type | Range | Notes |
|---|---|---|---|
| `ALBEDO` | `vec3` | 0–1 | Base colour, no lighting applied |
| `METALLIC` | `float` | 0–1 | 0 = dielectric, 1 = metal |
| `ROUGHNESS` | `float` | 0–1 | 0 = mirror, 1 = fully diffuse |
| `NORMAL_MAP` | `vec3` | 0–1 (raw) | Godot unpacks to tangent space automatically |
| `AO` | `float` | 0–1 | Multiplied into indirect lighting |
| `SPECULAR` | `float` | 0–1 | F0 reflectance for dielectrics (default 0.5) |

One common mistake: writing world-space normals directly to `NORMAL` instead of using `NORMAL_MAP`. If you write a raw normal map texture to `NORMAL`, you will see broken shading that ignores mesh tangents. Always write to `NORMAL_MAP` and let Godot handle the tangent-space transform, unless you are computing normals entirely in the vertex shader yourself.

For assets sourced from the BitSoul marketplace, ORM packing is consistent across all packs, so the three-channel approach above will work without modification on any asset tagged `pbr-ready`.

Advanced Effects — Emission, Rim Lighting, and Custom Blending

Advanced Effects — Emission, Rim Lighting, and Custom Blending — illustrated

Once your base PBR pass works, adding emission and rim lighting is a matter of writing to two more output variables.

Emission adds unlit, bloom-able light directly to the surface:

```glsl
uniform sampler2D emission_map : source_color, hint_default_black;
uniform float emission_power : hint_range(0.0, 8.0) = 1.0;

// inside fragment():
vec3 emis = texture(emission_map, UV).rgb;
EMISSION = emis * emission_power;
```

Set `emission_power` above 1.0 and enable Glow in the Environment settings for HDR bloom on emissive areas.

Rim lighting fakes the light-scattering edge glow seen on hair, fabric, and translucent objects. Godot exposes `RIM` and `RIM_TINT` for this:

```glsl
uniform float rim_amount : hint_range(0.0, 1.0) = 0.3;
uniform float rim_tint : hint_range(0.0, 1.0) = 0.5;

// inside fragment():
RIM = rim_amount;
RIM_TINT = rim_tint;
```

`RIM` controls intensity; `RIM_TINT` blends between the light colour (0.0) and the albedo colour (1.0). For character hair, a value around 0.7 rim tint gives a naturalistic subsurface scatter approximation without the cost of real SSS.

Custom alpha blending for dissolve effects uses the `ALPHA` output with a noise texture:

```glsl
render_mode blend_mix, depth_draw_alpha_prepass;

uniform sampler2D dissolve_noise : hint_default_white;
uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;

// inside fragment():
float noise = texture(dissolve_noise, UV).r;
ALPHA = step(dissolve_amount, noise);
```

Animate `dissolve_amount` from 0 to 1 via GDScript or an AnimationPlayer to get a hard-edge dissolve. Replace `step()` with `smoothstep(dissolve_amount, dissolve_amount + 0.05, noise)` for a soft, glowing edge.

Optimizing Shaders for Performance

Custom shaders can hurt GPU frame time if written carelessly. Apply these rules from the start:

Pack texture channels. The ORM map samples three values in one `texture()` call. Avoid separate AO, roughness, and metallic textures; pack them before import.

Use `render_mode` flags to skip unneeded passes. If your material is fully opaque with no backface visibility, add `render_mode cull_back;` (the default) and `render_mode depth_draw_always;`. Transparent materials need `render_mode blend_mix, depth_draw_alpha_prepass;`.

Avoid branching on uniforms. An `if (use_emission)` check that branches based on a uniform still compiles both paths on most GPU architectures. Use multiplication by a 0/1 float instead:

```glsl
// Instead of: if (use_emission) { EMISSION = emis; }
EMISSION = emis * float(emission_power > 0.001);
```

Limit unique shader variants. Every `#if` or `#ifdef` in a shader creates a new compiled variant. Keep conditional logic to a minimum and favour data-driven parameters (uniforms) over compile-time branches.

For game-ready assets that already ship with PBR-correct textures and clean UV sets, browse the BitSoul 3D marketplace—every asset is tested for PBR compatibility so you can drop the ORM shader above straight onto it without rework.

Start Writing Shaders Today

Godot 4's spatial shader system is powerful precisely because it stays close to the metal. Once you understand the fragment output variables and the PBR inputs, you can build virtually any material effect—dissolves, tri-planar projection, animated scrolling textures, custom rim passes—without ever leaving the `.gdshader` file.

Grab a PBR-ready asset from bitsoulhosting.com/marketplace, apply the ORM shader above, and start experimenting. The fastest way to master spatial shaders is to break things on real content.

---

*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.*

Tags: godot shaders pbr game-development 3d materials glsl

Skip the modelling — download it instead

A free BitSoul account gets you 2 game-ready models every month plus 25 AI Engine credits to generate one of your own, no card required. Clean topology, PBR textures, and GLB downloads that drop straight into Unreal, Unity, Godot or Blender — plus OBJ and 3D-printable STL export.

Create a free account → Browse 846 models