Surface shaders are custom shaders that can be applied to materials and decals in R3D. They provide a simplified way to write GLSL code by abstracting away the complexity of multiple render passes.
A surface shader is a simplified shader interface that allows you to modify vertex and fragment behavior without worrying about the underlying render pipeline. R3D automatically handles multiple render passes (opaque, transparent, shadows, etc.) from a single shader definition.
uniform float u_time;
void fragment() {
ALBEDO *= 0.5 + 0.5 * sin(u_time);
}
// From file
R3D_SurfaceShader* shader = R3D_LoadSurfaceShader("my_shader.glsl");
// From memory
const char* code = "void fragment() { ALBEDO = vec3(1.0, 0.0, 0.0); }";
R3D_SurfaceShader* shader = R3D_LoadSurfaceShaderFromMemory(code);
// Don't forget to unload when done
R3D_UnloadSurfaceShader(shader);
Surface shaders have two optional entry points: vertex() and fragment(). At least one must be defined.
Runs once per vertex, before rasterization. Use it to modify vertex positions, colors, or pass data to the fragment stage.
void vertex() {
POSITION.y += sin(POSITION.x * 10.0) * 0.1;
}
Runs once per pixel. Use it to modify final surface properties like albedo, roughness, or emission.
void fragment() {
ALBEDO = vec3(1.0, 0.0, 0.0); // Red surface
ROUGHNESS = 0.5;
}
You can define both stages to create complex effects:
varying float v_height;
void vertex() {
v_height = POSITION.y;
}
void fragment() {
ALBEDO = mix(vec3(0.0, 0.5, 0.0), vec3(1.0, 1.0, 1.0), v_height);
}
Built-in variables are pre-defined values you can read and modify in your shader. They can only be accessed within their corresponding entry point.
All vertex-stage variables are initialized with local (pre-transformation) attribute values:
| Variable | Type | Description |
|---|---|---|
POSITION |
vec3 |
Vertex position (local space) |
TEXCOORD |
vec2 |
Texture coordinates |
NORMAL |
vec3 |
Vertex normal (local space) |
TANGENT |
vec4 |
Vertex tangent (w = handedness) |
COLOR |
vec4 |
Vertex color |
EMISSION |
vec3 |
Vertex emission |
INSTANCE_POSITION |
vec3 |
Instance position (world space) |
INSTANCE_ROTATION |
vec4 |
Instance rotation (quaternion: x, y, z, w) |
INSTANCE_SCALE |
vec3 |
Instance scale |
INSTANCE_COLOR |
vec4 |
Instance color |
INSTANCE_CUSTOM |
vec4 |
Custom user-defined instance data |
Instance Variables:
The INSTANCE_* variables are always available, even for non-instanced rendering.
When instancing is not used or when an instance buffer doesn't define certain attributes, they default to:
INSTANCE_POSITION = vec3(0.0);
INSTANCE_ROTATION = vec4(0.0, 0.0, 0.0, 1.0); // Identity quaternion
INSTANCE_SCALE = vec3(1.0);
INSTANCE_COLOR = vec4(1.0);
INSTANCE_CUSTOM = vec4(0.0);
You can modify mesh-local attributes (POSITION, NORMAL, etc.) and instance attributes (INSTANCE_*) independently.
R3D automatically composes them internally, so you don't need to manually combine them.
Custom Instance Data:
INSTANCE_CUSTOM is reserved for user-defined data. Unlike other instance attributes, it has no predefined meaning and can store any data you need.
If you want to use it in the fragment stage, pass it through a varying:
varying vec4 v_custom_data;
void vertex() {
// Use custom data however you want
v_custom_data = INSTANCE_CUSTOM;
// Example: use as animation offset
POSITION.y += INSTANCE_CUSTOM.x * sin(INSTANCE_CUSTOM.y);
}
void fragment() {
// Access custom data passed from vertex stage
ALBEDO *= v_custom_data.rgb;
}
Example:
void vertex() {
// Modify local mesh attributes
POSITION *= 1.5; // Scale vertex position
COLOR.rgb *= 0.5; // Darken vertex color
// Modify instance attributes separately
INSTANCE_SCALE *= 2.0; // Double instance scale
INSTANCE_COLOR.a *= 0.8; // Make instance more transparent
// R3D will compose these automatically
}
Fragment-stage variables are pre-initialized with material values (unless R3D_NO_AUTO_FETCH is defined):
| Variable | Type | Description |
|---|---|---|
TEXCOORD |
vec2 |
Interpolated texture coordinates |
NORMAL |
vec3 |
Surface normal (world space) |
TANGENT |
vec3 |
Surface tangent |
BITANGENT |
vec3 |
Surface bitangent |
ALBEDO |
vec3 |
Base color |
ALPHA |
float |
Transparency |
EMISSION |
vec3 |
Emissive color |
NORMAL_MAP |
vec3 |
Normal map value |
OCCLUSION |
float |
Ambient occlusion |
ROUGHNESS |
float |
Surface roughness (0 = smooth, 1 = rough) |
METALNESS |
float |
Metallic property (0 = dielectric, 1 = metal) |
Example:
void fragment() {
ALBEDO = vec3(1.0, 0.0, 0.0); // Red surface
ROUGHNESS = 0.2; // Shiny
METALNESS = 1.0; // Metallic
}
ALBEDO in vertex() or POSITION in fragment().To use built-in variables in helper functions, pass them as parameters:
vec3 darken(vec3 color, float amount) {
return color * amount;
}
void fragment() {
ALBEDO = darken(ALBEDO, 0.5);
}
Varyings allow you to pass data from the vertex stage to the fragment stage. They are automatically interpolated across the triangle.
varying float v_height;
void vertex() {
v_height = POSITION.y;
}
void fragment() {
ALBEDO = mix(vec3(0.2, 0.8, 0.2), vec3(1.0), v_height);
}
You can control how varyings are interpolated using qualifiers:
| Qualifier | Description |
|---|---|
flat |
No interpolation (use value from provoking vertex) |
smooth |
Perspective-correct interpolation (default) |
noperspective |
Linear interpolation in screen space |
Example:
flat varying int v_material_id;
noperspective varying vec2 v_screen_uv;
void vertex() {
v_material_id = 1;
v_screen_uv = TEXCOORD;
}
void fragment() {
if (v_material_id == 1) {
ALBEDO = texture(u_texture, v_screen_uv).rgb;
}
}
Uniforms are constant values that can be set from your C code. They remain constant across all vertices/fragments in a draw call.
Values:
bool, int, floatvec2, vec3, vec4mat2, mat3, mat4Samplers:
sampler1D, sampler2D, sampler3D, samplerCubeR3D_MAX_SHADER_UNIFORMS)R3D_MAX_SHADER_SAMPLERS)uniform float u_time;
uniform vec3 u_color;
uniform mat4 u_transform;
uniform sampler2D u_texture;
Values:
float time = GetTime();
R3D_SetSurfaceShaderUniform(shader, "u_time", &time);
Vector3 color = {1.0f, 0.0f, 0.0f};
R3D_SetSurfaceShaderUniform(shader, "u_color", &color);
// For booleans, use int (4 bytes)
int flag = 1; // true
R3D_SetSurfaceShaderUniform(shader, "u_flag", &flag);
Samplers:
Texture2D texture = LoadTexture("texture.png");
R3D_SetSurfaceShaderSampler(shader, "u_texture", texture);
bool uniforms from C, pass an int (4 bytes). Non-zero values are true, zero is false.R3D_End(), only the last uniform value set before R3D_End() will be used.uniform float u_time;
uniform sampler2D u_noise;
uniform bool u_enable_effect;
void fragment() {
if (u_enable_effect) {
vec2 uv = TEXCOORD + texture(u_noise, TEXCOORD * 2.0).xy * 0.1;
ALBEDO *= 0.5 + 0.5 * sin(u_time + uv.x * 10.0);
}
}
float time = 0.0f;
Texture2D noise = LoadTexture("noise.png");
int enableEffect = 1;
R3D_SetSurfaceShaderSampler(shader, "u_noise", noise);
R3D_SetSurfaceShaderUniform(shader, "u_enable_effect", &enableEffect);
while (!WindowShouldClose()) {
time += GetFrameTime();
R3D_SetSurfaceShaderUniform(shader, "u_time", &time);
R3D_Begin();
// ... draw with shader ...
R3D_End();
}
By default, material textures (albedo, normal, ORM) are automatically sampled and available as built-in variables in the fragment stage.
If you want to start with zero values and sample materials manually, define:
#define R3D_NO_AUTO_FETCH
This sets ALBEDO, NORMAL_MAP, and OCCLUSION/ROUGHNESS/METALNESS to zero.
vec4 SampleAlbedo(vec2 texCoord);
vec3 SampleEmission(vec2 texCoord);
vec3 SampleNormal(vec2 texCoord);
vec3 SampleOrm(vec2 texCoord); // (Occlusion, Roughness, Metalness)
To automatically fill all built-in variables with material values:
void FetchMaterial(vec2 texCoord);
#define R3D_NO_AUTO_FETCH
void fragment() {
// Sample material at distorted UV
vec2 distorted_uv = TEXCOORD + vec2(sin(TEXCOORD.y * 10.0) * 0.1, 0.0);
FetchMaterial(distorted_uv);
// Or sample individual textures
// ALBEDO = SampleAlbedo(distorted_uv).rgb;
// vec3 orm = SampleOrm(distorted_uv);
// ROUGHNESS = orm.g;
}
R3D compiles multiple shader variants for different render passes (opaque, transparent, shadows, etc.). By default, only the opaque variant is pre-compiled; others compile on-demand when needed.
On-demand compilation can cause stuttering when a new variant is first used. For example, if your shader is used on a transparent object, the transparent variant compiles when the object first becomes visible.
Use #pragma usage to specify which variants should be pre-compiled:
#pragma usage transparent shadow
| Hint | Description |
|---|---|
opaque |
Opaque rendering for lit objects (default if no pragma specified) |
prepass |
Transparent pre-pass rendering for lit objects |
transparent |
Transparent rendering (color/alpha blending) for lit objects |
unlit |
Unlit rendering (handles both opaque and transparent unlit objects) |
shadow |
Shadow map rendering |
decal |
Decal rendering |
probe |
Reflection probe rendering |
Opaque object with shadows:
#pragma usage opaque shadow
void fragment() {
ALBEDO = vec3(1.0, 0.0, 0.0);
ALPHA = 0.5; // Alpha cutoff
}
Transparent object:
#pragma usage transparent
void fragment() {
ALBEDO = vec3(0.0, 0.5, 1.0);
ALPHA = 0.5; // Alpha blending
}
Unlit object:
#pragma usage unlit
void fragment() {
ALBEDO = vec3(1.0, 1.0, 0.0);
ALPHA = 0.5; // Alpha cutoff or blending
}
Decal shader:
#pragma usage decal
void fragment() {
ALBEDO = vec3(1.0, 1.0, 0.0);
ALPHA = 0.5; // Alpha fading
}
#pragma usage opaque transparent shadowopaque, prepass, and transparent apply to lit objects only, while unlit applies to unlit objects regardless of opacityopaque is pre-compiledEMISSION = vec3(some_value) to visualize valuesu_, varyings with v_R3D_SurfaceShader* R3D_LoadSurfaceShader(const char* filePath);
R3D_SurfaceShader* R3D_LoadSurfaceShaderFromMemory(const char* code);
void R3D_UnloadSurfaceShader(R3D_SurfaceShader* shader);
void R3D_SetSurfaceShaderUniform(R3D_SurfaceShader* shader, const char* name, const void* value);
void R3D_SetSurfaceShaderSampler(R3D_SurfaceShader* shader, const char* name, Texture texture);
#pragma usage <hints> // Optional: opaque, transparent, shadow, etc.
#define R3D_NO_AUTO_FETCH // Optional: disable automatic material sampling
uniform <type> <name>; // Uniforms
varying <type> <name>; // Varyings (communication between stages)
void vertex() { // Optional: vertex stage
// Modify POSITION, NORMAL, etc.
}
void fragment() { // Optional: fragment stage
// Modify ALBEDO, ROUGHNESS, etc.
}