Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Drawing with your own render pipeline

A client mod can draw world quads with its own fragment shader. The mod describes the pipeline as data, the game builds it, and the mod draws by handle. This is the counterpart of NeoForge 26.3's RegisterRenderPipelinesEvent, in a restricted form: the mod supplies the fragment stage and the host fixes the rest. Vanilla requires a vertex shader and a primitive type; this API fixes both rather than exposing the full pipeline description.

1. Write the shader

Put a WGSL file at assets/<namespace>/shaders/pipeline/<path>.wgsl. It defines one function:

fn pipeline_fragment(input: PipelineInput) -> vec4<f32> {
    let pulse = 1.0 + sin(frame.time * 4.0);
    return vec4(input.color.rgb * pulse, input.color.a * (1.0 - input.uv.y));
}

The game puts this before your file:

NameTypeMeaning
input.uvvec2<f32>The quad's texture coordinate at this fragment, as its corners gave it.
input.colorvec4<f32>The quad's colour in linear light; alpha is coverage.
input.positionvec3<f32>This point in blocks, relative to the camera.
frame.timef32Seconds of game time, smooth between ticks, wrapping to 0 every hour.
frame.viewportvec2<f32>The world picture's size in pixels.

Colours follow the mod render colour rule: the function reads and returns linear light, 1.0 is paper white, and more is brighter where the frame can hold it. The alpha you return is coverage. The game supplies the vertex stage, the bindings and the conversion to the frame, so a file that declares a binding or an entry point is refused. Names that start with horizon_ belong to the game.

2. Register the pipeline

fn register_render_pipelines(
    &self,
    _context: &RegistrationContext,
    event: &mut RegisterRenderPipelinesEvent<'_>,
) {
    event.pipelines.push(RenderPipeline::new(
        Identifier::parse("example:glow").unwrap(), // the pipeline's id
        Identifier::parse("example:glow").unwrap(), // assets/example/shaders/pipeline/glow.wgsl
        PipelineBlend::Additive,
        WorldDepth::Tested,
    ));
}

PipelineBlend::Translucent lays the result over the frame by its alpha, and Additive adds it, scaled by its alpha. WorldDepth::Tested hides the quads behind nearer world surfaces, and ThroughWalls draws them over everything. No pipeline writes depth.

The id must be in a namespace your mod owns. There is no per-mod pipeline count cap. A registration that breaks either rule, or reuses an id, is logged with the mod's id and dropped.

3. Resolve and draw

fn client_setup(&self, host: &dyn ClientHost, _event: &mut ClientSetupEvent<'_>) {
    let id = Identifier::parse("example:glow").unwrap();
    *self.glow.lock().unwrap() = host.render_assets().render_pipeline(&id).ok();
}

fn submit_custom_geometry(&self, _host: &dyn ClientHost, event: &mut SubmitCustomGeometryEvent<'_>) {
    let Some(glow) = *self.glow.lock().unwrap() else { return };
    let quad = PipelineQuad::spanning(corners, 0xFFFF_C040);
    if let Err(error) = event.draw.pipeline_quads(glow, &[quad]) {
        log::warn!("glow not drawn: {error:?}");
    }
}

Resolve again in client_resources_reloaded: every resource reload builds the pipelines again from the new resources, so a resource pack can replace your shader, and a handle from before the reload answers Stale.

ErrorWhen
RenderPipelineError::UnknownNo mod registered that id.
RenderPipelineError::InvalidThe shader file is missing or was refused. The log has a line render pipeline <id> refused: <file>: <reason>, with the file's line number for a syntax or type error.
RenderPipelineError::StaleThe handle is from before a resource reload.
PipelineDrawError::NonFiniteA corner or texture coordinate is not finite.
PipelineDrawError::CapacityExceededThe view's 4,096 quads, shared with WorldDrawContext::quads, are used up.

A refused batch draws nothing. A pipeline that is Invalid never stops the game from starting.

What a pipeline draws, and when

pipeline_quads works wherever a WorldDrawContext is given: submit_custom_geometry and each render_level_stage. Within a stage, pipeline quads draw after that stage's plain quads and lines, in the order submitted. Under a shader pack they follow the plain quads: a stage the pack draws itself is drawn with the next stage the game reaches.

A pipeline cannot sample textures, take values of its own from the mod, or replace the vertex stage yet.