Veduta

Model format — model/1

A model is a mesh described as a list of primitive parts (boxes, cylinders, spheres, planes, extrusions, lathed profiles and mirror copies). The compiler turns the parts into one triangle mesh with normals, UVs, one material slot per part and a pivot. Parts are joined by concatenation only (no boolean operations): overlapping parts simply overlap.

File: assets/models/<name>.vmodel. The model name is the file name without .vmodel; it must be 1–64 characters of a-z, 0-9, _, -, starting with a letter or digit. A folder under assets/models/ works too (assets/models/enemies/bat.vmodel): the name is still the file name, so it must be unique across the folders.

Units and axes: lengths are meters, angles are degrees. Coordinates are right-handed, +Y up, +X right, +Z towards the viewer of a camera looking down −Z.

Minimal example

{
  "veduta": "model/1",
  "parts": [
    { "shape": "box", "size": [1, 1, 1], "position": [0, 0.5, 0], "material": "crate_wood" }
  ]
}

Top-level fields

FieldTypeDefaultMeaning
vedutastringrequiredMust be "model/1".
namestringfile nameOptional. When present it must equal the file name's model name.
unitsstring"m"Only "m" (meters) exists.
pivotstring"origin"origin, center or bottom-center (see Pivot).
smooth_angle_degnumber30In [0, 180]. Faces meeting at an angle ≤ this are shaded smoothly (see Normals).
symmetrystring"""" (none), x, y or z: asks inspect to check that the model is mirror-symmetric across the plane perpendicular to that axis (issue MESH_ASYMMETRIC). It does not change the geometry.
triangle_budgetinteger20000In [1, 1000000]. inspect reports MESH_TRIANGLE_BUDGET above it. The compiler never refuses a model for exceeding it.
lodarray[]Up to 4 levels of detail, nearest first (see Levels of detail). Since v1.3.0.
draw_distancenumbernoneIn (0, 100000] meters, farther than the last lod level: beyond it the model is not drawn at all. Since v1.3.0.
partsarrayrequiredAt least one part object (see below).

Parts

Every part is an object with a shape field. Each shape accepts only its own fields plus the common fields; **any other field is an error, even when it is null, 0 or false**. Part indices (used by mirror and in error messages and reports) start at 0.

Common fields (every shape except mirror)

FieldTypeDefaultMeaning
position[x, y, z][0, 0, 0]Translation in meters.
rotation_deg[x, y, z][0, 0, 0]Euler angles in degrees, applied Z first, then X, then Y (R = Ry·Rx·Rz), the same convention as scene entities.
scale[x, y, z][1, 1, 1]Per-axis scale; every component must be non-zero. Negative components mirror the part; the compiler keeps its faces pointing outwards.
materialstringnoneMaterial name (assets/materials/<name>.vmat). A part without a material is drawn with the entity's material (or the default white material).
uvstringper shapebox, planar, cylindrical or spherical (see UV mapping). Defaults: box → box, plane → planar, cylinder → cylindrical, sphere → spherical, extrude → box, lathe → cylindrical.
flip_normalsbooleanfalseReverses the winding and the normals so the part faces inwards (for example the inside of a room or a sky dome).

A part is built in its local space (the shape centered at the local origin as described below), then scaled, rotated, and translated: p' = R·(scale ⊙ p) + position.

box

FieldTypeDefaultMeaning
size[x, y, z]requiredFull extents in meters, each > 0.

A box centered at the origin spanning ±size/2. 12 triangles, 24 vertices (4 per face), always sharp edges unless smooth_angle_deg ≥ 90.

cylinder

FieldTypeDefaultMeaning
radiusnumberrequired> 0.
heightnumberrequired> 0.
segmentsinteger163–256 sides around the Y axis.

Axis along Y from y = −height/2 to +height/2, closed by flat caps. The first side edge is at +Z. 4 × segments triangles. At the default 30° smoothing a cylinder with 12 or more segments has a smooth side (adjacent sides meet at 360°/segments); caps stay sharp.

sphere

FieldTypeDefaultMeaning
radiusnumberrequired> 0.
segmentsinteger163–256 divisions around the Y axis.
ringsinteger82–128 latitude bands from pole to pole.

A UV sphere centered at the origin with poles at (0, ±radius, 0). 2 × segments × (rings − 1) triangles (triangle fans at the poles, quads elsewhere). Smooth when 360°/segments and 180°/rings are both ≤ smooth_angle_deg (true for the defaults).

plane

FieldTypeDefaultMeaning
size[x, z]requiredExtents along X and Z, each > 0.

A rectangle in the XZ plane (y = 0) centered at the origin, facing +Y. It is single-sided: with the default back-face culling it is invisible from below. Use a material with "cull": "none" or a second, flipped plane for two sides. 2 triangles.

extrude

FieldTypeDefaultMeaning
profile[[x, y], …]requiredA polygon in the XY plane, 3–1024 points.
depthnumberrequired> 0.

The profile is extruded along Z from z = −depth/2 to z = +depth/2, with a flat cap at each end (the front cap faces +Z) and one rectangular wall per profile edge. Profile coordinates are used as given (not re-centered). Rules for the profile:

Caps are triangulated by ear clipping. 2 × (points − 2) + 2 × points triangles.

lathe

FieldTypeDefaultMeaning
profile[[r, y], …]required2–1024 points; r ≥ 0 is the distance from the Y axis, y the height.
segmentsinteger163–256 divisions around the Y axis.

The profile is revolved around the Y axis (the first division is at +Z). There are no implicit caps: the profile is the whole surface. To make a closed solid, start and end the profile on the axis (r = 0), for example [[0, 0], [0.5, 0], [0.4, 1], [0, 1.2]], or end it on its first point to make a ring ([[1, 0], [1.5, 0], [1.5, 1], [1, 1], [1, 0]]).

mirror

FieldTypeDefaultMeaning
axisstringrequiredx, y or z.
ofintegerrequiredIndex of an earlier part (any shape, including another mirror).
materialstringsource'sOverrides the material of the copy.

Copies the final geometry of part of (after its own scale, rotation and translation) and reflects it across the model-space plane through the origin perpendicular to axis ("x" reflects across the plane x = 0, before the pivot is applied). The winding is reversed so the copy faces outwards like its source. UVs are copied, so a texture appears mirrored. The copy inherits the source's uv mode and flip_normals (a copy of a flipped part is flipped too). No other fields are allowed: position, rotation_deg, scale, uv and flip_normals are errors on a mirror. To mirror a part in place, build it on one side of the plane and mirror it: both halves meet exactly.

Levels of detail

Far away a model covers a few pixels, so it can be drawn with fewer triangles, or not at all. Each lod level is an object:

FieldTypeDefaultMeaning
distancenumberrequiredMeters from which the level is drawn, in (0, 100000], each level farther than the one before.
modelstringnoneAnother model drawn at this level (its base mesh, with its own materials). Without it the level is this model rebuilt with fewer segments.

A level without model number k (1 for the first level) rebuilds every cylinder, sphere and lathe with segments halved k times and a sphere's rings halved k times, rounding to the nearest integer and never below 3 segments and 2 rings; boxes, planes and extrusions keep their triangles. The level has the same parts, materials and pivot offset as the model. Example: segments: 16 draws 16, then 8, 4, 3 segments.

The distance a level is chosen by is measured every frame, per entity, from the camera to the nearest point of the entity's drawn bounds, as seen through a 60° lens: a perspective camera's distance is scaled by tan(fov/2) / tan(30°), so zooming in (a smaller fov_deg) keeps detail farther away; an orthographic camera counts every entity at 0.866 × size, the distance at which a 60° lens sees size meters, so zooming out drops detail everywhere at once. draw_distance uses the same measure. A world's ground and flora use the same rules (world topic).

{
  "veduta": "model/1",
  "pivot": "bottom-center",
  "lod": [ { "distance": 15 }, { "distance": 35, "model": "tree_far" } ],
  "draw_distance": 80,
  "parts": [
    { "shape": "cylinder", "radius": 0.15, "height": 1.2, "segments": 8, "position": [0, 0.6, 0], "material": "bark" },
    { "shape": "sphere", "radius": 0.7, "segments": 12, "rings": 6, "position": [0, 1.6, 0], "material": "leaf" }
  ]
}

Beyond 15 m the trunk has 4 segments and the crown 6 segments and 3 rings (40 triangles instead of 152), beyond 35 m it is tree_far, beyond 80 m it is not drawn. inspect model reports the triangles of every level (lod_triangles) and a level that saves nothing (MESH_LOD_NO_GAIN).

Winding and orientation

Every triangle is counter-clockwise seen from the side it faces: its outward normal is cross(b − a, c − a). All closed shapes (box, cylinder, sphere, extrude, closed lathe) are watertight and face outwards, whatever the sign of scale, unless flip_normals is set.

Normals

Normals are computed per part from its final triangles; parts never smooth into each other. Around each vertex position, triangles that share an edge there and whose face normals differ by at most smooth_angle_deg (+0.001° tolerance) form a smoothing group (groups chain through such edges). A corner's normal is the average of its group's face normals, each weighted by the triangle's angle at that corner. So with the default 30°: a 16×8 sphere and a cylinder side with ≥ 12 segments are smooth, box edges and cylinder caps are sharp. smooth_angle_deg: 0 gives flat shading; 180 smooths everything except faces that fold back onto each other. Vertices are shared within a part only when position, normal and UV are all identical; otherwise they are split.

UV mapping

UVs are in meters, computed from the part's geometry after scale but before rotation and translation: 1 UV unit = 1 m, so a tiling texture repeats every meter, stays attached to the part when it moves or turns, and scaling a part covers more texture instead of stretching it. v grows downwards (like image rows), so textures appear upright on vertical faces.

Notation: p = (x, y, z) a corner in that scaled local space; min, max the part's bounding box in the same space; θ = atan2(x, z), the angle around +Y (0 at +Z, growing towards +X, ±180° at −Z); ρ = √(x² + z²), the distance from the Y axis; r = |p|, the distance from the part origin; φ = acos(y / r), the angle from +Y.

**box** — each triangle is projected along the axis of its normal's largest component (ties prefer Y, then Z, then X):

Face normaluv
+Xmax.z − zmax.y − y
−Xz − min.zmax.y − y
+Yx − min.xz − min.z
−Yx − min.xmax.z − z
+Zx − min.xmax.y − y
−Zmax.x − xmax.y − y

Every face's top-left corner, seen from outside, is at (0, 0) and u grows to the right (top faces are seen from above with −Z up, bottom faces from below with +Z up). Example: a box of size [2, 1, 1] has u in [0, 2] and v in [0, 1] on its front (+Z) face, (0, 0) at its top-left corner (−1, 0.5, 0.5).

**planar** — the whole part is projected along the axis of its smallest bounding-box extent (ties prefer Y, then Z, then X) with the +Y, +Z or +X row of the table above. A plane therefore uses u = x − min.x, v = z − min.z.

**cylindrical** — triangles whose normal is mostly vertical (largest component Y, e.g. caps) use the ±Y rows of the box table; all others use u = (θ + 180°)·ρ with θ in radians, v = max.y − y. On a cylinder of radius R, u runs from 0 at the back (−Z) around to 2πR, and is πR at the front (+Z).

**spherical** — u = (θ + 180°)·r, v = φ·r (θ, φ in radians). On a sphere of radius R, v is 0 at the top pole and πR at the bottom pole, and u runs from 0 to 2πR.

For cylindrical and spherical, θ is unwrapped per triangle so no triangle straddles the seam, which lies exactly at −Z; corners on the Y axis (poles, cone tips) take the angle of their triangle's center. A texture repeating every meter only matches across the seam when the circumference is a whole number of meters, so keep the −Z side at the back of the model.

Pivot

After all parts are built the compiler takes the bounding box of every vertex and moves the whole mesh so that the pivot point lands on the origin:

pivotPivot point
origin(0, 0, 0): the mesh is not moved.
centerthe bounding box center.
bottom-center(center x, min y, center z): the model stands on y = 0, centered on the Y axis.

The applied translation (minus the pivot point) is reported as pivot_offset. Mirror planes are evaluated before the pivot is applied.

What the compiler produces

Compilation is deterministic: the same source always produces the same bytes.

Errors

Decoding is strict. Unknown fields (typos), wrong JSON types and duplicate keys are errors. Validation then reports every problem at once, each with the file, line, column and JSON path of the offending value. For example, crate.vmodel containing

{
  "veduta": "model/1",
  "parts": [
    { "shape": "box", "size": [1, 1, 1], "radius": 0.5 },
    { "shape": "cylinder", "radius": 0.2, "height": 1, "segments": 2 },
    { "shape": "mirror", "axis": "x", "of": 7 }
  ]
}

reports

crate.vmodel:4:52: parts[0].radius: not used by shape box
crate.vmodel:5:68: parts[1].segments: 2 out of range [3, 256]
crate.vmodel:6:45: parts[2].of: 7 is not the index of an earlier part (want 0..1)

Checks: required fields present; numbers finite; sizes, radii, heights and depths > 0; counts in range (an explicit 0 is out of range, not "default"); scale components non-zero; enum values from the lists above; material names valid; fields that the shape does not use absent; profiles valid as described per shape; of refers to an earlier part; name, when present, equal to the file name.

Limits

ItemRange
segments (cylinder, sphere, lathe)3–256, default 16
rings (sphere)2–128, default 8
profile points (extrude, lathe)extrude 3–1024, lathe 2–1024
smooth_angle_deg0–180, default 30
triangle_budget1–1000000, default 20000
lod levels0–4; distance and draw_distance in (0, 100000] m

Full example

The crate of the specification, with a material on the box only:

{
  "veduta": "model/1",
  "name": "crate",
  "units": "m",
  "pivot": "bottom-center",
  "smooth_angle_deg": 30,
  "parts": [
    { "shape": "box", "size": [1, 1, 1], "position": [0, 0.5, 0], "material": "crate_wood" },
    { "shape": "cylinder", "radius": 0.1, "height": 1.2, "segments": 12, "position": [0.6, 0.6, 0] },
    { "shape": "sphere", "radius": 0.3, "segments": 16, "rings": 8 },
    { "shape": "plane", "size": [10, 10] },
    { "shape": "extrude", "profile": [[0,0],[1,0],[1,2],[0,2]], "depth": 0.5 },
    { "shape": "lathe", "profile": [[0,0],[0.5,0],[0.4,1],[0,1.2]], "segments": 16 },
    { "shape": "mirror", "axis": "x", "of": 1 }
  ]
}

A symmetric figure built from one side and mirrored:

{
  "veduta": "model/1",
  "name": "robot",
  "pivot": "bottom-center",
  "symmetry": "x",
  "parts": [
    { "shape": "box", "size": [0.5, 0.6, 0.4], "position": [0, 0.3, 0], "material": "body" },
    { "shape": "cylinder", "radius": 0.07, "height": 0.7, "segments": 12,
      "position": [0.45, 0.55, 0], "rotation_deg": [0, 0, -50], "material": "arm" },
    { "shape": "mirror", "axis": "x", "of": 1 },
    { "shape": "sphere", "radius": 0.12, "segments": 12, "rings": 6,
      "position": [0.72, 0.8, 0.1], "material": "hand" },
    { "shape": "mirror", "axis": "x", "of": 3 },
    { "shape": "lathe", "profile": [[0, 0.6], [0.18, 0.6], [0.16, 0.8], [0, 0.85]], "segments": 16,
      "material": "head" }
  ]
}