Skip to content
VSPT

Making material packs

VSPT renders Vintage Story with a path tracer, so a surface’s look depends on what it is made of: how rough or polished it is, whether it is metal, where it has relief, and whether it glows. A material pack describes that for a set of textures, with a few images per texture.

This kit has everything to make one:

vspt-pack.exe Builds a material pack from the images your paint program exports, checks it the way the game will, and zips it for the Mod DB
sample/ A complete pack project: dirt, the chest, granite, the temporal gear, the glass pane, the anvil and the iron pickaxe
vsptpbrsample_1.0.0.zip The sample, built: drop it into your Mods folder to see it in the game

vspt-pack needs the .NET 10 runtime, which Vintage Story itself needs, so if the game runs, the tool runs.

A material pack is an ordinary mod. It does nothing without VSPT, and the game loads it fine without VSPT. Do not make your pack depend on vspt: a player without the path tracer could not then use your pack’s textures either.

vspt-pack new mypack

makes a project folder. Put images in it, named after the texture they belong to:

mypack/
pack.json your mod's name, id, version: like modinfo.json
textures/game/block/soil/fertmedium.normal.png vanilla's medium soil: its normal map
textures/game/block/soil/fertmedium.height.png its height map
textures/game/block/soil/fertmedium.roughness.png its roughness

then:

vspt-pack build mypack builds mypack/build/mypack/ and checks it
vspt-pack install mypack builds and copies it into the game's Mods folder
vspt-pack zip mypack builds, checks and zips it: mypack/build/mypack_1.0.0.zip

Join a world and type .vspt materials in chat: it lists the packs it read and anything wrong with them. After changing an image, build or install again, then leave the world and join it again; there is no need to restart the game.

An image’s path is the texture’s path under the game’s assets/<folder>/textures/, with its domain in front, and the channel before .png:

textures/<domain>/<path>.<channel>.png
The game’s texture Your image
assets/survival/textures/block/stone/rock/granite1.png (domain game) textures/game/block/stone/rock/granite1.roughness.png
assets/survival/textures/item/resource/temporalgear.png textures/game/item/resource/temporalgear.emissive.png
Another mod’s assets/coolmod/textures/block/lamp.png textures/coolmod/block/lamp.normal.png

Vanilla’s textures are all domain game, whether they sit in the game’s survival, creative or game folder.

Alternate textures each need their own images. Granite is granite1 to granite4, so give all four, or the ones without images keep the default look.

Every image of a texture is the texture’s size, texel for texel: a 32x32 texture has 32x32 maps. One texel of art, one material. An image of 1x1 pixel is the same value everywhere (handy for “all of it is metal”).

Channel What it is Values
.normal.png Which way each texel’s surface faces RGB tangent-space normal, Y up (OpenGL). A DirectX map (Y down)? Set "normalY": "down" in pack.json, or name it .normaldx.png
.height.png Relief Grey, 128 is the surface, the full range is 4 texels of depth. With no normal map, the normals are worked out from it
.roughness.png How rough Grey: 0 a mirror, 255 fully matte. Polished metal about 60 to 120; stone, wood and soil about 180 to 230
.smoothness.png The same upside down For tools that export smoothness or gloss. Give one of the two
.metallic.png Metal or not 255 metal, 0 not. A texel is metal or it is not: avoid the grey in between
.emissive.png Where it glows Grey, 0 none to 255 full. How bright and in what colour comes from a rule (below)
.translucency.png Light passing through thin things Grey, 0 none (the default) to 255. Leaves, cloth, paper. Not glass: glass is glass because the game draws it as glass
.tracedalbedo.png Base colour, path tracer only A version of the texture for the path tracer alone: shadows or a painted glow taken out of the art, since the path tracer makes its own. The game without the path tracer keeps the normal texture
.albedo.png Base colour, for everyone Replaces the texture itself, as a texture pack does, in both renderers

Your tool’s names work too: .basecolor, .diffuse, .nrm, .displacement, .bump, .gloss, .metalness, .emission and a few more are read as the channel they mean.

Leave out what you do not need. A texture with only .roughness.png gets flat normals; one with only a normal map keeps the default finish. Missing parts of the surface map default to roughness 0.85, not metal, no glow and no translucency.

Ready-made maps in this renderer’s own format, <name>_n.png and <name>_m.png, are copied as they are. Their layout is at the end of this guide.

It is your mod’s modinfo.json, and becomes it, with a few settings of the tool’s own:

{
"modid": "mypack",
"name": "My PBR pack",
"version": "1.0.0",
"description": "Materials for VSPT.",
"authors": ["you"],
"type": "theme",
"side": "Client",
"dependencies": { "game": "1.22.0" },
"normalY": "up"
}
Key
modid Lowercase letters and digits only, as the game requires
type theme for a pack of maps, which changes how things look and nothing about the game. content if it also changes game content
side Client: a material pack is for the player’s own screen, so a server never asks for it and it works on any server
normalY up (OpenGL, the default) or down (DirectX), for .normal.png images
heightScale How deep relief worked out from a height map is: 1 (the default) is the 4 texels of the height range
heightEdges wrap (the default) for block faces, which tile; clamp for item and shape textures, which do not

Anything else (website, more dependencies) is passed through to modinfo.json. Put a modicon.png beside pack.json and it goes in too.

How bright a glowing texture is, and its colour, goes in a .material.json beside its images:

