Skip to content

Custom pipelines

Compile your own GLSL pipelines and textures, then draw world-space meshes through them.

render.box and render.begin give fixed shading. A gfx pipeline runs your own vertex and fragment shader instead, optionally with a texture.

Handle Built by Is
Pipeline gfx.pipeline{...} Compiled shader pair, vertex format, blend and depth settings
Texture gfx.texture(path) A PNG uploaded to the GPU
Layer gfx.layer(pipeline, tex?) Pipeline bound to an optional texture, ready to draw

Build them once at script top level (the client thread, which top level and render callbacks both are). Draw each frame with render.begin_layer(layer) inside render_3d.

local pipeline = gfx.pipeline{
    name = "solid",
    vertex = [[
        #version 330
        #moj_import <minecraft:dynamictransforms.glsl>
        #moj_import <minecraft:projection.glsl>
        in vec3 Position;
        in vec4 Color;
        out vec4 vertexColor;
        void main() {
            gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
            vertexColor = Color;
        }
    ]],
    fragment = [[
        #version 330
        in vec4 vertexColor;
        out vec4 fragColor;
        void main() { fragColor = vertexColor; }
    ]],
}
local layer = gfx.layer(pipeline)

module:event("render_3d", function(render)
    local pos = player:position()
    render.begin_layer(layer)
    render.vertex(pos + vec3(0, 2, 0), 0xFF4FF2A6)
    render.vertex(pos + vec3(1, 2, 0), 0xFF4FF2A6)
    render.vertex(pos + vec3(1, 3, 0), 0x004FF2A6)
    render.vertex(pos + vec3(0, 3, 0), 0x004FF2A6)
    render.finish()
end)

gfx.pipeline

Compiles inline GLSL. A compile error raises a Lua error and the GLSL log lands in latest.log. Pipelines recompile after a resource reload (F3+T), so a live script keeps working.

Field Default Meaning
name required Label used in errors and GPU debug output
vertex required GLSL vertex source
fragment required GLSL fragment source
format "position_color" Vertex layout, which decides the shader inputs and what vertex/uv write
mode "quads" quads, triangles, triangle_strip, triangle_fan, lines, line_strip, points
blend "translucent" none, translucent, premultiplied, additive, lightning, overlay, glint, invert
depth_test "lequal" lequal, less, equal, greater, none
depth_write false Whether fragments write depth
cull false Back-face culling

Depth is baked into the pipeline, so there is no per-mesh through_walls. Use depth_test = "none" for x-ray shapes.

Shader environment

format Inputs Texture
position in vec3 Position; none
position_color in vec3 Position; in vec4 Color; none
position_tex in vec3 Position; in vec2 UV0; Sampler0
position_tex_color in vec3 Position; in vec2 UV0; in vec4 Color; Sampler0

The clip transform is gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);. Positions are camera-relative, so feed render.vertex plain world coordinates.

Vanilla uniform blocks come in through #moj_import:

Import Provides
<minecraft:dynamictransforms.glsl> ModelViewMat, ColorModulator, ModelOffset, TextureMat
<minecraft:projection.glsl> ProjMat, projection_from_position
<minecraft:globals.glsl> GameTime, ScreenSize, CameraBlockPos, CameraOffset, …

Textured pipelines declare uniform sampler2D Sampler0; and sample with the interpolated UV0. There are no custom uniforms, so animation rides on GameTime (a fraction of the day, wrapping every 24000 ticks), transforms and vertex data.

gfx.texture and gfx.layer

gfx.texture(path) loads a PNG and uploads it, returning the handle plus its pixel size. The path is relative to the scripts directory, and absolute paths work. It is released when the script unloads.

local tex, w, h = gfx.texture("textures/logo.png")

gfx.layer(pipeline, texture?) binds a pipeline to a texture. position_tex* formats require one, either a gfx.texture handle or a vanilla id string; plain position and position_color must be called without. Layers are memoized per pipeline and texture, so calling it every frame is fine.

local textured = gfx.pipeline{ name = "billboard", format = "position_tex", vertex = ..., fragment = ... }
local layer    = gfx.layer(textured, tex)
local creeper  = gfx.layer(textured, "minecraft:textures/entity/creeper/creeper.png")

Drawing

Inside render_3d, render.begin_layer(layer) opens a mesh whose mode and format come from the pipeline:

  • render.vertex(pos[, color]) takes world-space vec3. Color applies only when the format carries one, and holds until changed.
  • render.uv(u, v) sets texture coordinates for the vertices after it (position_tex* only), and holds until changed.
  • render.finish() flushes the mesh.

The vertex count must match the mode (quads ×4, triangles ×3, strips and fans ≥3, lines ×2) or the call raises an error.

A textured billboard scrolling on GameTime:

local pipeline = gfx.pipeline{
    name = "scroll",
    format = "position_tex",
    blend = "additive",
    depth_test = "none",
    vertex = [[
        #version 330
        #moj_import <minecraft:dynamictransforms.glsl>
        #moj_import <minecraft:projection.glsl>
        #moj_import <minecraft:globals.glsl>
        in vec3 Position;
        in vec2 UV0;
        out vec2 uv;
        void main() {
            gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
            uv = UV0 + vec2(GameTime * 20.0, 0.0);
        }
    ]],
    fragment = [[
        #version 330
        uniform sampler2D Sampler0;
        in vec2 uv;
        out vec4 fragColor;
        void main() { fragColor = texture(Sampler0, uv); }
    ]],
}
local layer = gfx.layer(pipeline, gfx.texture("textures/aura.png"))

module:event("render_3d", function(render)
    for _, p in ipairs(world:players()) do
        if not p:is_self() then
            render.push()
            render.origin(p:position() + vec3(0, p:height() / 2, 0))
            render.rotate_camera()
            render.begin_layer(layer)
            render.uv(0, 0); render.vertex(vec3(-0.6, 0.6, 0))
            render.uv(0, 1); render.vertex(vec3(-0.6, -0.6, 0))
            render.uv(1, 1); render.vertex(vec3(0.6, -0.6, 0))
            render.uv(1, 0); render.vertex(vec3(0.6, 0.6, 0))
            render.finish()
            render.pop()
        end
    end
end)

push, origin and rotate_camera are the same transform helpers the built-in 3D drawing uses.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close