Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Assets

The platform keeps a library of shared assets on its CDN. spawnite add asset <id> adds one to a game. A model is measured as it lands: the command bakes the game’s public/assets/ folder into src/models.json and registers the model from it through the game’s src/models.ts, with no size to type. Entity says how a walker reads that manifest. Each entry also keeps the corners of the model’s convex hull, which a hull collider wraps on the page and in a room alike; a manifest that no page registers from, such as the kit’s, sets hullPoints: false in its package.json’s models field to leave them out. A particle effect lands in src/particles/<id>.json instead, with its sprites inside it, and the command prints the import and the call that plays it, as Particles says. The MCP’s add_asset tool does the same for an agent.

A game can also name a shared file without copying it. @spawnite/assets exports each of the team’s files by name from its kind’s entry: @spawnite/assets/avatars, animations, models, textures and sounds, such as treeTall from models for tree-tall.glb. Each name is the file’s URL on the CDN, under the SHA-256 of its bytes, so it goes wherever the engine or three.js takes a URL, and no game’s deploy carries a copy. The engine loads its own defaults the same way: its clips, its ground textures and the models it scatters. A published page and a test that loads a shared file need https://assets.spawnite.com reachable. A dev server, in pnpm dev and spawnite play, serves each shared file from a cache on your disk under its own path, /__spawnite/assets/<sha256>.<ext>, and fetches it from the site only the first time: a game loads offline once it has run online, apart from the URLs listed two sentences on. Install says where the cache lives and how to move it. The dev server points each whole shared URL written in a module, a stylesheet or the page at that path; a URL your code builds at run time, one in a file under public/, and one that arrives in fetched JSON or from a room still load from the site. Treat a shared URL as something to load, never as a key or data you send, since the page holds the local path while a room holds the real one. The package’s README lists the entries and each license.

The manifest at the CDN’s assets/index.json carries, for each asset, its id, kind (model, avatar, clip, texture, sound or particle), size, sha256 and URL; its name, category (characters, animals, nature, buildings, props, vehicles, textures, skies, sounds, animations or effects), tags, style (low-poly, stylized or realistic), license (cc0, platform, mixamo, vroid or unknown) and source, with a name, a link where there is one and a credit line; the pack it belongs to, where its file sits in a folder under its kind’s; and what the file itself tells: a model’s or an avatar’s triangles, clip names, whether it is rigged and its bounds in metres, a clip’s names, a texture’s resolution in pixels and, where recorded, the width in metres its tile covers, a sound’s length in seconds, and whether a particle effect loops. A particle effect’s entry also carries a thumbnail URL: a picture of the effect playing, which the publish renders. The hand-written fields come from a catalog.json, one row per file, and the publish refuses a file with no row.

spawnite assets list [query] searches the manifest, the best matches first: a word that starts the name ranks over one that starts a word of the name, over a tag, over the category, pack, source or id, and every word of the query must match. The filters --kind, --category, --pack, --style, --licence, --source, --rigged and --animated narrow the list, and --assets names another manifest. list_assets in the MCP takes the same query and filters.

The asset store shows the same manifest: search, the category and filter chips, and each asset’s page with a live viewer, its facts, its credit line and the command that adds it. The asset store design page holds its rules. An agent reading this page through read_wiki calls list_assets in the MCP for the same list, with a query and filters.

The manifest holds every published asset: the team’s, and each one a creator published with spawnite assets publish. Search it before you fetch, model or draw anything, because a game that names a library asset needs no manual download. Check the license spawnite assets list prints for each file before you use it: cc0 is free for any use, and platform is free in any game on Spawnite and nowhere else. A few carry other terms: Mixamo’s for 7 animations, and VRoid Hub’s for 2 avatars. Three textures have no recorded license, textures/grass.webp, textures/path.webp and textures/path-basis.ktx2, so leave them out of a game until they carry one (counted 2026-10-05).

@spawnite/assets exports each of the team’s files by name. The package holds no file. Each name is the file’s URL on https://assets.spawnite.com, under the SHA-256 of its bytes, such as https://assets.spawnite.com/assets/9c1e….glb. The site serves each file for a year as immutable, so a game names the same bytes for as long as it pins a version of the package, and no game’s deploy carries a copy.

A page that loads a shared file needs https://assets.spawnite.com reachable: a published game and a test that loads a file. A dev server, in pnpm dev and spawnite play, keeps each file in a cache on your disk after its first load, so a game loads offline once it has run online; the install page says where the cache lives. Allow the host in a sandbox that lists the hosts an agent may reach. A published game’s page loads from it under the platform’s content policy, and a game’s own deploy allows it in the policy roomsContentPolicy writes.

