Veduta

Texture format — texture/1

A texture is an image described as a layer program: a list of layers (solid fills, value noise, stripes, rectangles, circles, gradients, checkerboards and PNG images) that are painted one over the other, each with a blend mode and an opacity. The compiler renders the program into a BGRA8 image with a mip chain. Materials use textures by name.

File: assets/textures/<name>.vtex. The texture name is the file name without .vtex (crate_wood.vtexcrate_wood); it must be 1–64 characters of a-z, 0-9, _, -, starting with a letter or digit. A folder under assets/textures/ works too (assets/textures/ui/icons.vtex): the name is still the file name, so it must be unique across the folders.

Minimal example

{
  "veduta": "texture/1",
  "size": [64, 64],
  "layers": [
    { "type": "solid", "color": "#8a5a2b" }
  ]
}

Top-level fields

FieldTypeDefaultMeaning
vedutastringrequiredMust be "texture/1".
size[width, height]requiredIntegers in pixels, each 1–4096. Non-power-of-two sizes are allowed (inspect texture warns with TEX_NOT_POWER_OF_TWO).
tilingbooleanfalsetrue: the texture is meant to repeat. It is sampled with wrap-around (repeat) addressing, and noise layers wrap so the texture repeats without a seam (see Tiling). false: texture coordinates outside [0, 1] clamp to the edge.
mipmapsbooleantrue, false for a sheettrue: the compiler also produces the mip chain (each level half the size of the previous one, down to 1×1). false: only the full-size image. A sheet of frames (grid or frames) has no mipmaps unless it asks: small levels would mix neighbouring frames.
layersarrayrequired1–64 layer objects, painted in list order: the first layer is at the bottom, each later layer is painted over the result of the previous ones. Optional with frames, where they are painted under every frame.
grid[columns, rows]noneThe image is a sheet of frames: columns × rows frames of equal size, counted left to right, top to bottom, from 0. Each 1–256, dividing size evenly. See Frames and clips.
framesarraynoneFrames painted one by one: 1–256 objects { "layers": [...] }, each size large, painted over the texture's own layers. They are put side by side (grid [n, 1]); n × width must stay within 4096. Not with grid. With tiling, every frame tiles (its noise wraps), so a map can lay it next to itself; the sheet itself does not repeat.
clipsobjectnoneNamed animations of the frames: name → clip (below). Needs grid or frames.
playstringnoneA clip shown wherever the texture is drawn and nothing picks a frame: the tiles of a map, an entity without a clip of its own.
edgeobjectnoneHow the texture spills over lower terrains around it when a map paints with it. See Edges.
autotilebooleanfalsetrue: every frame is 6 × 3 tiles, an island and a lake, and a map picks a cell's tile by its neighbours. See Autotiles. Not with edge.

Coordinates, angles and colors

Layers and compositing

Every layer is an object with a type field. Each type accepts only its own fields plus the common fields below; **any other field is an error, even when it is null, 0, "" or false**. Layer indices (in error messages and in inspect reports) start at 0.

Common fields (every layer type)

FieldTypeDefaultMeaning
typestringrequiredsolid, noise, stripes, rect, circle, gradient, checker or image.
opacitynumber1In [0, 1]. Multiplies the layer's alpha everywhere.
blendstring"normal"normal, multiply, screen or add: how the layer's color combines with what is below it. An empty string also means normal.

How a layer is painted

The canvas starts transparent black (every channel 0). For every pixel, a layer produces a color s (red, green, blue, each in [0, 1]) and an alpha:

