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.
From the modding book of arismoko/horizonbfbda6a
On this page
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:
| Name | Type | Meaning |
|---|---|---|
input.uv | vec2<f32> | The quad's texture coordinate at this fragment, as its corners gave it. |
input.color | vec4<f32> | The quad's colour in linear light; alpha is coverage. |
input.position | vec3<f32> | This point in blocks, relative to the camera. |
frame.time | f32 | Seconds of game time, smooth between ticks, wrapping to 0 every hour. |
frame.viewport | vec2<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.
| Error | When |
|---|---|
RenderPipelineError:: | No mod registered that id. |
RenderPipelineError:: | The 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:: | The handle is from before a resource reload. |
PipelineDrawError:: | A corner or texture coordinate is not finite. |
PipelineDrawError:: | The view's 4,096 quads, shared with WorldDrawContext::, 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.
Edit this page on GitHub · Spotted something wrong? Open an issue .