
# Custom pipelines

`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`](world-render.md).

```lua
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[, filter])` 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.

`filter` is `"nearest"` by default, which keeps pixel art crisp. `"linear"` interpolates
between texels, which distance-field textures such as MSDF font atlases need. Both repeat
outside the 0..1 UV range.

```lua
local tex, w, h = gfx.texture("textures/logo.png")
local atlas     = gfx.texture("fonts/inter.png", "linear")
```

`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.

```lua
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](vec3.md). 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`:

```lua
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](world-render.md#transforms) the built-in 3D drawing uses.