a = (alpha of the layer's color at that pixel) × coverage × opacity

coverage is how much of the pixel the layer covers (for example 0.5 on the anti-aliased edge of a circle; the noise value for noise). With d the canvas color and dA the canvas alpha at that pixel before the layer, the blend function B is applied per channel:

blendB(d, s)Effect
normalspaints the color
multiplyd · sdarkens (white changes nothing, black gives black)
screen1 − (1 − d) · (1 − s)lightens (black changes nothing, white gives white)
addmin(1, d + s)lightens by adding, clamped at 1

On an opaque canvas (dA = 1, the usual case once a solid layer is at the bottom):

result color = d + (B(d, s) − d) · a result alpha = 1

In general (W3C source-over compositing with blending), which gives the formula above when dA = 1:

result alpha = a + dA · (1 − a) result color = (a · (1 − dA) · s + a · dA · B(d, s) + (1 − a) · dA · d) / result alpha

So over a transparent part of the canvas (dA = 0) a layer keeps its own color: a half-transparent white circle on nothing is white at 50% alpha, not grey. A layer whose alpha is 0 at a pixel changes nothing there.

The canvas is kept in 32-bit floating point while the layers are painted. At the end every channel is clamped to [0, 1], multiplied by 255 and rounded (halves round up) to 8 bits. The result has straight alpha. Fully transparent pixels store color 0 (black).

Anti-aliasing: stripes, rect and circle take 4 × 4 samples per pixel, at offsets 0.125, 0.375, 0.625 and 0.875 in x and y. Coverage is the fraction of samples inside the shape; a sample exactly on a shape's edge counts as inside. When the samples of one pixel have different colors (a stripe edge), they are averaged with premultiplied alpha: the pixel's alpha is the mean of the sample alphas and its color is the alpha-weighted mean of the sample colors.

Layer types

solid

FieldTypeDefaultMeaning
colorcolorrequiredFills the whole texture.

noise

FieldTypeDefaultMeaning
colorcolorrequiredThe color painted; the noise value multiplies its alpha.
seedinteger0Any 64-bit signed integer. The same seed always gives the same pattern; different seeds give unrelated patterns.
scalenumber8Size of the pattern: the number of noise cells across the texture width, in (0, 4096]. Larger values give smaller features.
octavesinteger11–8 layers of detail. Each octave has twice the cells of the previous one and half its weight.

Value noise: octave o (0, 1, …) is a grid of cells whose corners hold random values in [0, 1) from a hash of (seed, o, corner column, corner row). A pixel's value in one octave is the smooth interpolation of the four corners around its center, with weights t²·(3 − 2t) of its position t inside the cell in each direction. The octaves are summed with weights 1, 1/2, 1/4, … and divided by the sum of the weights, so the noise value n stays in [0, 1). The layer paints color with alpha = color alpha × n × opacity.

Cells across the texture:

Example: "scale": 16, "octaves": 3 on a 256 × 256 texture gives cells of 16, 8 and 4 pixels. A dark multiply noise at low opacity is the usual way to add grain or dirt.

stripes

FieldTypeDefaultMeaning
widthnumberrequiredWidth of one band in pixels, > 0 (need not be an integer).
angle_degnumber0Direction along which the bands follow each other. 0: the color changes along x, so the bands are vertical; 90: horizontal bands.
colors[color, …]required2–64 colors, used in order and repeated.

For a sample point (X, Y), t = X · cos(angle) + Y · sin(angle) is its distance along the direction from the top-left corner. It lies in band k = floor(t / width), which has color colors[k mod n] (n colors; the index is never negative). Band 0 starts exactly at the origin. Colors may be transparent: ["#00000000", "#00000040"] darkens every other band. To draw thin lines between wide bands, repeat a color: "width": 2 with 15 transparent colors followed by one dark color gives a 2-pixel line every 32 pixels.

rect

FieldTypeDefaultMeaning
xy[x, y]requiredTop-left corner in pixels (any finite numbers, may be negative).
size[width, height]requiredSize in pixels, each > 0. The rectangle covers x to x + width and y to y + height.
colorcolorrequiredFill or outline color.
cornernumber0Corner radius in pixels, ≥ 0. Clamped to half the smaller side, so a very large value makes a pill or, for a square, a circle.
outlinenumber0≥ 0. 0 fills the rectangle. A positive value draws only an outline of that width, inside the rectangle: its outer edge is the rectangle's edge, its inner edge is the rectangle inset by outline on every side, with corner radius max(corneroutline, 0). An outline of at least half the smaller side fills the rectangle.

circle

FieldTypeDefaultMeaning
center[x, y]requiredCenter in pixels (any finite numbers).
radiusnumberrequiredRadius in pixels, > 0.
colorcolorrequiredFill or outline color.
outlinenumber0≥ 0. 0 fills the disk. A positive value draws a ring inside the circle, from radius − outline to radius. An outline ≥ radius fills the disk.

gradient

FieldTypeDefaultMeaning
fromcolorrequiredColor at the start.
tocolorrequiredColor at the end.
angle_degnumber0Direction from from to to. 0: left → right; 90: top → bottom; 45: top-left → bottom-right; 180: right → left.

A linear gradient across the whole texture. With direction D = (cos(angle), sin(angle)) and P a pixel center, the position is t = (P · D − t0) / (t1 − t0), where t0 and t1 are the smallest and largest P · D over the centers of the four corner pixels. So the first pixel(s) along the direction are exactly from and the last exactly to (a texture one pixel long in that direction is from). Colors are mixed with premultiplied alpha: alpha = (1 − t) · from.alpha + t · to.alpha and color = ((1 − t) · from.alpha · from.rgb + t · to.alpha · to.rgb) / alpha, so fading from "#ffffff" to "#ffffff00" stays white and fading "#ff000000" to "#0000ff" never shows red. The gradient does not repeat when tiling is true.

checker

FieldTypeDefaultMeaning
cellsintegerrequiredCells per side, 1–4096: cells columns across the width and cells rows across the height (cells are not square when the texture is not).
colors[color, color]requiredExactly 2 colors. The top-left cell has the first.

A pixel takes the color of the cell containing its center: column = floor((x + 0.5) · cells / width), row = floor((y + 0.5) · cells / height), color = colors[(column + row) mod 2]. There is no anti-aliasing; use sizes that are multiples of cells for sharp, equal cells. A tiling checker repeats seamlessly only when cells is even (with an odd count the last and first cells of a row have the same color).

image

FieldTypeDefaultMeaning
pathstringrequiredA PNG file relative to the assets directory, for example "textures/src/logo.png".
fitstring"contain"How the image is placed: contain, cover or stretch (below). An empty string also means contain.
rect[x, y, width, height]the whole imageIntegers in pixels of the PNG: only this part of the image is used, as if it were the whole file. It must lie inside the image. Many textures can take their part of one atlas this way.

path rules: forward slashes only; no leading /, no drive letter or :, no .. segment (the file must be inside the assets directory), no . or empty segments, no trailing /; the name must end in .png (any case). The file must be a PNG image (any color type; 16-bit channels are reduced to 8 bits) of at most 8192 × 8192 pixels. It is read when the texture is compiled; veduta cook recompiles the texture when the PNG changes.

Placement, for a W × H texture and an iw × ih image:

Sampling: image pixels are interpolated bilinearly (image pixel centers at half-integer positions, edges clamped, premultiplied alpha so transparent pixels do not darken their neighbours). When the image is shrunk by a factor f > 1 along an axis, each texture pixel averages ceil(f) evenly spaced bilinear samples along that axis, so large images do not alias. The layer's coverage at a pixel is the fraction of the pixel's area covered by the placed image (letterbox edges are anti-aliased); the image's own alpha multiplies it. The image never repeats, even when tiling is true. An image exactly as large as the texture is copied pixel for pixel (fully transparent pixels become transparent black).

Frames and clips

A texture with frames is a sheet: the frames of an animation side by side in one image. grid cuts the image into equal frames; frames paints each frame with its own layers. A material that uses a sheet shows one frame at a time: the frame an entity picks with frame, the frame of the clip it plays with anim, or else the frame of the texture's play clip.

A walking hero, from a PNG of 4 × 2 frames of 16 × 16 pixels:

{
  "veduta": "texture/1",
  "size": [64, 32],
  "layers": [
    { "type": "image", "path": "textures/src/logo.png", "fit": "stretch" }
  ],
  "grid": [4, 2],
  "clips": {
    "walk": { "frames": [0, 1, 2, 3], "fps": 8 },
    "idle": { "frames": [4], "fps": 1 },
    "hit":  { "frames": [5, 6], "fps": 12, "loop": false, "next": "idle" }
  }
}

Water that ripples by itself, drawn frame by frame (every frame is the blue below, then its own noise; tiling makes each frame seamless, for the cells of a map):

{
  "veduta": "texture/1",
  "size": [16, 16],
  "tiling": true,
  "layers": [
    { "type": "solid", "color": "#2a6fdb" }
  ],
  "frames": [
    { "layers": [ { "type": "noise", "seed": 1, "scale": 4, "color": "#ffffff", "opacity": 0.3 } ] },
    { "layers": [ { "type": "noise", "seed": 2, "scale": 4, "color": "#ffffff", "opacity": 0.3 } ] },
    { "layers": [ { "type": "noise", "seed": 3, "scale": 4, "color": "#ffffff", "opacity": 0.3 } ] }
  ],
  "clips": { "flow": { "frames": [0, 1, 2], "fps": 4 } },
  "play": "flow"
}

A clip:

FieldTypeDefaultMeaning
frames[integer, …]requiredThe frames shown in turn, each from 0 to the number of frames − 1. A frame may repeat.
fpsnumberrequiredFrames per second, above 0 and at most 1000. The clip shows step ⌊t · fps / tick_rate⌋ at t ticks after it started: at 20 ticks a second and 8 fps, a frame lasts 2 or 3 ticks.
loopbooleantruefalse: the clip stops on its last frame.
nextstringnoneOnly with "loop": false: the clip that starts when this one ends (a hit, then idle again).

Clip names follow the asset name rules. Clips run on ticks, so they are deterministic: the same run shows the same frame at the same tick, in the player, in tests and in traces.

Edges

edge makes a texture a terrain with a border when a map (docs/map.md) paints cells with it: next to a lower terrain it spills over the neighbouring cells with a wandering border, so water meets grass without square corners and without drawing transition tiles. Priority decides who spills over whom: the higher draws its border on the lower. A cell next to an empty cell of an upper map layer counts as lower than any terrain, so a path painted on an upper layer gets a border over the ground below.

{
  "veduta": "texture/1",
  "size": [16, 16],
  "layers": [
    { "type": "solid", "color": "#2a6fdb" },
    { "type": "noise", "seed": 4, "scale": 4, "color": "#ffffff", "opacity": 0.2 }
  ],
  "edge": { "priority": 20, "width": 4, "roughness": 0.5, "seed": 1 }
}
FieldTypeDefaultMeaning
priorityintegerrequired1–1000. A terrain draws its border over neighbours of lower priority.
widthnumbera quarter of a frame's smaller sideHow far the border reaches into the neighbouring cell, in pixels of a frame, above 0 and at most half the smaller side.
roughnessnumber0.50–1: 0 is a straight border, 1 wanders by up to the whole width.
seedinteger0Shapes the wandering; the same seed always gives the same border.

A frame's width and height must be even (the border is built from the four quarters of a cell). The border is part of the texture: an animated texture's border moves with it.

Autotiles

"autotile": true makes a texture a terrain drawn with tiles made by hand: its borders where it meets anything else (another terrain, or an empty cell of its layer) are the ones drawn in the image, not generated. Every frame is 6 × 3 square tiles of an even size:

 island (convex)   lake (concave)
 ┌──┬──┬──┐        ┌──┬──┬──┐
 │NW│N │NE│        │nw│n │ne│
 ├──┼──┼──┤        ├──┼──┼──┤
 │W │C │E │        │w │  │e │   the lake's middle is not used
 ├──┼──┼──┤        ├──┼──┼──┤
 │SW│S │SE│        │sw│s │se│
 └──┴──┴──┘        └──┴──┴──┘

The island is the terrain in 3 × 3 cells with nothing around it; the lake is the terrain in the 8 cells around one empty cell, going on outside. Draw them as one picture each: the map cuts them where it needs.

A 16-pixel autotile, animated in 2 frames by a PNG of 192 × 48 pixels:

{
  "veduta": "texture/1",
  "size": [192, 48],
  "layers": [ { "type": "image", "path": "textures/src/cliff.png" } ],
  "grid": [2, 1],
  "clips": { "shine": { "frames": [0, 1], "fps": 2 } },
  "play": "shine",
  "autotile": true
}

A map cell of the terrain looks at its 8 neighbours: the same terrain on its layer counts as itself, and so does a neighbour off the map. Each quarter of the cell falls in one of seven cases from the two sides and the corner it touches: all of them the terrain (C), the sides but not the corner (a concave corner: the lake corner diagonal to the empty cell), neither side (a convex corner: the island's corner), or one side missing, with the corner missing too (a straight border: the island's side) or not (a border that turns in: the lake's side). When the four quarters are those of a tile as it sits in its drawing, the cell draws the whole tile; else each quarter is cut from the tile of its case. So the 17 tiles give every shape: lines one cell wide, lone cells, diagonal touches.

A frame's tiles are its width / 6; the height must be half the width. The VS Code extension has a tile editor that draws autotiles, animated tiles and plain tiles as PNGs.

Tiling

With "tiling": true the compiled texture uses repeat addressing, and noise layers are built to wrap (see noise). The other layers are painted once, as described; they do not wrap around the edges. To keep a tiling texture seamless (inspect texture reports TEX_SEAM otherwise):

Mipmaps

When mipmaps is true, level 0 is the rendered image and every next level halves each dimension (rounding down, never below 1) until 1 × 1. Each pixel of level n + 1 is the rounded average of a 2 × 2 block of level n (an odd last row or column is averaged with itself). The average uses straight colors, so the black of fully transparent pixels darkens the edges of transparent areas in small levels; put an opaque layer at the bottom when the texture has no intended transparency.

What the compiler produces

asset.Texture: Name; Data.Levels (level 0 is size, or the frames side by side; pixels are 32-bit 0xAARRGGBB, straight alpha, rows top to bottom); Data.Wrap (repeat when tiling, else clamp); Tiling; Layers (the number of source layers); Grid (columns and rows of frames, 0 0 for a single image); Clips (by name); Play; Edge; Autotile. Its binary layout in .vda files is in docs/vda.md (chunk TEXR). Compilation is deterministic: the same source and the same image files always produce the same bytes.

Errors

Decoding is strict. Unknown fields (typos), wrong JSON types (for example 2.5 where an integer is expected) 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, wall.vtex containing

{
  "veduta": "texture/1",
  "size": [256, 0],
  "layers": [
    { "type": "solid", "color": "#8a5a2b", "radius": 4 },
    { "type": "noise", "color": "#000000", "octaves": 12, "opacity": 2 },
    { "type": "circle", "center": [128, 128], "color": "#ff0000" },
    { "type": "image", "path": "../logo.png" }
  ]
}

reports

wall.vtex:3:17: size[1]: 0 out of range [1, 4096]
wall.vtex:5:54: layers[0].radius: not used by layer type solid
wall.vtex:6:55: layers[1].octaves: 12 out of range [1, 8]
wall.vtex:6:70: layers[1].opacity: 2 out of range [0, 1]
wall.vtex:7:5: layers[2].radius: is required
wall.vtex:8:32: layers[3].path: "../logo.png": must not leave the assets directory ("..")

Checks: required fields present; size integers in range; 1–64 layers; type known; fields the type does not use absent; colors valid; numbers finite and in range (an explicit 0 for octaves or cells is out of range, not "default"); enum values from the lists above; image paths valid, and the file present, a PNG and not too large; an image rect inside the image; a grid that divides size; clip frames inside the sheet; next and play naming clips; a grid not tiling; an autotile frame of 6 × 3 even square tiles, without edge.

Limits

ItemRange
size1–4096 per side
layers1–64
opacity0–1, default 1
noise scale(0, 4096], default 8
noise octaves1–8, default 1
stripes colors2–64
checker cells1–4096
image filePNG, 1–8192 pixels per side
grid1–256 columns and rows
frames1–256, all side by side within 4096 pixels
clip fps(0, 1000]
edge priority1–1000

Full examples

The example of the specification. The checker is opaque, so it hides every layer below it; only the checker and the logo are visible in the result:

{
  "veduta": "texture/1",
  "size": [256, 256],
  "tiling": true,
  "mipmaps": true,
  "layers": [
    { "type": "solid",    "color": "#8a5a2b" },
    { "type": "noise",    "seed": 7, "scale": 16, "octaves": 3, "color": "#000000", "opacity": 0.25, "blend": "multiply" },
    { "type": "stripes",  "width": 8, "angle_deg": 90, "colors": ["#00000000", "#00000030"] },
    { "type": "rect",     "xy": [16, 16], "size": [224, 224], "color": "#ffffff20", "corner": 6, "outline": 2 },
    { "type": "circle",   "center": [128, 128], "radius": 40, "color": "#ff0000" },
    { "type": "gradient", "from": "#ffffff", "to": "#000000", "angle_deg": 45, "opacity": 0.3 },
    { "type": "checker",  "cells": 8, "colors": ["#c0c0c0", "#404040"] },
    { "type": "image",    "path": "textures/src/logo.png", "fit": "contain" }
  ]
}

A seamless wooden crate side: grain from two noises, horizontal planks separated by thin dark lines, a dark frame and a slight vertical light falloff:

{
  "veduta": "texture/1",
  "size": [128, 128],
  "tiling": true,
  "layers": [
    { "type": "solid", "color": "#9c6b3c" },
    { "type": "noise", "seed": 3, "scale": 2, "octaves": 5, "color": "#5a3a1a", "opacity": 0.7 },
    { "type": "noise", "seed": 9, "scale": 32, "octaves": 2, "color": "#000000", "opacity": 0.15, "blend": "multiply" },
    { "type": "stripes", "width": 2, "angle_deg": 90,
      "colors": ["#00000000", "#00000000", "#00000000", "#00000000", "#00000000", "#00000000",
                 "#00000000", "#00000000", "#00000000", "#00000000", "#00000000", "#00000000",
                 "#00000000", "#00000000", "#00000000", "#2a1a0acc"] },
    { "type": "rect", "xy": [0, 0], "size": [128, 128], "color": "#3a2410", "outline": 4 },
    { "type": "gradient", "from": "#ffffff", "to": "#000000", "angle_deg": 90, "opacity": 0.12, "blend": "screen" }
  ]
}

A HUD badge with transparency (not tiling): a translucent rounded panel, a ring and a logo letterboxed inside:

{
  "veduta": "texture/1",
  "size": [128, 64],
  "mipmaps": false,
  "layers": [
    { "type": "rect", "xy": [0, 0], "size": [128, 64], "color": "#10182080", "corner": 12 },
    { "type": "rect", "xy": [0, 0], "size": [128, 64], "color": "#e9c46a", "corner": 12, "outline": 2 },
    { "type": "circle", "center": [32, 32], "radius": 22, "color": "#ffffff", "outline": 3 },
    { "type": "image", "path": "textures/src/logo.png", "fit": "contain", "opacity": 0.9 }
  ]
}