Adding content: 3D models
This guide shows how to turn an OBJ file (a common 3D model format) into the model of an item or a piece of furniture. It is the model part of Content, which covers items, recipes, shops and loot. Read that page first, because the snippets here use the plugin set up there. The model code is marked experimental, so names and options may change (registry).
Files you ship
Put these files in your mod’s folder under BepInEx\plugins\, next to your DLL.
- The
.objfile. This is the shape. - The
.mtlfile, in the same folder as the OBJ. It lists the materials (colours and textures). The framework reads the file named on themtllibline of the OBJ, from the OBJ’s folder. - The texture pictures named by
map_Kdlines in the.mtl. Each texture must be inside the OBJ’s folder (registry). - Optionally, one PNG to use for every part of the model. You pass its full path as
texturePath. Passnullto use the textures from the.mtl.
Your code finds its folder from Info.Location, which is the path of your DLL:
string folder = Path.GetDirectoryName(Info.Location); // the folder your DLL is in
Both objPath and texturePath must be full paths. A short path does not throw: it gives you a model that is not ready, with an error text.
Loading a model
ModelRegistry.LoadObj reads the OBJ file straight away and gives you a ModelHandle. The handle stands for the loaded model. You hand it to an item or furniture definition. The code goes in Awake of your plugin.
string folder = Path.GetDirectoryName(Info.Location);
ModelHandle chair = ModelRegistry.LoadObj(ModGuid, "chair-oak", Path.Combine(folder, "chair-oak.obj"), null, new ModelOptions()); // mod id, key, OBJ path, texture path (or null), options
if (!chair.IsReady) Log.Error("chair-oak: " + chair.Error); // IsReady is false when the file could not be used
- The mod id and the key are checked. A bad pair throws
ArgumentException. The key is 1 to 64 characters of letters, digits,.,_or-. - A bad file does not throw. The handle has
IsReadyset tofalseandErrorholds the reason. The reason is also written to the log. - Loading the same mod id and key twice gives you the first handle, if that handle is ready. A handle that failed is read again.
ModelRegistry.Get(modGuid, key)gives you a handle you loaded earlier, ornull.
ModelOptions
Pass a ModelOptions to LoadObj to change how the model is sized, turned and given a collider (the invisible shape the game uses for bumping into things). These are the fields and their defaults.
| Field | Default | What it does |
|---|---|---|
Scale | 1f | Multiplies the size after the fit. It must be a number above 0 and at most 1000. |
FitToTemplate | true | Resizes the model so its longest side matches the longest side of the item you copied. A 27 cm model on a 10 cm template becomes 10 cm. false keeps the model’s own size (registry). |
Pivot | ModelPivot.Keep | Where the model’s centre point is. Center moves the middle of the model to the origin. BottomCenter moves the middle of the sides and the lowest point to the origin. Keep moves nothing. |
EulerOffset | zero | Turns the model, in degrees, before it is resized. |
PositionOffset | zero | Moves the model after the resize and the pivot. |
FlipX | true | Mirrors left and right while reading the file, and turns the triangles round so the faces still point outward. |
Collider | ModelCollider.Box | Box fits a box to the model. ConvexMesh uses the model’s own simple shape. A convex shape over 255 triangles becomes a box, with a warning. Template adds no collider of its own. |
The resize uses the model’s size from before EulerOffset is applied. So if you turn a model that is not a cube by 90 degrees, its longest side may not match the template’s. Check the size in the game.
The framework changes the model in this order: turn it by EulerOffset, resize it by FitToTemplate and Scale, then move it by Pivot and PositionOffset. PositionOffset is in the item’s own units, after the resize.
This example turns a model round by 180 degrees:
string folder = Path.GetDirectoryName(Info.Location);
ModelHandle rack = ModelRegistry.LoadObj(ModGuid, "rack-wire", Path.Combine(folder, "rack-wire.obj"), null, new ModelOptions { EulerOffset = new Vector3(0f, 180f, 0f) }); // turn 180 degrees around the up axis
The front is +Z
Make your model’s front face +Z. +Z is the direction Unity calls forward (furniture). If your export faces another way, turn it with EulerOffset. The front only changes how the piece looks. Where people sit comes from the template, not from your model (furniture).
Models for furniture
A furniture model is handled differently from an item model in four ways (furniture).
- The model keeps its real size times
Scale.FitToTemplateis ignored, so the model is never resized to match the template. - The bottom middle of the model is placed on the bottom middle of the template’s visible part.
PivotandPositionOffsethave no effect on furniture.EulerOffsetstill turns the model. - The template’s own look is switched off. Your model replaces it.
- Technical: every renderer under the template’s child 0 is disabled.
ModelOptions.Collideris ignored. Each part of the template that was solid gets one box fitted to your model (furniture).
This example loads two models and gives one of them to a furniture piece. FurnitureRegistry.Define is explained in Content.
string folder = Path.GetDirectoryName(Info.Location);
ModelHandle chair = ModelRegistry.LoadObj(ModGuid, "chair-oak", Path.Combine(folder, "chair-oak.obj"), null, new ModelOptions());
if (!chair.IsReady) Log.Error("chair-oak: " + chair.Error);
ModelHandle rack = ModelRegistry.LoadObj(ModGuid, "rack-wire", Path.Combine(folder, "rack-wire.obj"), null, new ModelOptions { EulerOffset = new Vector3(0f, 180f, 0f) });
FurnitureRegistry.Define(ModGuid, "jerky-rack-wire", new FurnitureDefinition
{
TemplateName = "Beer Crate", // the game's furniture to copy
Title = "Wire Jerky Rack", // the name the player sees
Model = rack // the model from LoadObj
});
If a model is not ready when the piece is first built, you get one error in the log. The template’s look stays, and the piece is still built and sold (furniture).
Replacing the world model of an existing item
An item has a model for when it is in the world (dropped, on a shelf, stored) and one for when it is held. ModelRegistry.OverrideVanilla(itemId, world, held) replaces the world model. It works for items from the game and for items from mods. Nothing is saved, so removing your mod brings the old look back (registry).
string folder = Path.GetDirectoryName(Info.Location);
ModelHandle tin = ModelRegistry.LoadObj(ModGuid, "tin", Path.Combine(folder, "tin.obj"), null, new ModelOptions());
if (!ModelRegistry.OverrideVanilla(19990, tin, null)) Log.Error("tin model was not applied"); // item id, world model, held model (must be null)
- It returns
falsewhen it could not apply the model. That means the model changes are not installed, the handle is not ready, or the item list is loaded and has no item with that id and a look. - It throws
ArgumentNullExceptionifworldisnull. heldmust benull. A held model (the one in the player’s hand) throwsArgumentException. Set one withItemRegistry.SetHeldModelinstead (registry).
Limits
The loader refuses files that are too big. A file over a limit gives a model that is not ready, or the part over the limit is left out.
- An OBJ file over 64 MB is refused.
- An OBJ with more than 2,000,000 vertices (corner points) is refused.
- A model can have up to 8 parts, one per material. Extra material groups (
usemtllines) are merged into the last part. - An MTL file over 1 MB is ignored, with a warning.
- A texture over 2048 pixels on a side is refused, and its material keeps its plain colour.
- A face with more than 4 corners is cut into triangles, with a warning (registry).
Common mistakes
- Short paths.
objPathandtexturePathmust be full paths. Build them fromPath.GetDirectoryName(Info.Location). - Not checking
IsReady. A model that is not ready leaves the template’s look, so the item just looks wrong with no other sign. CheckIsReadyand logError. - A mirrored model.
FlipXis on by default. If the model looks mirrored in the game, setFlipX = false. - A handle from somewhere else.
ItemDefinition.ModelandFurnitureDefinition.Modelaccept only a handle fromLoadObj. Anything else throwsArgumentException. - A held model.
OverrideVanillawith a held model that is notnullthrows. Passnull. - A collider on furniture.
ModelOptions.Colliderdoes nothing for furniture. The framework fits the collider to the model. - Reusing a key. A second
LoadObjwith the same mod id and key gives you the first handle when it is ready. Give each model its own key.
Where to go next
- Content: items, recipes, icons, furniture, shops, loot and mail.
- Gameplay and economy: the player, survival, money and jobs.
- registry: the model and item reference.
- furniture: the furniture reference, including the model rules.