The package also exports the following data:

  • @spawnite/assets/avatars.json holds each body’s record under its file’s name: everything a VrmBody holds but the URL, which the game adds.
  • @spawnite/assets/models.json holds each model’s measurements under its id: its bounds, its collider and, where it has one, its hull, with path the model’s URL.
  • @spawnite/assets/particles/<file>.json is a particle effect for the engine’s Particles, imported as JSON with its sprites inside it.
import records from "@spawnite/assets/avatars.json";
const body: VrmBody = {
...(records.fris as Omit<VrmBody, "model">),
model: fris,
};

The cast is there because JSON types a bone name as a plain string.

The engine names its grounds and its skies. You change either one by name, before you fetch any file.

The map sets the ground. A <World> with no map stands on grid, a grey floor 8 m across. <World map="meadow"> stands on hills of textured grass. World describes both.

A map paints its ground from 14 named materials, such as grass, dirt, sand, snow and stone. Use the following commands to lay and change them:

To do this Run this
Paint one dab spawnite map terrain paint --map meadow --material sand --at 0,0 --radius 6
Fill a region spawnite map terrain fill --map meadow --material stone --region rollingField
Change how a material looks everywhere spawnite map material grass --tint '#8fbf5a' --tile 3
Make a new material from an existing one spawnite map material lava --from rock

To swap the hand-painted textures for photographed ones, set "set": "photographed" in the game’s src/maps/materials.json. snow and ice keep their hand-painted textures.

To use a texture you fetched, put the file in public/assets/ and name it in a material’s texture field:

"moss": { "from": "grass", "texture": "/assets/moss.webp" }

The texture field takes the colour image only, so the material draws without bumps of its own. The map editor lists every field.

The look sets the sky. The engine has five: noon, morning, dusk, night and overcast. Import one from @spawnite/engine/looks/<name> and pass it to the World, as <World look={dusk}>. Each look brings its own fog.

Each look lights the scene from the engine’s own sky, so it adds nothing to the download. To light the scene from an HDRI you fetched instead, name it as the look’s environment image, as look={{ base: noon, environment: { image: { file: "/assets/sky.hdr", sunU: 0.25 } } }}. sunU is the column of the image where the sun stands, from 0 to 1. Looks describes the image.

When the library has nothing that fits, take the asset from a source whose files are CC0, free for any use with no attribution required. The following sources serve CC0 files, most of them to a terminal with no browser and no sign-in; the table names each that needs a browser:

Source What it has How to fetch with curl
Poly Haven HDRI skies, PBR textures, realistic models https://api.polyhaven.com/assets?t=hdris lists the skies, t=textures and t=models the rest. https://api.polyhaven.com/files/<asset> lists each file of one asset with its URL; take the URL from that list, since file names vary.
ambientCG PBR ground and surface materials, HDRI skies https://ambientcg.com/api/v2/full_json?type=Material&include=downloadData lists the materials and their zips, such as https://ambientcg.com/get?file=Bricks097_1K-JPG.zip. The zip link redirects, so pass curl -L.
Kenney Low-poly model kits, textures, UI art, sounds No API: each pack’s page holds one zip link. curl -s https://kenney.nl/assets/<pack> | grep -oE 'https://kenney.nl/media/[^"]+\.zip' prints it.
Poly Pizza Low-poly models as GLB, CC0 and CC-BY mixed Each model’s page holds its license, as "Licence":"CC0 1.0", and its GLB link on static.poly.pizza. Take a model only when its page says CC0. The API needs a key from an account, and the terms forbid scraping.
Quaternius Low-poly model packs: nature, characters, monsters Each pack’s download is a Google Drive folder, which needs a browser, so ask your user to fetch the pack, or take a model from another source.

Take the following steps to bring an asset in:

  1. Fetch the file at the smallest size that looks right: a 1K texture or a 1K HDRI. The library’s ground textures are 1024 pixels square, from 12 KB to 557 KB as WebP, and its five skies are 1K HDRIs of 1.4 to 1.9 MB.
  2. Convert it to a format the platform takes: a model to .glb with its textures inside, a texture to .webp, and a sound to a mono .mp3, such as ffmpeg -i hit.ogg -ac 1 -codec:a libmp3lame -b:a 96k hit.mp3. The library’s short sounds weigh 1.5 to 40 KB as mono MP3s. A sky stays an .hdr, which the publish does not take: put it in the game’s public/ folder, and a look’s environment.image names it, as Looks says.
  3. Check it: a model with spawnite model check, as Models says, and a sound with spawnite sound check, as Check a sound for hiss says.
  4. Write its row in the folder’s catalog.json with the license cc0 and a source that names the site, the page’s url, and a credit line with the pack, the original file’s name and what you changed, as Publish an asset shows.
  5. Publish it with spawnite assets publish, so the next game finds it in the library rather than on the web. A sky stays in the game.

