3D models
3D Engine
3D Engine puts real 3D models into vanilla Minecraft. Drop a .glb, .gltf
or .obj file into a folder on your server, run /3d reload, and it is in the
game: the actual mesh, with its own textures and animations, not a blocky
remake of it. Furniture, statues, vehicles, props and creatures, anything you
can download from Sketchfab or export from Blender.
The plugin bakes every file into item models drawn by core shaders, builds a resource pack from them and sends it to players as they join. Players install nothing.
Requirements#
- Spigot or Paper on Minecraft 26.3, on Java 21. The shaders and the pack format are that version's, so the players need to be on 26.3 too.
- On Paper, models that walk use Paper's mob pathfinding; on Spigot they use a path search of the engine's own. Everything else is the same on both.
Installing#
- 1
Get the jar
Download 3DEngine.jar, or get it from SpigotMC. That link is always the current release.
- 2
Drop it in and start the server
3DEngine.jarintoplugins/. On first start it makes:plugins/3DEngine/ config.yml models/ ← your 3D files effects/ ← shader effects, optional cache/ ← baked models, kept between restarts pack.zip ← the resource pack it built - 3
Let players reach the pack
The plugin serves its pack over HTTP on port 8163. Open that port, and set
pack.urlinconfig.ymlto the address players reach it on:pack: url: "http://play.example.com:8163"Left empty, players are sent this server's own IP, which is rarely what they can reach. The console warns you on startup when it is empty.
- 4
Add a model
Put a model file in
plugins/3DEngine/models/and run/3d reload. Then look at a block and/3d place <model>.
How players get the pack#
Every /3d reload builds a new pack, writes it to plugins/3DEngine/pack.zip
and serves it from the plugin's own web server. Players get it as they join,
and everybody online is sent the new one whenever a reload changes it. The
pack's address includes its hash, so a client never keeps a stale copy.
| Key | Default | What it does |
|---|---|---|
pack.host | true | Serve the pack from this server over HTTP |
pack.port | 8163 | The port it is served on |
pack.bind | empty | The address to listen on. Empty is all of them |
pack.url | empty | The address players download it from |
pack.send | true | Send it to players as they join |
pack.required | false | Kick players who decline it |
pack.prompt | empty | The message on the download prompt |
pack.format | [97, 1] | The pack format. Leave it alone unless you know why |
pack.description | 3D Engine models | The pack's description |
Already sending your own pack? Set pack.send: false and merge
plugins/3DEngine/pack.zip into yours after every reload. /3d pack re-sends
the engine's pack to you, for checking it arrived.
The pack replaces some of the game's shaders
Models are drawn by core shaders, so the pack replaces the game's block,
entity, item and terrain shaders. If you add any
shader effects, it also replaces the text shader.
Only one pack can replace a given shader. If another pack on the same client
replaces any of these, whichever is higher in the list wins and the other's
drawing silently breaks. That includes other plugins that draw with the
text shader, such as RP Engine's shader objects. The models' own item
models are in a 3dengine namespace and override nothing vanilla.
Adding models#
Formats: .glb, .gltf and .obj. An .obj file's .mtl and textures,
and a .gltf file's .bin and textures, go beside it.
Where: anywhere under plugins/3DEngine/models/, in folders if you like.
A model's name is its path in that folder, lower case, without the extension:
models/furniture/armchair.glb is furniture/armchair.
Sketchfab downloads go in as they come. Drop the .zip into the models
folder and the next reload unpacks it into a folder of the same name. A file
called scene.gltf, which is what every Sketchfab download contains, takes
its folder's name instead, and its .yml is scene.yml beside it. Delete
the unpacked folder to unpack the zip again.
Baking is cached. A reload only re-bakes a model whose file, .yml or
detail settings changed, so a restart with nothing new is quick. Baking runs
off the main thread, and /3d reload reports how many models and effects are
ready, any that failed and why, and how big the pack came out.
/3d list shows every model with its height and states, and any that failed.
/3d info <model> adds its width, points, seats, hitboxes and what it costs
the client.
Placing models#
| How | |
|---|---|
/3d place <model> [scale] | Puts it on the block you look at, facing you |
/3d give <model> [player] | A model item: right-click a block to put the model there |
Both are saved with the world and come back after a restart. The model
item shows the model itself as its icon and is not used up, so one item places
as many as you like. A player without 3dengine.place can hold one, but using
it places nothing and tells them they need that permission.
Changing a placed model. Look at it (within eight blocks) and:
| Command | |
|---|---|
/3d remove | Takes it away |
/3d scale <n> | Resizes it. 1 is its normal size |
/3d rotate <degrees> | Turns it |
/3d state <state> | Plays one of its animation states |
/3d sit [seat] | Sits you in it, in a named seat or the nearest free one |
Sitting down. Right-clicking a model that has seats sits you in the nearest
free one, if you have 3dengine.ride (everyone does by default). Sneak to get
off. Right-clicking while sneaking never sits you down.
The model's .yml#
Every model file can have a .yml of the same name beside it:
armchair.glb and armchair.yml. Every key is optional. A file with no
.yml comes in at its own size, with one looping state per animation in the
file (or a single still one if it has none).
Positions in blocks (seats, hitboxes) are measured from the middle of the
model's base: x to its right, y up, z forward.
Size and placement#
height: 1.8 # resize to this many blocks tall
# scale: 0.5 # or: blocks per unit in the file (ignored when height is set)
yaw: 90 # turn it so its front faces the way it is placed
offset: [0, 0, 0] # move it, in blocks, after resizing
hide: [Ground, Camera] # leave out parts of the file by node or bone name
icon-scale: 1 # how big it is drawn as an item icon; fitted if left outhide is for the extras a downloaded file often carries: a ground plane, a
backdrop, a camera rig. Everything under a hidden node goes with it.
States and animation#
A state is an animation the model can settle into and loop: idle, walk,
open. Without a states: block every clip in the file becomes one, named after
the clip in lower case.
default-state: idle
states:
idle: {clip: Idle, fps: 10, syncs: 2}
walk: {clip: Walk, fps: 20, wait: true}
sit: {clip: Idle, pose: {Body: {rotate: [-40, 0, 0], translate: [0, -0.3, 0]}}}
shuffle: {clip: Walk, blend: {clip: Idle, weight: 0.45}}
transition-frames: 6
transitions:
- idle <-> walk
- {from: idle, to: sit, frames: 12}| Key | Default | What it does |
|---|---|---|
clip | The animation in the file. A state with no clip is a still pose | |
fps | 10 | Frames baked per second. More is smoother and makes a bigger pack |
syncs | 1 | How many evenly spaced points in the loop it can leave from |
sync-at | Or the exact ticks it can leave from | |
wait | false | Wait for a sync point before leaving, rather than cutting at the nearest |
pose | Bones turned (degrees) or moved on top of the clip | |
blend | Mix in another clip: clip, weight (0 to 1), and at (seconds into it) |
default-state is the state a model starts in. Left out, it is idle if
there is one, or the first state.
A transition is a short blend from one state into another, baked from the
pose the first leaves on. a -> b is one way, a <-> b both ways, and the map
form takes frames and both-ways: true. Each lasts transition-frames
ticks unless it says otherwise. Without a transition, a state change cuts
straight across.
Animations from another file. animations-from: other.glb drives this
model with that file's clips, matched by bone name, and the other file is
not loaded as a model of its own. retarget: {translate: [Body]} limits which
bones' movement is carried over (by default, all of them), and rebind: [Bone]
re-weights the vertices of a bone nothing animates onto their neighbours.
legs: lists IK chains, {chain: [Upper, Lower], target: IKFoot}, that keep
feet planted.
Points#
Named places that follow a bone through every animation, such as a mouth or a hand, for plugins to hang things on:
points:
mouth: {bone: Head, at: [0, 2.1, 2.4]}Looking at players#
look:
bone: Head # only the head turns, on its neck; leave out to turn the whole model
with: [Jaw] # more bones that turn with the head
yaw: 70 # the most it turns either way, in degrees
pitch: 35
range: 8 # blocks
speed: 9 # degrees per tick
states: [idle, sit] # the states it looks in; all if left outlook: true turns the whole model with the defaults above, and look: Head
is the same with a head bone. A turning head is drawn as a second model, so it
costs one more item model per animation.
Rigs often hang the jaw, tongue or ears beside the head bone rather than under
it. Any bone beside it whose vertices sit within the head's own space turns
with the head anyway (the bake log names them); with: adds any it misses.
Seats and driving#
seats:
driver: {at: [-0.4, 0.5, 0.2]}
passenger: {at: [0.4, 0.5, 0.2], bone: Body, pose: standing}
drive:
seat: driver # whose rider steers; the first seat if left out
speed: 0.2 # blocks per tick
sprint: 0.32
back: 0.08
turn: 7 # degrees per tick
jump: 0.42 # 0 = it can't jump
brain: horse # the invisible mob that carries it; picked by size if left outA seat's at is where the rider's hips go, or their feet with
pose: standing. With a bone it rides that bone through every animation.
A rider can click past the model they are sitting in.
With drive:, whoever sits in the driving seat steers: the model goes where
they look while they hold forward. drive: true uses the defaults above. The
brain is an invisible mob under the model that does the walking, and its
size decides what gaps the model fits through.
If riders sit a little high or low on your server, seats.height-offset in
config.yml moves every seat at once.
Hitboxes and solid models#
hitboxes:
- {at: [0, 0, 0.6], width: 1, height: 1.2}
solid: trueHitboxes are what make a model clickable, and they are fitted to it unless
you write your own: one box, or a row of up to six along a long model, so a car
can be clicked at either end. hitbox: {at, width, height} is a single box of
your own, and hitbox: false makes the model unclickable.
solid: true fills a placed model's boxes with barrier blocks, so nobody walks
through a statue. Barriers go only in empty blocks, never where a rider sits,
and are taken away when the model is removed, moved or resized. A model that
moves is never solid.
Held as an item#
item: true lets the model be held as an item. To set how it sits in each
view, give it a map of display contexts, as a block model's display does:
item:
thirdperson_righthand: {rotation: [0, 90, 0], translation: [0, 2, 1], scale: 0.6}
firstperson_righthand: {rotation: [0, 90, 0], scale: 0.5}Walking#
locomotion: tells a model that walks which states to play and when:
locomotion:
idle: idle
walk: walk
run: run
turn: turn # turning on the spot
walk-above: 0.035 # blocks per tick
run-above: 0.13Each name defaults to the state of that name if the model has one. Plugins walk models through the API.
Any other key in the file is kept for plugins to read, so an addon can keep its own settings beside a model.
Detail and pack size#
Every model is simplified to a number of triangles and its texture to a width when it is baked. More is sharper, and makes the pack bigger and the game heavier for players. An animated model stores its whole mesh once per frame and its texture once per animation, so the texture is usually what makes a pack big.
| Where | Wins over |
|---|---|
/3d quality <model> <triangles> [texture width] | everything |
triangles: and texture-width: in the model's .yml | the server's defaults |
quality.triangles and quality.texture-width in config.yml | nothing: 20,000 and 512 |
Nothing ever keeps more than quality.max-triangles (200,000) and
quality.max-texture-width (2,048).
/3d quality with no arguments shows the defaults and every per-model
setting; with a model it shows what that model has. Setting one re-bakes it
straight away and is kept in config.yml under quality.models, so a plugin
that re-copies its own model files cannot undo it. It reads config.yml from
disk before saving, so anything you have changed in the file by hand is kept. /3d quality <model> reset
takes it back to its own settings.
Shader effects#
An effect is a small GLSL program drawn into the world: a fireball, a puff
of flame, a black hole. Put .glsl files in plugins/3DEngine/effects/, run
/3d reload, and /3d effect <name> [size] puts one down where you look,
size blocks across. /3d effect alone lists them, and /3d effect clear
takes away every one put down by the command. They are not saved, so a restart
clears them too.
An effect file defines one function, from a ray to a colour:
vec4 object(vec3 ro, vec3 rd, float t0, float t1, float time, vec3 light, inout float depth) {
// ...
}It is checked before it reaches anybody's graphics card, and refused if it
breaks a rule: no while or do loops, and every for loop needs a literal
bound of at most 160; no preprocessor directives, uniforms, samplers, textures
or gl_ built-ins; no main() and no discard (return an alpha of 0
instead); at most 20,000 characters. There can be up to 255 effects. The
reload names any that were refused and why.
Effects replace the text shader
The text shader is only replaced when the effects folder has at least one
effect in it. Leave the folder empty if another plugin on your server needs
that shader.
Commands#
/3d, or /engine3d. Each subcommand has its own permission,
and /3d alone lists the ones you may use.
| Command | What it does |
|---|---|
/3d list | Every model, its height and states, and any that failed to bake |
/3d place <model> [scale] | Places a model on the block you look at, facing you. Saved with the world |
/3d give <model> [player] | A model item that places the model where it is used |
/3d remove | Takes away the model you look at |
/3d scale <n> | Resizes the model you look at |
/3d rotate <degrees> | Turns the model you look at |
/3d state <state> | Plays a state on the model you look at |
/3d sit [seat] | Sits you in the model you look at |
/3d info [model] | Size, states, points, seats, hitboxes, and what it costs the client |
/3d quality [model] [triangles|reset] [texture width] | Shows the detail settings, or sets one model's and re-bakes it |
/3d effect <name> [size] | Puts down a shader effect. /3d effect clear takes them away |
/3d reload | Re-reads config.yml, re-bakes the models folder and re-sends the pack |
/3d pack | Re-sends you the pack |
From the console, a player's name at the end of the command runs it as
that player: /3d effect fire 2 Steve. The console's own permissions are what
count.
Permissions#
One node per subcommand, so you can hand out exactly the ones you want: whoever may resize a model is not necessarily whoever may re-bake the whole folder. Somebody without a command's node is told which one they need, and tab completion only offers what they may use.
3dengine.admin is a parent granting every command and placing with a model
item, for a server that wants them all in one line. Nothing checks it
directly.
| Node | Default | What it allows |
|---|---|---|
3dengine.admin | op | Every /3d command, and placing models with a model item |
3dengine.list | op | /3d list, every model and any that failed to load |
3dengine.place.command | op | /3d place, putting any model down where you look |
3dengine.give | op | /3d give, a model item for yourself |
3dengine.give.others | op | /3d give <model> <player>, a model item for somebody else (on top of 3dengine.give) |
3dengine.remove | op | /3d remove, taking away the model you look at |
3dengine.scale | op | /3d scale, resizing the model you look at |
3dengine.rotate | op | /3d rotate, turning the model you look at |
3dengine.state | op | /3d state, switching the animation of the model you look at |
3dengine.sit | op | /3d sit, sitting in a seat of the model you look at, by name |
3dengine.info | op | /3d info, a model's size, states, seats and hitboxes |
3dengine.quality | op | /3d quality, seeing or changing how detailed a model is baked (re-bakes it) |
3dengine.effect | op | /3d effect, putting down or clearing shader effects |
3dengine.reload | op | /3d reload, baking the models folder again |
3dengine.pack | op | /3d pack, having the resource pack sent to you again |
3dengine.place | op | Placing models with a model item (from /3d give) |
3dengine.ride | everyone | Sitting in a model's seats by right-clicking it |
3dengine.update | op | Being told on joining when a newer 3D Engine is on SpigotMC |
3dengine.admin grants everything above it in the table and
3dengine.place, but not 3dengine.ride or 3dengine.update: riding is
everyone's already, and update notices are a preference rather than a power.
Why /3d place is 3dengine.place.command. 3dengine.place already meant
using a model item, and a server that hands those items to players should not
find that it also lets them put down any model they like, anywhere.
3D Engine checks ordinary Bukkit permissions, so LuckPerms and anything else that provides them works:
/lp group builder permission set 3dengine.give true
/lp group builder permission set 3dengine.place true
/lp group admin permission set 3dengine.admin trueConfiguration#
plugins/3DEngine/config.yml. Edit it and run /3d reload, and the changes
take effect, except for pack.host, pack.port and pack.bind: the pack
server is started with the server, so those need a restart, and the reload
tells you when one of them has changed. If the file has a mistake in it and
can't be read, the reload says so and keeps the settings it already had, and
/3d quality refuses to save rather than write over it. Saving keeps the
file's comments.
pack:
host: true
port: 8163
bind: ""
url: "" # e.g. http://play.example.com:8163
send: true
required: false
prompt: ""
format: [97, 1]
description: "3D Engine models"
quality:
triangles: 20000
texture-width: 512
max-triangles: 200000
max-texture-width: 2048
models: {} # per model, written by /3d quality
seats:
height-offset: 0.0 # raise (or, negative, lower) every seated rider
telemetry: true
update-check: trueTwo switches concern what the plugin connects to:
update-checkasks SpigotMC every six hours whether a newer 3D Engine is out, and if so tells the console and anybody with3dengine.update.telemetryis an anonymous usage count: every 30 seconds, a random id the plugin made up the first time it started, how many players are online, and the names of installed plugins that depend on 3D Engine, sent to resourcepack.ai. No address, server name, player names or content.
Set either to false to turn it off.
Developer API#
Add depend: [3DEngine] to your plugin.yml, compile against 3DEngine.jar
as a compile-only dependency, and call Engine3D.get():
Engine3D engine = Engine3D.get();
engine.whenReady(() -> {
ModelInstance chair = engine.place("furniture/armchair", location); // saved with the chunk
chair.mount(player, null); // into the nearest free seat
chair.lookAt(player); // or lookAround(), lookAway()
ModelInstance dog = engine.spawn("dog", location); // not saved
Mover legs = engine.mover(dog, EntityType.WOLF);
legs.follow(player);
});whenReady runs once every model is baked, and again after every
/3d reload, so a plugin can respawn what it drew against the new models. A
plugin can copy its own model files into engine.modelsFolder() in its
onEnable: the first bake waits for the first tick.
Engine3D | Models and instances; spawn (not saved) and place (saved); mover; item(model, state), an item that looks like the model; effect, particle and the effect variants |
Model | A baked model: its states, points, seats, height, and the extra keys from its .yml |
ModelInstance | One model in the world: change state through its transitions, find where a point is on any frame, scale, seat riders, look at things, turn hitboxes on and off |
Mover | Legs for a model: an invisible mob that follows, goes to a place, faces, and plays walk, run and turn to match its speed |
EffectInstance, Particle | Shader effects placed in the world, and ones the engine flies |
Engine3D.instance(entity) finds the model any of its entities belongs to:
its display, its head, a hitbox, a seat or its brain.
Events#
All in ai.resourcepack.engine3d.api.event:
| Event | When | Cancel to |
|---|---|---|
ModelInteractEvent | A player right-clicks a model | Stop the default (sitting down) |
ModelDamageEvent | A player punches it, or a projectile or mob hits it | Tell later listeners |
ModelMountEvent | Something is about to sit in a seat | Keep them off |
ModelDismountEvent | A rider is getting off | Keep them on, when isCancellable() |
ModelStateChangeEvent | A new state's loop starts | |
ModelSpawnEvent / ModelRemoveEvent | A model is put down / taken away |
A model has no health of its own: what a hit does is up to you. 3D Pets is an addon built this way, on nothing but this API.
Credits#
The meshes are drawn by obj³, after Godlander's objmc. Its core shaders ship in the pack, with their licence.