textures/game/item/resource/temporalgear.material.json
{ "emissiveNits": 1200 }
Key Meaning
emissiveNits How bright the brightest texels are (emissive 255), in real units: candela per square metre. For scale, in the path tracer: a torch flame 10,000; a candle 8,000; lava 3,000; the temporal gear 1,200; a rock salt lamp 200; a glowworm 48. A screen or a small indicator light is about 100 to 500
emissiveColor [r, g, b], the light’s colour in place of the texels’ own. Linear Rec.2020, any scale; it may be more saturated than an ordinary picture can show
emissiveKelvin A colour temperature in place of emissiveColor: 1,900 a candle, 2,700 a warm bulb, 6,500 daylight
roughness, metalness 0 to 1, the whole texture at once, for a texture with no roughness, metallic or _m image
ior Glass only: its refractive index, 1.0 to 3.0. Without it, 1.5. Diamond is 2.42, sapphire 1.77, flint glass 1.62, crown glass 1.52, fused quartz 1.46, ice 1.31
solid Glass only. true: a solid body, bending light in and out as a lens does. false: a thin pane, whatever the players’ Solid glass setting says. Leave it out to let the setting decide
emissiveFlicker { "amplitude": 0.15, "hz": 8 }: the light swings about its brightness by up to the amplitude (0 to 1), as smooth noise at about the rate (up to 30 Hz), the whole block together. A torch is 0.15 at 8, a candle 0.08 at 5, a failing tube about 0.45 at 1.5
absorption Glass only: { "color": [r, g, b], "distance": metres }, the colour that gets through that far, linear Rec.2020, 0 to 1 each. On a solid it deepens with thickness: a block of [0.4, 0.5, 0.92] after 0.25 is a deep blue, a chiselled sliver of it pale. On a thin pane color is what the whole pane passes. It takes the place of the texture’s tint, for the whole texture
diffuseTransmission Glass only, 0 to 1: for milky, scattering glass and stone (a texel whose _m carries translucency), the share of the light it scatters that goes on through it, so it glows with a lamp or the sun behind it. Opal and milk glass about 0.5, alabaster 0.4. Leave it out for ice, which is lit from the front only

Without a colour, each glowing texel glows in its own colour: paint an RGB strip and it lights the room in rainbow. Glowing surfaces light their surroundings, so a bright one lights a whole room. The block’s lightHsv still sets the game’s light level, which is what plants and creatures go by, so keep the two roughly in step.

For many textures at once, put rule files in rules/<domain>/:

rules/game/metals.json
{
"contract": "1.12",
"materials": {
"block/metal/sheet-plain/**": { "roughness": 0.3, "metalness": 1 },
"block/metal/sheet-plain/gold*": { "roughness": 0.15 }
}
}

A key is a texture path, without .png and without the domain (the folder the file is in gives that). * matches within one folder name, ** across folders. Every matching entry applies, the least specific first, so a family-wide value can be refined by a more specific one.

Not read yet, but kept for when the path tracer learns them: class, emissiveRefLevel. The tool and the game mention them as notes, not as problems.

vspt-pack build checks what it built, and stops zip and install when anything is wrong. It looks for:

It says What to do
An image of another size than its texture Resize it, nearest neighbour, to the texture’s size
No channel in a file name, or an unknown one Name it <texture>.<channel>.png, with a channel from the table above
A normal map that does not look like one A height map named .normal.png? Rename it .height.png
Texels tilting more than 60 degrees Soften the relief: the path tracer flattens them to 60
Part-metal texels Use 0 or 255 for metallic
A glow with no emissiveNits Add a .material.json saying how bright
A map whose texture does not exist The path or the domain folder is misspelt

Give it the game’s folder with --game <install folder> if it cannot find the game by itself (it looks at %VINTAGE_STORY% and the usual install folder). With the game, it sizes and checks maps for vanilla’s textures against the real ones.

A pack for another mod’s textures (a texture pack’s, say) names that mod with --with <folder or zip>, once per mod: vspt-pack build mypack --with D:\mods\conquest. Its textures then count as present and your maps are sized and checked against them; without it the tool can only see the game’s own textures and reports your maps as unused. Where such a mod replaces a vanilla texture (a 64x64 repaint of a 32x32 plant, say), its picture is the one the tool sizes and checks against, as it is the one the game loads; with several --with mods, the last one given wins.

vspt-pack check <mod folder or zip> checks any built pack, yours or someone else’s.

Packs load in the game’s mod load order, and a later one wins, image by image: your normal map can replace the path tracer’s own while its roughness stays.

One rule on top keeps texture packs safe: a map applies only to the picture it was made for. If a texture pack repaints a texture, the maps made for the old picture (the path tracer’s own, and any pack’s that loads before the repaint) are dropped for that texture, so they are never drawn over art they do not fit. Maps from the texture pack itself, or from a pack that loads after it, apply. An identical copy of a texture is not a repaint.

So a texture pack with PBR should ship its maps with its art, in the same mod. A PBR add-on for somebody else’s texture pack should list that pack in dependencies, which makes it load after.

What vspt-pack writes, for anyone making maps another way:

modinfo.json
assets/<domain>/materials/textures/<path>_n.png normal map: RGB normal (Y up), alpha height
assets/<domain>/materials/textures/<path>_m.png surface map: R roughness, G metallic, B emissive, A opacity
assets/<domain>/materials/textures/<path>.png path traced base colour (from .tracedalbedo)
assets/<domain>/materials/*.json rules
assets/<domain>/textures/<path>.png replaced texture (from .albedo)

The surface map’s alpha is opacity to light passing through: 255 none passes (the default), 0 all of it. It is upside down from .translucency.png so that an image editor’s usual opaque alpha means “solid”.

The maps never go in textures/ beside the art. The game treats every picture there as a texture, and block types that pick their textures by wildcard (vanilla’s bricks do) would draw a normal map as a block.

vspt-pack split <assets folder> game:block/soil/fertmedium --into mypack

splits a built pack’s _n and _m maps into channel images in your project, for editing in any paint program. The sample was made this way from the path tracer’s own maps.