A recording can carry a constant noise under it, such as a microphone’s hiss, or be nothing but noise, such as a wind recording that turns out to be a hiss. spawnite sound check <files...> reads sound files with the installed ffmpeg, in any format a source ships, and prints each one’s length, peak, loudness, and the level and spectral flatness of its quietest tenth. It measures each channel on its own and reports the noisiest. The MCP’s check_sound tool does the same:

$ spawnite sound check fire-crackle.mp3
fire-crackle.mp3: 60.06 s, peak 0.1 dBFS, loudness -50.1 dBFS, quietest tenth -57.7 dBFS at flatness 0.77.
It carries a constant noise: its quietest tenth is louder than -65.0 dBFS and as flat as broadband noise, at 0.6 or more. Take another recording, or clean this one with ffmpeg's afftdn filter and check it again.

Flatness runs from 0 for a pure tone to 1 for white noise. The check measures it below 11 kHz, under the cut an MP3 encoder makes, where white noise reads 0.80, pink noise 0.71, brown noise 0.47, and the quiet of crickets or steam 0.33. A sound of 1 s or more carries a constant noise when its quietest tenth is both louder than −65 dBFS and at a flatness of 0.6 or more. The check does not judge a shorter sound, because the quietest tenth of a hit or a step is its own decay.

ffmpeg -i fire-crackle.mp3 -af afftdn fire-clean.mp3 takes the fire above to a quietest tenth of −68.0 dBFS, which passes. A sound that is all noise stays noise after cleaning, so take another recording. With no ffmpeg on the PATH, the check prints the command that installs it.

You can add your own model, avatar, animation clip, texture, sound or particle effect to the shared assets, so any game on the platform adds it with spawnite add asset. Publishing needs a sign-in: run spawnite login first.

Take the following steps:

  1. Put the file in a folder. A model is a .glb, an avatar a .vrm, a clip a .vrma, a texture a .webp, a sound an .mp3 and a particle effect the .json the three.quarks editor exports. A .glb or .vrm is at most 50 MB, an effect at most 5 MB, and the other types at most 20 MB. An effect that names its sprite by a file name, such as spark.png, takes that file beside it in the folder: the publish carries it inside the effect.

  2. Write the file’s row in a catalog.json in that folder, under the file’s path from the folder:

    {
    "rock-flat.glb": {
    "name": "Flat rock",
    "category": "nature",
    "tags": ["rock", "stone"],
    "style": "low-poly",
    "licence": "cc0",
    "source": { "name": "Max", "credit": "Made in Blender." }
    }
    }

    The name, category, license and source are required. The source names who made the asset, with a credit line and an optional url. The license is cc0, free for any use, platform, free in any game on the platform and nowhere else, mixamo or vroid for a file under those sites’ terms, or unknown.

  3. Run spawnite assets publish rock-flat.glb, or name the folder to publish every file in it. The MCP’s publish_assets tool does the same for an agent.

The command prints the asset’s id, such as flat-rock-48201973: its name as a slug, then a number the platform draws, so two assets never share an id. The store and spawnite assets list show the asset within a minute.

The platform checks each file before it is public, and refuses one that fails with the reason:

  • A model, an avatar or a clip must be a glTF Binary file that holds everything it uses. A file that loads a buffer or an image from another address is refused, so export with every texture embedded.
  • A texture must be a WebP image that decodes, every frame of it. The platform removes its EXIF and XMP, which can hold a name or a place.
  • A sound must be an MP3. The platform removes its ID3 tags, which can hold a name or a picture.
  • A particle effect must be three.quarks JSON whose sprites decode. Before it sends anything, the command plays the effect in headless Chromium, which refuses a file three.quarks cannot parse, and keeps a picture of it in the air as the store’s preview. A sprite named by a file that is not beside the effect, or loaded from a site, is refused, and so is an effect past the limits that Sharing an effect lists.
  • A file of any other type is refused by its content, whatever its name says.

An account publishes at most 100 assets a day, a refused file among them, and each asset counts against the account’s 10 GB of storage. Publishing the same file again sends nothing and prints the id it already has.