Skip to main content

Overview

Custom Pipeline graphics mods allow advanced users to replace Dolphin’s internal pixel shader with custom GLSL code. This enables complete control over final pixel output, including custom lighting, normal mapping, texture manipulation, and visual effects.
This is an advanced feature requiring knowledge of GLSL shading and graphics programming.

Graphics Mod Integration

Custom pipelines are defined as graphics mod actions. You must be familiar with the graphics mod format before using custom pipelines.

Action Type

The action type is custom_pipeline with the following structure:
passes
array
required
Array of rendering passes. Currently only single-pass is supported.

Complete Example

{
  "assets": [
    {
      "name": "material_replace_normal",
      "data": {
        "": "normal.material.json"
      }
    },
    {
      "name": "shader_replace_normal",
      "data": {
        "metadata": "replace_normal.shader.json",
        "shader": "replace_normal.glsl"
      }
    },
    {
      "name": "normal_texture",
      "data": {
        "texture": "normal_texture.png"
      }
    }
  ],
  "features": [
    {
      "action": "custom_pipeline",
      "action_data": {
        "passes": [
          {
            "pixel_material_asset": "material_replace_normal"
          }
        ]
      },
      "group": "PipelineTarget"
    }
  ],
  "groups": [
    {
      "name": "PipelineTarget",
      "targets": [
        {
          "texture_filename": "tex1_512x512_m_afdbe7efg332229e_14",
          "type": "draw_started"
        },
        {
          "texture_filename": "tex1_512x512_m_afdbe7efg332229e_14",
          "type": "create_texture"
        }
      ]
    }
  ]
}

Shader Format

Entry Point

All custom shaders must provide this entry point:
CustomShaderOutput custom_main(in CustomShaderData data)

Output Structure

CustomShaderOutput
struct

Input Data Structure

CustomShaderData
struct

Lighting Data

CustomShaderLightData
struct

TEV Stage Data

TEV (Texture Environment) stages represent GameCube/Wii graphics operations.
CustomShaderTevStage
struct

TEV Input Types

CUSTOM_SHADER_TEV_STAGE_INPUT_TYPE_PREV     // From previous stage
CUSTOM_SHADER_TEV_STAGE_INPUT_TYPE_COLOR    // From color data
CUSTOM_SHADER_TEV_STAGE_INPUT_TYPE_TEX      // From texture
CUSTOM_SHADER_TEV_STAGE_INPUT_TYPE_RAS      // From rasterizer
CUSTOM_SHADER_TEV_STAGE_INPUT_TYPE_KONST    // Constant from software
CUSTOM_SHADER_TEV_STAGE_INPUT_TYPE_NUMERIC  // Numeric constant (0 or 1)

Shader Examples

Single Color Output

CustomShaderOutput custom_main(in CustomShaderData data)
{
    CustomShaderOutput custom_output;
    custom_output.main_rt = vec4(1.0, 0.0, 0.0, 1.0);  // Red
    return custom_output;
}

Display Normal Map

CustomShaderOutput custom_main(in CustomShaderData data)
{
    CustomShaderOutput custom_output;
    // Convert normal from [-1,1] to [0,1] for display
    custom_output.main_rt = vec4(data.normal * 0.5 + 0.5, 1);
    return custom_output;
}

Reading Custom Texture

CustomShaderOutput custom_main(in CustomShaderData data)
{
    CustomShaderOutput custom_output;
    // samp_MY_TEX is defined in shader asset
    // TEX_COORD0 is first texture coordinate
    custom_output.main_rt = texture(samp_MY_TEX, TEX_COORD0);
    return custom_output;
}

First Game Texture

Display the first texture the game uses, ignoring other operations:
CustomShaderOutput custom_main(in CustomShaderData data)
{
    vec4 final_color = data.final_color;
    uint texture_set = 0;
    
    for (uint i = 0; i < data.tev_stage_count; i++)
    {
        // Check 4 color inputs per stage
        for (uint j = 0; j < 4; j++)
        {
            if (data.tev_stages[i].input_color[j].input_type == 
                CUSTOM_SHADER_TEV_STAGE_INPUT_TYPE_TEX && texture_set == 0)
            {
                final_color = vec4(data.tev_stages[i].input_color[j].value, 1.0);
                texture_set = 1;
            }
        }
    }
    
    CustomShaderOutput custom_output;
    custom_output.main_rt = final_color;
    return custom_output;
}

Point Light Lighting

Apply point lighting for channel 0 color lights:
CustomShaderOutput custom_main(in CustomShaderData data)
{
    float total_diffuse = 0;
    vec3 normal = data.normal;
    
    for (int i = 0; i < data.light_chan0_color_count; i++)
    {
        if (data.lights_chan0_color[i].attenuation_type == 
            CUSTOM_SHADER_LIGHTING_ATTENUATION_TYPE_POINT)
        {
            vec3 light_dir = normalize(
                data.lights_chan0_color[i].position - data.position.xyz
            );
            
            float attn = (dot(normal, light_dir) >= 0.0) 
                ? max(0.0, dot(normal, data.lights_chan0_color[i].direction.xyz)) 
                : 0.0;
            
            vec3 cosAttn = data.lights_chan0_color[i].cosatt.xyz;
            vec3 distAttn = data.lights_chan0_color[i].distatt.xyz;
            
            attn = max(0.0, dot(cosAttn, vec3(1.0, attn, attn*attn))) 
                / dot(distAttn, vec3(1.0, attn, attn * attn));
            
            total_diffuse += attn * max(0.0, dot(normal, light_dir));
        }
    }
    
    CustomShaderOutput custom_output;
    custom_output.main_rt = vec4(total_diffuse * vec3(0, 0, 1), 1); // Blue base
    return custom_output;
}

Texture Sampling

Access textures using samp_<NAME> samplers defined in your material asset:
// Defined in material asset as "MY_TEX"
texture(samp_MY_TEX, data.texcoord[0])

// Built-in coordinate macros
texture(samp_MY_TEX, TEX_COORD0)
texture(samp_MY_TEX, TEX_COORD1)

Time-Based Animation

Use data.time_ms for animated effects:
CustomShaderOutput custom_main(in CustomShaderData data)
{
    float time_sec = float(data.time_ms) / 1000.0;
    float pulse = (sin(time_sec * 2.0) + 1.0) / 2.0; // 0 to 1
    
    CustomShaderOutput custom_output;
    custom_output.main_rt = vec4(pulse, pulse, pulse, 1.0);
    return custom_output;
}

Best Practices

  • Minimize texture lookups in loops
  • Cache frequently accessed values
  • Use early returns when possible
  • Avoid complex branching in inner loops
  • GLSL is converted to backend-specific shaders (D3D, Vulkan, Metal)
  • Test on multiple backends if possible
  • Avoid GLSL extensions unless necessary
  • Use standard GLSL types and functions
  • Start with simple solid colors to verify shader loads
  • Output intermediate values as colors for visualization
  • Check Dolphin log for shader compilation errors
  • Use data.final_color as fallback
Shaders are compiled at runtime. Syntax errors will prevent the graphics mod from loading.

Limitations

  • Single pass only: Multi-pass rendering not currently supported
  • Pixel shaders only: Vertex shaders cannot be customized
  • View space only: Position and normal data are in view space, not world space
  • Backend conversion: GLSL is converted to target API, some features may not translate

Graphics Mods

Learn the graphics mod system basics

Resource Packs

Package mods for distribution