Skip to content

Custom pipelines — gfx

Compile your own GLSL shaders and draw world-space meshes through them. Below the 3D renderer: where render.box / render.begin give fixed shading, a gfx pipeline is your own vertex + fragment shader, optionally with a texture.

Handles

Handle Built by Role
Pipeline gfx.pipeline{...} Compiled vertex+fragment shader, vertex format, blend, depth
Texture gfx.texture(path) 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 (client thread — top level and render callbacks both qualify). 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 from inline GLSL. A compile error raises a Lua error; the GLSL log lands in latest.log.

Field Default Meaning
name required Label for errors and GPU debug
vertex required GLSL vertex source
fragment required GLSL fragment source
format "position_color" Vertex layout — decides 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"none" draws through walls
depth_write false Whether fragments write depth
cull false Back-face culling

No per-mesh through_walls flag — depth is baked into the pipeline. Use depth_test = "none" for x-ray shapes.

Pipelines recompile after resource reload (F3+T), so a live script keeps working.

Shader environment

Vertex inputs by format:

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

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

Vanilla uniform blocks via #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 interpolated UV0. GameTime (fraction of the day, wraps every 24000 ticks) is the usual animation handle. No custom uniforms — drive everything from GameTime, transforms and vertex data.

gfx.texture

gfx.texture(path) loads a PNG and uploads it. Path is relative to the scripts directory (absolute paths work). Returns the handle plus pixel size. Released when the script unloads.

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

gfx.layer

gfx.layer(pipeline, texture?) binds a pipeline to a texture. position_tex* formats require a texture (a gfx.texture handle or a vanilla id string). Plain position / position_color must be called with no texture. Layers are memoized per (pipeline, texture), so calling 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]) — world-space vec3. Color only when the format carries one; sticky until changed.
  • render.uv(u, v) — texture coords for subsequent vertices (position_tex* only); sticky.
  • render.finish() — flushes the mesh.

Vertex count must match the mode: quads ×4, triangles ×3, strips/fans ≥3, lines ×2. A mismatch raises an error.

Textured, GameTime-scrolling billboard:

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 / rotate_camera are the same transform helpers as built-in 3D drawing.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close