Veduta

Skeletons and Animation

A 3D character that walks, waves and swings a sword is one model with a skeleton: bones at its joints, parts that follow the bones, and animations that turn the bones over time. The engine poses the skeleton every tick and draws each part with its bone's matrix, so an animation costs no extra triangles: a character of a few hundred triangles animates freely on the console.

Free polygons

Primitive parts (boxes, cylinders, spheres, lathes) cover most shapes. For anything else, write the polygons yourself with a mesh part: vertices are the corners, faces lists each polygon's corners counter-clockwise seen from outside.

{
  "veduta": "model/1",
  "pivot": "bottom-center",
  "parts": [
    { "shape": "mesh", "material": "stone",
      "vertices": [[-0.5, 0, 0.5], [0.5, 0, 0.5], [0.5, 0, -0.5], [-0.5, 0, -0.5], [0, 0.8, 0]],
      "faces": [[3, 2, 1, 0], [0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4]] }
  ]
}

A face may have any number of corners and may be concave (a star, an L); the compiler cuts it into triangles. If a face shows from the wrong side, reverse its corners.

Bones

bones lists the joints, parents first. position is where the joint is in the model as you built it (the rest pose); each part names the bone it follows.

{
  "veduta": "model/1",
  "pivot": "bottom-center",
  "bones": [
    { "name": "hips", "position": [0, 0.9, 0] },
    { "name": "chest", "parent": "hips", "position": [0, 1.1, 0] },
    { "name": "head", "parent": "chest", "position": [0, 1.5, 0] },
    { "name": "arm_l", "parent": "chest", "position": [0.28, 1.42, 0] },
    { "name": "arm_r", "parent": "chest", "position": [-0.28, 1.42, 0] },
    { "name": "leg_l", "parent": "hips", "position": [0.12, 0.9, 0] },
    { "name": "leg_r", "parent": "hips", "position": [-0.12, 0.9, 0] }
  ],
  "parts": [
    { "shape": "box", "size": [0.44, 0.25, 0.24], "position": [0, 0.98, 0], "material": "pants", "bone": "hips" },
    { "shape": "box", "size": [0.5, 0.4, 0.25], "position": [0, 1.28, 0], "material": "shirt", "bone": "chest" },
    { "shape": "sphere", "radius": 0.17, "segments": 10, "rings": 6, "position": [0, 1.68, 0], "material": "skin", "bone": "head" },
    { "shape": "box", "size": [0.12, 0.55, 0.12], "position": [0.37, 1.18, 0], "material": "skin", "bone": "arm_l" },
    { "shape": "mirror", "axis": "x", "of": 3, "bone": "arm_r" },
    { "shape": "box", "size": [0.15, 0.85, 0.15], "position": [0.12, 0.45, 0], "material": "pants", "bone": "leg_l" },
    { "shape": "mirror", "axis": "x", "of": 5, "bone": "leg_r" }
  ],
  "animations": {
    "idle": { "loop": true, "length": 2, "tracks": {
      "chest": [ { "t": 0, "rotation_deg": [0, 0, 0], "ease": "smooth" }, { "t": 1, "rotation_deg": [3, 0, 0], "ease": "smooth" } ] } },
    "walk": { "loop": true, "length": 0.8, "tracks": {
      "leg_l": [ { "t": 0, "rotation_deg": [30, 0, 0] }, { "t": 0.4, "rotation_deg": [-30, 0, 0] } ],
      "leg_r": [ { "t": 0, "rotation_deg": [-30, 0, 0] }, { "t": 0.4, "rotation_deg": [30, 0, 0] } ],
      "arm_l": [ { "t": 0, "rotation_deg": [-25, 0, 0] }, { "t": 0.4, "rotation_deg": [25, 0, 0] } ],
      "arm_r": [ { "t": 0, "rotation_deg": [25, 0, 0] }, { "t": 0.4, "rotation_deg": [-25, 0, 0] } ] } },
    "wave": { "next": "idle", "tracks": {
      "arm_l": [ { "t": 0, "rotation_deg": [0, 0, 0] }, { "t": 0.3, "rotation_deg": [0, 0, 150], "ease": "smooth" },
                 { "t": 0.9, "rotation_deg": [0, 0, 150] }, { "t": 1.2, "rotation_deg": [0, 0, 0] } ] } }
  }
}

Tips:

Animations

Each animation has tracks: per bone, a list of keys by time. A key sets rotation_deg (about the joint), position (an offset of the joint) or scale, or several; the engine moves between keys (ease: linear, step or smooth).

Playing them

local hero = scene.find("hero")
hero.anim_fade = 0.15          -- changing anim blends over 0.15 s

function game.update()
  if input.pressed("a") then
    hero:play("wave", 0.1)      -- start over, blending in 0.1 s
  elseif input.dpad() ~= 0 then
    hero.anim = "walk"          -- the same name again changes nothing
  elseif hero.anim ~= "wave" or hero.anim_done then
    hero.anim = "idle"
  end
end

A scene can start one too: "anim": "idle" on the entity.

Aiming bones and holding things

e:pose(bone, rx, ry, rz) turns a bone on top of its animation until you change it: a head that follows the player, an arm that aims. e:pose(bone) takes it back.

local x, _, z = player:position()
local hx, _, hz = guard:bone_position("head")
guard:pose("head", 0, math.deg(math.atan(x - hx, z - hz)), 0)

An entity can ride a bone: give it the character as parent and the bone as bone. Its position is then relative to the joint and it turns with the bone.

{ "name": "sword", "kind": "static", "model": "sword", "parent": "hero", "bone": "arm_r", "position": [0, -0.5, 0] }

In Lua: scene.spawn{model = "sword", parent = hero, bone = "arm_r"}.

Generating characters

mesh.model(name, t) compiles a table written like a model file, bones and animations included, into a model the game can use at once: random creatures, procedural buildings.

local legs, bones, tracks = {}, {}, {}
for i = 1, 4 do
  local x = (i <= 2) and -0.3 or 0.3
  local z = (i % 2 == 0) and -0.3 or 0.3
  local name = "leg" .. i
  bones[i] = {name = name, position = {x, 0.4, z}}
  legs[i] = {shape = "box", size = {0.08, 0.4, 0.08}, position = {x, 0.2, z}, bone = name}
  local swing = (i == 1 or i == 4) and 30 or -30
  tracks[name] = {{t = 0, rotation_deg = {swing, 0, 0}}, {t = 0.25, rotation_deg = {-swing, 0, 0}}}
end
local parts = {{shape = "sphere", radius = 0.4, segments = 8, rings = 5, position = {0, 0.6, 0}}}
for _, leg in ipairs(legs) do parts[#parts + 1] = leg end
mesh.model("game:beetle", {bones = bones, parts = parts,
  animations = {walk = {loop = true, length = 0.5, tracks = tracks}}})
scene.spawn{name = "beetle", model = "game:beetle", anim = "walk"}

Checking them

veduta inspect model hero --sheets poses (or the MCP inspect tool with sheets: ["poses"]) draws every animation at four moments, and the report lists the bones, the parts each one moves and the animations. render --tick N shows the pose the game has at tick N; scenarios can check anim and anim_done through state.

Collisions use the rest pose (a swinging arm does not change what the entity touches); give a character a hitbox for its body.

Veduta (Lua games). This wiki is built from wiki/ in the engine's repository, where its examples are tested, and published with every engine release on https://veduta.roomve.it/docs/.