Creating Custom Shaders
This skill guides creation of GLSL shaders that integrate with the engine's shadow system.
Architecture Overview
The engine uses a shader injection system:
- •You write fragment shaders with custom effects
- •Engine automatically injects shadow uniforms and
getShadow()function - •Use standard vertex shader or write custom one
- •Register shader → Create material → Assign to entities
File Locations
Place shaders in your game's assets folder:
code
assets/
└── shaders/
├── my_effect.fs # Fragment shader (required)
└── my_effect.vs # Vertex shader (optional, use standard)
Fragment Shader Template
glsl
#version 330
// Varyings from vertex shader (standard vertex shader provides these)
in vec3 fragPosition; // World-space position
in vec2 fragTexCoord; // Texture coordinates
in vec4 fragColor; // Vertex color
in vec3 fragNormal; // World-space normal
// Material uniforms (auto-set by raylib)
uniform sampler2D texture0; // Diffuse texture
uniform vec4 colDiffuse; // Material color
// Engine injects these automatically:
// uniform vec3 lightDir; // Directional light direction
// uniform vec4 lightColor; // Light color
// uniform vec4 ambient; // Ambient color
// uniform vec3 viewPos; // Camera position
// float getShadow(vec3 fragPos, vec3 normal); // Returns 0.0 (shadow) to 1.0 (lit)
// Time uniform (auto-updated by engine each frame)
uniform float time;
// Output
out vec4 finalColor;
void main()
{
// Sample texture
vec4 texColor = texture(texture0, fragTexCoord);
vec3 baseColor = texColor.rgb * colDiffuse.rgb;
// Basic lighting
vec3 normal = normalize(fragNormal);
float NdotL = max(dot(normal, -lightDir), 0.0);
// Get shadow factor (1.0 = lit, 0.0 = shadowed)
float shadow = getShadow(fragPosition, normal);
// Apply lighting
vec3 litColor = baseColor * (NdotL * shadow * lightColor.rgb + ambient.rgb);
finalColor = vec4(litColor, texColor.a * colDiffuse.a);
// Gamma correction
finalColor.rgb = pow(finalColor.rgb, vec3(1.0 / 2.2));
}
Loading and Registering Shaders
In your main.go or init function:
go
eng.InitFunc = func() {
// Load with standard vertex shader (pass "" for vsPath)
myShader, err := eng.LoadShaderFromFiles("", "shaders/my_effect.fs")
if err != nil {
log.Fatal(err)
}
eng.ShaderRegistry().Add("my_effect", myShader)
// Create material with shader and color
err = eng.CreateMaterial("my_material", myShader, rl.Red)
if err != nil {
log.Fatal(err)
}
// Or create textured material
err = eng.CreateMaterialTextured("my_textured_mat", myShader, "textures/wood.png")
if err != nil {
log.Fatal(err)
}
}
Using Shaders on Entities
Via RenderMesh Component
go
// Use material ID (shader embedded in material)
scene.SetRenderMesh(entity, component.RenderMeshData{
MeshId: "cube",
MaterialId: "my_material",
})
// Or override shader on any material
scene.SetRenderMesh(entity, component.RenderMeshData{
MeshId: "cube",
MaterialId: "default",
ShaderId: "my_effect", // Override material's shader
})
Via Entity Builder
go
entityId := scene.Spawn("MyEntity").
At(math.NewVector3(0, 1, 0)).
Mesh("cube", "my_material"). // Uses material's embedded shader
Build()
Available Injected Uniforms
These are automatically available in your fragment shader after injection:
| Uniform | Type | Description |
|---|---|---|
lightDir | vec3 | Directional light direction (normalized) |
lightColor | vec4 | Light color (RGBA) |
ambient | vec4 | Ambient light color |
viewPos | vec3 | Camera position in world space |
lightVP | mat4 | Light's view-projection matrix (for shadows) |
shadowMap | sampler2D | Shadow depth texture |
shadowMapResolution | int | Shadow map size (e.g., 2048) |
Injected Functions
glsl
// Get shadow factor at world position // Returns: 1.0 = fully lit, 0.0 = fully in shadow // Uses PCF for soft shadow edges float getShadow(vec3 fragPos, vec3 normal);
Time Uniform
The engine automatically updates a time uniform each frame:
glsl
uniform float time; // Seconds since game start
void main() {
// Animate based on time
float wave = sin(time * 2.0) * 0.5 + 0.5;
// ...
}
Custom Vertex Shader
Only needed for special effects (vertex animation, displacement). Use standard for most cases.
glsl
#version 330
// Required inputs
in vec3 vertexPosition;
in vec2 vertexTexCoord;
in vec3 vertexNormal;
in vec4 vertexColor;
// Required uniforms
uniform mat4 mvp;
uniform mat4 matModel;
uniform mat4 matNormal;
// Required outputs (for fragment shader and shadow system)
out vec3 fragPosition;
out vec2 fragTexCoord;
out vec4 fragColor;
out vec3 fragNormal;
// Custom uniforms
uniform float time;
void main()
{
// Custom vertex manipulation
vec3 pos = vertexPosition;
pos.y += sin(pos.x * 3.0 + time) * 0.2; // Wave effect
// Transform to world space
fragPosition = vec3(matModel * vec4(pos, 1.0));
fragTexCoord = vertexTexCoord;
fragColor = vertexColor;
fragNormal = normalize(vec3(matNormal * vec4(vertexNormal, 1.0)));
// Final position
gl_Position = mvp * vec4(pos, 1.0);
}
Load with custom vertex shader:
go
shader, err := eng.LoadShaderFromFiles("shaders/wave.vs", "shaders/wave.fs")
Common Shader Recipes
Toon/Cel Shading
glsl
// Quantize lighting into discrete bands
float toonFactor;
if (NdotL * shadow > 0.8) {
toonFactor = 1.0;
} else if (NdotL * shadow > 0.4) {
toonFactor = 0.7;
} else if (NdotL * shadow > 0.2) {
toonFactor = 0.45;
} else {
toonFactor = 0.25;
}
vec3 litColor = baseColor * toonFactor + baseColor * ambient.rgb;
Rim/Fresnel Glow
glsl
vec3 viewDir = normalize(viewPos - fragPosition); float fresnel = 1.0 - max(dot(normal, viewDir), 0.0); fresnel = pow(fresnel, 2.0); vec3 rimColor = vec3(0.0, 0.8, 1.0) * fresnel * 2.0; finalColor.rgb += rimColor;
Hologram Scan Lines
glsl
float scanLineSpeed = 2.0; float scanLineFreq = 50.0; float scanLine = sin((fragPosition.y + time * scanLineSpeed) * scanLineFreq) * 0.5 + 0.5; scanLine = pow(scanLine, 1.5); finalColor.a *= scanLine;
Pulse/Glow Effect
glsl
float pulse = sin(time * 3.0) * 0.3 + 0.7; vec3 glowColor = vec3(1.0, 0.5, 0.0); // Orange finalColor.rgb += glowColor * pulse * fresnel;
Dissolve Effect
glsl
uniform float dissolveAmount; // 0.0 to 1.0
// Pseudo-random based on world position
float noise = fract(sin(dot(fragPosition.xz, vec2(12.9898, 78.233))) * 43758.5453);
if (noise < dissolveAmount) {
discard; // Remove fragment
}
// Glow at dissolve edge
if (noise < dissolveAmount + 0.05) {
finalColor.rgb += vec3(1.0, 0.5, 0.0) * 2.0;
}
Setting Custom Uniforms
For dynamic uniforms beyond time:
go
// In a system's Update or init
shader, _ := eng.ShaderRegistry().Get("my_shader")
loc := rl.GetShaderLocation(shader, "customValue")
rl.SetShaderValue(shader, loc, []float32{1.5}, rl.ShaderUniformFloat)
// For vec3:
rl.SetShaderValue(shader, loc, []float32{1.0, 2.0, 3.0}, rl.ShaderUniformVec3)
Transparent Materials
For transparent shaders, mark the RenderMesh:
go
scene.SetRenderMesh(entity, component.RenderMeshData{
MeshId: "cube",
MaterialId: "hologram_material",
IsTransparent: true, // Renders in transparent pass
})
Transparent entities:
- •Sorted back-to-front by view depth
- •Don't cast shadows
- •Rendered after opaque geometry
Debugging Shaders
- •Compile Errors: Check console output when loading shader
- •Visual Debug: Output intermediate values as colors:
glsl
finalColor = vec4(normal * 0.5 + 0.5, 1.0); // Visualize normals finalColor = vec4(vec3(shadow), 1.0); // Visualize shadows
- •Debug Color Shader: Use
shaders/debug_color.fsas reference
Example Shaders in Project
See existing implementations:
- •Toon shader:
game/assets/shaders/example_toon.fs - •Hologram:
game/assets/shaders/hologram.vs,hologram.fs - •Debug:
game/assets/shaders/debug_color.fs - •Standard vertex:
engine/core/shader.go(embedded)
Important Notes
- •Shadow Injection: Engine auto-injects shadow code after
#versionline - •Standard Vertex Shader: Pass
""for vsPath to use built-in - •Gamma Correction: Apply
pow(color, 1/2.2)at end for correct display - •Required Varyings: Fragment shader MUST declare expected varyings from vertex shader
- •Time Uniform: Declare
uniform float time;to use it (engine updates automatically)