generateVanilla is the converter in ResourcePackScripts that turns the Sodium variant of a pack into its Vanilla variant, and, with --limit 2, its Lite variant. The Sodium variant's 3D models are .obj files that only render with the Special Model Loader client mod. generateVanilla bakes each of them with objmc into an ordinary block model plus a texture that also carries the geometry, and the shaders in the pack draw the real shape, so players see the same models without any mods. This page explains what it does, step by step. The rules this means for pack authors are in Pack repository.
Run it
You need Python 3.10 or newer. In a checkout of ResourcePackScripts:
pip install -r requirements.txt
python generateVanilla/generateVanilla.py <sodium pack> <output folder> <vanilla resources> --objmc generateVanilla/objmc.py
<sodium pack>is a checkout of the pack repository, for example../RP-Human.<vanilla resources>is a folder holding theassets/folder of the Minecraft client jar. The server uses 26.2's, so use those to see what a release will contain. Pack repository says how to get it.- Always pass
--objmc. Without it, the converter looks forobjmc.pyin the folder you run it from. - Use a new, empty output folder every run. The converter adds to an existing folder but never deletes from it, so files removed from the pack would stay behind.
| Option | What it does |
|---|---|
--objmc PATH |
The objmc script to bake with. Default: objmc.py in the current folder. |
--limit N |
Keep only the first N models of each blockstate list. The Lite variant uses --limit 2. N must be 1 or more. |
--compress |
Write the generated JSON without indentation. |
--debug |
Print every step. The same lines go to debug.log. |
--noblocks |
Skip the blockstates. |
--noitems |
Skip the item definitions. |
Overview
flowchart TD
A["pack.mcmeta, pack.png, license.txt, README.md"] --> OUT["Output pack"]
B["assets/, except the rebuilt folders"] --> OUT
C["vanilla/assets/, except the rebuilt folders"] --> OUT
D["Overlay folders"] --> OUT
S["The shader base"] --> OUT
V["Vanilla resources: the list of blockstates and items"] --> W["Walk every blockstate, then every item"]
W -->|"mcme: model"| OBJ["Bake with objmc"] --> OUT
W -->|"other model"| CH["Copy what the pack overrides along its parent chain"] --> OUT
The input pack is only read. Temporary files go to a temporary folder.
1. pack.mcmeta and the top-level files
pack.mcmetais read from the pack root. The wordSodiuminpack.descriptionbecomesVanilla(case-sensitive). Everything else, overlays included, is kept. The description must be a plain string: a text component stops the run.pack.png,license.txtandREADME.mdare copied. No other top-level file is.
2. Copying assets
assets/ is copied as it is, except these folders in every namespace, because step 5 rebuilds them from what is used:
| Skipped | Why |
|---|---|
blockstates, items |
Rewritten from the walk |
models |
Only models the walk reaches are written |
textures/block, textures/item |
Only textures those models use are copied |
assets/mcme/sml_load_scopes |
Only Special Model Loader reads it |
The rest of textures/ (gui, entity, font and so on) is copied whole. The modelengine namespace keeps its models and items, because its own loader reads them.
So a model, or a textures/block texture, that no blockstate or item definition reaches, directly or through a parent, is not in the output.
3. The vanilla folder and overlays
vanilla/assets/is copied on top, with the same folders skipped. Sovanilla/assets/*/modelsandvanilla/assets/*/textures/block|itemare never used. Onlyvanilla/'sblockstatesanditemsplay a part, and those are read in step 5. A pack withoutvanilla/gets the warningMissing vanilla overrides folder, which is harmless.- Overlay folders are copied as they are: every top-level folder whose name starts with
1_, every other folder thatpack.mcmetalists underoverlays(such asmc26_3), and thenvanilla/1_*on top of the folder of the same name. Their contents are not converted: anmcme:model inside an overlay isn't baked.
4. The shader base
The newest shader base and the pack's modules are written into the output, over the copies the pack's repository has. For the Lite variant (any run with --limit), its mcme_lite.glsl defines MCME_LITE, so fluids are drawn as plain textures.
This step stops the run if the pack's copy of a base or module file was changed by hand. At the end of the run, the converter also signs the fluid textures, checks that every shader import resolves and that no shader breaks a platform rule, and stops if not. See The shader base.
5. Walking the blockstates and items
The converter doesn't walk the pack. It walks the vanilla client's assets/minecraft/blockstates/*.json, then assets/minecraft/items/*.json, from the vanilla resources folder. For each name it takes the first of:
- the pack's
vanilla/assets/minecraft/…file; - the pack's
assets/minecraft/…file; - the vanilla client's own file.
Only files from 1 or 2 are written to the output. A vanilla client's file is only walked, to copy any models and textures the pack overrides along its chains.
So the vanilla resources decide what can be converted. A blockstate for a block that isn't in that version's list is never read, and is missing from the output. The same goes for anything outside the minecraft namespace. The server uses Minecraft 26.2's resources, so blocks added in 26.3 are dropped.
- Blockstates: every value of
variants, and every part'sapplyinmultipart. A value is one model entry or a weighted list of them. --limit Ncuts every such list down to its first N entries before anything else happens. The written blockstate names only those. Weights aren't changed. Item definitions aren't limited.- Item definitions: every
{"type": "model", "model": …}and{"type": "special", "base": …}node is found, however deeply nested, and handled like a blockstate entry.
Each model entry then goes one of two ways:
- an
mcme:model is baked with objmc, and the entry is pointed at the baked model; - any other model has its parent chain followed, up to a
builtin/parent. Each model along the way that the pack has is copied, with every texture it names (plain or{"sprite": …}) and the texture's.png.mcmeta. A model only the vanilla client has is read, so the chain can continue, but not copied. A model neither has gives the warningMissing model file.
The water and lava textures (block/water_still, water_flow, lava_still, lava_flow and their .mcmeta) are always copied from the pack if it has them, used or not.
Baking a model with objmc
objmc turns a 3D model into an ordinary block model plus a texture. The texture carries the geometry as pixel data next to the real picture, and objmc's core shaders, part of the shader base, read that data and draw the real shape. The baked models only render with those shaders. ResourcePackScripts bundles its own Python port of objmc as generateVanilla/objmc.py, for static block models.
For one mcme: model entry the converter:
- finds the files: the model JSON
assets/mcme/models/<path>.json; the.objnamed in itsmodelkey; the.mtlnamed inmtl_override, or else the one with the model's own name; and the optional.objmetawith the model's name; - finds the texture: the
.objmeta'stexture, or else the.mtl's firstmap_Kdline.mcme:textures come fromassets/mcme/textures/,minecraft:ones from the pack'sassets/minecraft/textures/; - rotates a temporary copy of the
.obj, if the entry has a rotation (see Rotations); - runs objmc with the
.objmetasettings and four mipmap levels; - fits the result into the pack: the baked model goes to
assets/mcme/models/<path>[suffix].jsonand its texture toassets/mcme/textures/<path>[suffix].png. The texture is also the particle texture.display,gui_lightand everytintindexare removed, so baked models aren't biome-tinted; - shares the geometry, if the
.objis a shared parent (see Shared parents).
After every model is baked, bakes that hold the same picture are merged, so each texture is stored once.
A model whose JSON has no model key, or whose .obj is missing, is skipped without any message. A model that fails for another reason (a missing .mtl or texture, an objmc error) is skipped with a message. Either way the blockstate still names the model, so the block shows as a missing model in game.
What the baked texture looks like:
- Width: the same as the source texture's. It must be at least 8 pixels.
- Height: rounded up to a power of two, and always taller than the source, because the geometry is stored in the texture too. A simple model with a 16×16 texture bakes to 16×128; one with a 512×512 texture to 512×1024. More faces make it taller.
- Mipmapping: the picture starts on a 16-row boundary, padded with repeated edge rows, which the shader base's objmc shaders expect.
- Transparent pixels take the colour of their nearest visible neighbour (alpha stays 0), so edges don't turn dark at a distance.
- Edges: if every pixel is fully transparent or fully opaque, the shader draws crisp cut-out edges. One pixel with partial transparency makes the whole texture render with soft edges.
objmc's limits:
- Every vertex must stay within ±128 blocks of the model's origin.
- UVs outside 0 to 1 are clamped.
- Faces with more than four corners use only the first four.
- objmc's own warnings about these aren't shown when the bake succeeds, so check odd-looking models in the source.
Conversion settings (.objmeta)
An optional YAML file next to the model JSON, with the same name. Every key is optional.
| Key | Default | Effect |
|---|---|---|
texture |
the .mtl's map_Kd |
The texture to bake from: mcme:block/x, or minecraft:block/x for one in the pack's own assets/minecraft/textures/ (not the vanilla client's) |
output_texture |
the model's path | Name of the baked texture, under assets/mcme/textures/ |
offset |
-0.5 0.0 -0.5 |
Moves the model before baking. Three numbers in one string, separated by spaces. A YAML list stops the run. |
options |
none | A list: noshadow turns off face shading; flipuv is for a texture that comes out upside down. Nothing else is recognised. |
visibility |
7 |
Where the model shows, as a sum: 4 in the world, 2 in the hand, 1 in the GUI |
omnidirectional_parent |
false |
For a shared parent .obj only: the model looks the same from every side, so all its rotations share one parent |
For example:
offset: "-0.5 0.0 -0.5"
options: [noshadow]
visibility: 6
- The old
parentkey is ignored. Sharing now comes from the.obj's name. - A
.objmetawith a YAML error is reported (Error parsing objmeta file …) and ignored: the defaults are used. - An empty
.objmetastops the run. Delete it instead.
Rotations
A blockstate entry's x, y or z rotation is baked into the model:
- Only one axis. The first of
x,y,zis used. The others are dropped with the warningMultiple rotations for …. - The rotation must be a number. Otherwise the model is skipped with
Rotation … is not a number. - Each rotation is a separate bake with a suffix:
pine_leaves_y_90. Eveny: 0makes its own_y_0bake. - The
.objis rotated around the block's centre (0.5, 0.5, 0.5), in a temporary copy. The pack's file isn't touched.
Shared parents
Many models use the same shape with different textures. The Vanilla variant can store that shape once, as a parent model, so the pack stays smaller.
- Which
.objis shared: one namedparent, or ending in_parent, optionally followed by_<number>.leaves_parent.obj,leaves_parent_2.objandparent.objare shared parents;leaves_parent_1_2.objandlamp.objare not. - When it is shared: when a second model in the same run reads the same
.obj, at the same rotation, and its bake has the same texture size. A shared parent used by only one model keeps its geometry inline. - The parent model is written as
assets/mcme/models/<path of the .obj>.json:leaves_parent.objgivesleaves_parent.json. Each model that shares it gets its own small model JSON withparentand its own texture. - y rotations: models baked with a
yrotation of 90, 180 or 270 share a separate parent per angle:leaves_parent_1_2,_1_3,_1_4. If the name already ends in a number, the angle goes after it:leaves_parent_2_2. Withomnidirectional_parent: true, every rotation shares the one parent. - Blocks the game shifts at random (flowers, grass, ferns and other plants that sit off-centre in vanilla) get their models baked centred, so the shift can't carry them into the next block. Those bakes never share a parent with the others.
Make every texture used with one parent
.objthe same size. The geometry in a baked texture depends on the texture's size, so only bakes of the same size can share. When sizes differ you get… shares parent group … but its baked texture is …, not … - not sharing a parent with it. Treat it as an error and resize the textures.
Animated textures
A texture with a .png.mcmeta that has an animation section is treated as a flipbook:
- Frame size: the
.mcmeta'swidthandheight. With neither, square frames as wide as the sheet's shorter side; with only one, the other is the sheet's full size. - The sheet must divide evenly into frames. If it doesn't, you get
… does not divide into WxH frames - baking it unanimated. - Fewer than two frames count as no animation.
- objmc bakes the model once, and the bake is repeated for every frame, each copy with that frame's picture, stacked into one tall texture. Frames are read left to right, then top to bottom, so grid sheets work.
- The output
.mcmetais a copy of yours with the frame size of the bake. Frame numbers past the end of the sheet are dropped, andinterpolateis turned off. You get aNote:line for each.
What stops a run, and what doesn't
A problem with one model or file normally doesn't stop the run. The converter prints a message, skips that model or file, and carries on. Read the messages after every run: Troubleshooting lists them.
These stop the run with a Python error (exit code 1):
- invalid JSON in any blockstate, item definition or model the walk reaches, in
pack.mcmeta, or in the.png.mcmetaof a texture that gets baked; - a text-component pack description;
- an empty
.objmeta, or anoffsetthat isn't three numbers in a string; - a missing input pack folder;
- a broken rule of the shader base (an
ERROR:line names it).
A missing or wrong vanilla resources folder doesn't stop anything: the walk finds no blockstates or items, writes none, and the run ends normally. Check that folder if the output has no blockstates.
Exit codes: 0 when the run gets to the end, even with warnings and skipped models; 1 for a stop; 2 for wrong command line arguments.
In a release, a stop ends the release with ERROR: generateVanilla stopped, and nothing is published.
Output and logging
- With
--debug, every step is printed: every blockstate and item, every model converted, copied or read, every texture copied, every shared parent and flipbook. That is about 120,000 lines for Human. debug.log, in the folder you run from, gets the--debuglines plus Pillow's own messages. It is overwritten on every run.- Warnings go to the console only, not to
debug.log. --compresswrites blockstates, item definitions and baked models without indentation.
Speed
Everything runs one after another, and baking dominates: every model entry is a separate objmc run of roughly 50 to 100 ms for a normal texture, more for big ones (about 1¼ s for 1024×1024). A model used by several blockstates is baked again for each one. On the release server a Human Vanilla variant (about 2,200 bakes) takes about 3½ minutes, and Lite about 2.
Other scripts in the repository
Standalone helpers that generateVanilla doesn't use:
| Script | What it does |
|---|---|
generateVanilla/rotate_obj.py <file.obj> <axis> <angle> |
Rotates an .obj in place by 90, 180 or 270 degrees around x, y or z. |
finder.py <model> |
Reports which blockstates and parent models use the block model <model> (name without block/). Read-only. |
sorter.py |
Moves every block model and texture that no blockstate uses into unlinked_models/ and unlinked_textures/, without asking. Run it on a copy. |
finder.py and sorter.py don't read a pack. They run in a folder laid out as blockstates/, vanilla_blockstates/, models/block/, vanilla_models/block/ and textures/block/.
The shader scripts (syncShaderBase.py, checkShaders.py, signFluids.py, applyShaderBase.py) are on The shader base.
Working on the converter
The code map, the tests and the rules the code follows are in the ResourcePackScripts README. Branch from development and open pull requests into it; a release is a pull request from development into master. Change the objmc shaders in the shader base together with objmc.py, in the same pull request.