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.