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.