Veduta

3D, Worlds and Blocks

The same engine draws 3D: a perspective camera, lit models, and worlds far larger than a scene, streamed around the player. The console renders everything in software on its CPU, so keep scenes modest: a few hundred to a thousand triangles on screen (check with veduta bench, see Debugging and Performance).

A 3D scene

A perspective camera, a light, and models with lit materials:

{
  "veduta": "scene/1",
  "camera": { "type": "perspective", "fov_deg": 60, "position": [0, 6, 8], "look_at": [0, 1, 0] },
  "light": { "direction": [-0.4, -1, -0.3], "color": "#fff4e0", "ambient": "#303848" },
  "background": "#87a8c8",
  "entities": [
    { "name": "floor", "kind": "static", "model": "floor", "material": "stone" },
    { "name": "hero", "kind": "walker", "model": "hero", "material": "hero", "tags": ["player"] },
    { "name": "crate", "kind": "static", "model": "crate", "material": "wood", "position": [3, 0, -2] }
  ]
}

Models are built from boxes, cylinders, spheres, extrusions and lathed profiles, see Graphics Assets. A hero, feet at its origin:

{
  "veduta": "model/1",
  "pivot": "bottom-center",
  "parts": [
    { "shape": "box", "size": [0.6, 1.2, 0.4], "position": [0, 0.6, 0] },
    { "shape": "sphere", "radius": 0.3, "position": [0, 1.5, 0] }
  ]
}

Walking on the XZ plane: up on the D-pad goes forward, toward −Z, with a camera behind. The hero turns to face where it walks (for a model whose front faces −Z):

local SPEED = 4

kinds.walker = {
  update = function(e)
    local dx, dy = input.dpad()
    e.x = e.x + dx * SPEED * engine.dt
    e.z = e.z - dy * SPEED * engine.dt
    if dx ~= 0 or dy ~= 0 then
      -- face the direction of travel: yaw 0 looks toward -z, -90 toward +x
      e:set_rotation(0, math.deg(math.atan(-dx, dy)), 0)
    end
    camera.set{position = {e.x, e.y + 6, e.z + 8}, target = {e.x, e.y + 1, e.z}}
  end,
}

Streamed worlds

A world (assets/worlds/<name>.vworld) is a map generated from a seed: biomes, rolling ground, hills, lakes and seas, vegetation, villages and landmarks. Only the chunks around a focus point exist at a time, so a world can span kilometres.

{
  "veduta": "world/1",
  "seed": 7,
  "camera": { "type": "perspective", "fov_deg": 60, "position": [0, 6, 8], "look_at": [0, 0, 0] },
  "biomes": [ { "name": "meadow", "ground": "grass" } ],
  "terrain": { "relief": 2, "relief_scale": 32 },
  "entities": [
    { "name": "hero", "kind": "walker", "model": "hero", "material": "hero", "tags": ["player"] }
  ]
}

The ground material's texture must be "tiling": true. entities are the persistent ones (the hero), placed relative to the start cell.

From Lua:

FunctionDoes
world.load(name, cx, cz)replaces the scene with the world, starting at cell (cx, cz)
world.name()the loaded world, or nil
world.focus(x, y, z)where the chunks follow; call it every tick
world.height(x, z)the ground's height there
world.water(x, z)the water level there, or nil on dry land

A hero that walks on the ground and stays out of the water:

local SPEED = 4

function game.init()
  world.load("meadow", 0, 0)
end

kinds.walker = {
  update = function(e)
    local dx, dy = input.dpad()
    local x = e.x + dx * SPEED * engine.dt
    local z = e.z - dy * SPEED * engine.dt   -- up on the D-pad walks north, toward -z
    if world.water(x, z) == nil then
      e:set_position(x, world.height(x, z), z)
    end
    world.focus(e.x, e.y, e.z)
    camera.set{position = {e.x, e.y + 6, e.z + 8}, target = {e.x, e.y + 1, e.z}}
  end,
}

A project can also start straight in a world with "default_world" in veduta.json. Scenarios load one with "world" and "at" instead of "scene", and the trace records a chunk_load event per chunk loaded.

Authoring a world is a loop of editing rules and looking at the result: veduta world map <name> prints its map, veduta world query <name> --cell x,z describes a cell, and veduta world terrain|vegetation|place add hills, lakes, trees and landmarks where the rules allow (veduta help world). The full format is in the world reference.

Block worlds: mesh and volume

For geometry that changes while the game runs (a block world that is dug and built, a maze generated from the seed) scripts build models themselves. The heavy loops run inside the engine, so a whole chunk of blocks is one call.

A volume is a 3D grid of block ids (0 is empty, 1 to 255 are block types). mesh.voxels turns it into a mesh of the faces between a block and empty space, one part per block id with the material you name, and mesh.set registers it as a model entities can use:

local chunk = volume.new(16, 8, 16)

local function rebuild()
  mesh.set("game:chunk", mesh.voxels(chunk, {[1] = "stone", [2] = "grass", [3] = "wood"}))
end

function game.init()
  chunk:fill(0, 0, 0, 15, 2, 15, 1)    -- three layers of stone
  chunk:fill(0, 3, 0, 15, 3, 15, 2)    -- grass on top
  rebuild()
  scene.spawn{name = "chunk", model = "game:chunk"}
end

-- dig or place the block under a cursor
local function set_block(x, y, z, id)
  chunk:set(x, y, z, id)
  rebuild()
end
FunctionDoes
volume.new(x, y, z)an empty grid, at most 4 194 304 blocks
v:get(x, y, z), v:set(x, y, z, id)one block; cells count from 0
v:fill(x1, y1, z1, x2, y2, z2, id)every block of a box
v:size()x, y, z
mesh.voxels(v, materials [, size])the visible faces as a mesh; size is a block's edge in units (default 1)
mesh.new()an empty mesh, to build by hand
m:part([material])what follows is drawn with that material
m:quad(...), m:triangle(...)a face from 4 or 3 corners, counter-clockwise seen from the front
m:box(x, y, z, w, h, d [, faces])a box, optionally only some faces ("+y-y")
m:triangles()the triangle count
mesh.set(name, m)makes m the model name; the name must contain :
mesh.remove(name)forgets it

Models set this way survive scene.load. Setting a model copies the mesh, so change the mesh and call mesh.set again to update it. Split a large world into chunks and rebuild only the chunk that changed: fewer triangles to rebuild, and chunks out of view are not drawn.

Veduta v2 (Lua games, release candidates). 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/.