Vehicle and EVA paint
Paint is a session-only visual system with two independent masters: one for vehicle-part shaders and one for EVA material clones. Both ship off. The files remain discoverable while off, but gatOS does not install shader hooks, rebuild a renderer, rebind an EVA material, or allocate a GPU clone until you opt in.
Paint a whole vessel
Section titled “Paint a whole vessel”Choose a normalized sRGB color, enable the vessel rule, then opt in to the part shader. The rule is live: parts added to the same vessel inherit it. Part glass stays stock.
echo "0.12 0.55 0.95" > /sim/vessels/by-id/Hunter/paint/parts/colorecho 1 > /sim/vessels/by-id/Hunter/paint/parts/enabledecho tint > /sim/paint/parts/blendecho 1 > /sim/paint/parts/enabledcat /sim/paint/statuscurl -X POST --data '0.12 0.55 0.95' \ http://127.0.0.1:4242/v1/fs/vessels/by-id/Hunter/paint/parts/colorcurl -X POST --data '1' \ http://127.0.0.1:4242/v1/fs/vessels/by-id/Hunter/paint/parts/enabledcurl -X POST --data 'tint' \ http://127.0.0.1:4242/v1/fs/paint/parts/blendcurl -X POST --data '1' \ http://127.0.0.1:4242/v1/fs/paint/parts/enabledcurl http://127.0.0.1:4242/v1/fs/paint/statusmultiply modulates the sampled texture, tint mixes the texture toward the chosen color, and
replace uses the chosen color directly while retaining the shader’s lighting. Changing mode while
active requests another deferred renderer rebuild.
To opt out, write 0 to paint/parts/enabled. gatOS removes its hooks and requests a stock shader
rebuild. Desired rules remain in memory, ready for re-enable; use paint/parts/clear to discard them.
Choose a vehicle scope
Section titled “Choose a vehicle scope”| Scope | Path | Meaning |
|---|---|---|
| Global | paint/parts/global/{enabled,color,clear} | every paintable vehicle part |
| Template | paint/parts/templates/<template>/{enabled,color,clear} | every part with that raw Part.Template.Id |
| Vessel | vessels/by-id/<id>/paint/parts/{enabled,color,clear} | every current and future part in one live vessel |
| Part | vessels/by-id/<id>/parts/<n>/paint/{enabled,color,clear} | one stable top-level part instance |
| Subpart | .../subparts/<m>/paint/{enabled,color,clear} | one stable recursive subpart instance |
More specific rules win: part instance → vessel → template → global → stock. A disabled rule at a
more specific scope falls through to the next enabled rule. Individual part paths require
telemetry_vessel_parts=true; use parts/<n>/instance_id or parts/json to discover stable identity.
Paint EVA kittens
Section titled “Paint EVA kittens”EVA paint has its own master and does not modify KSA’s shared stock materials. gatOS creates bounded, pooled material clones and restores the original avatar slots on disable, despawn, avatar rebuild, or mod unload.
# Shared default for every live EVA.echo "0.85 0.25 0.55" > /sim/paint/kittens/shared/colorecho 1 > /sim/paint/kittens/shared/enabledecho 1 > /sim/paint/kittens/enabled
# Override one kitten's visor only.echo "0.1 0.9 0.35" > /sim/vessels/by-id/Valentina/paint/kitten/materials/visor/colorecho 1 > /sim/vessels/by-id/Valentina/paint/kitten/materials/visor/enabledSemantic material names are body, fur, helmet, visor, and mmu, with .n suffixes when an
avatar has multiple slots. Read /sim/paint/status and the material directories for live discovery.
Sclera and cosmetics remain stock.
EVA precedence is individual material → individual default → shared material → shared default →
stock. “Shared” means the rule applies to every live EVA through safe clones; it does not overwrite
one material used by unrelated characters. The clone pool defaults to 64 and is capped by
paint_max_material_clones.
Override a ground-clutter texture
Section titled “Override a ground-clutter texture”Ground-clutter textures are a third, independent paint mechanism: upload an image and bind it over a stock clutter texture — grass, trees, rocks. gatOS re-points the existing bindless texture slot at your image and keeps the stock view to write back on unbind, so nothing is patched, no shader is transformed, and no new GPU slot is consumed. Uploads live in mod memory for the session only.
Unlike vehicle and EVA paint there is no runtime master to enable: with nothing bound the whole
feature costs one integer comparison per frame. The subtree exists whenever
paint_textures_enabled=true, which is the default.
Read paint/textures/clutter first — it lists every stock texture you can override, one row per
texture, and it is the only place the texture ids come from.
cat /sim/paint/textures/clutter # texture-id slot w h mips used_by ecotypescat rock.png > /sim/paint/textures/file/rock.png # upload; commits on closeecho 'EarthGrassClutterDiffuse rock.png' > /sim/paint/textures/bind # renders as authoredecho 'EarthGrassClutterDiffuse rock.png raw' > /sim/paint/textures/bind # …or as KSA reads its owncat /sim/paint/textures/applied # what actually reached the GPUecho all > /sim/paint/textures/unbind # restore every stock textureH=http://127.0.0.1:4242/v1curl -s "$H/fs/paint/textures/clutter"curl -T rock.png "$H/paint/texture/file/rock.png"curl -X POST --data 'EarthGrassClutterDiffuse rock.png' "$H/fs/paint/textures/bind"curl -X POST --data 'EarthGrassClutterDiffuse rock.png raw' "$H/fs/paint/textures/bind"curl -s "$H/fs/paint/textures/applied"curl -X POST --data 'all' "$H/fs/paint/textures/unbind"Read and control the texture store
Section titled “Read and control the texture store”| Leaf | Read / write |
|---|---|
paint/textures/file/ | Writable upload directory. Chunked writes commit on close; rm evicts and unbinds first. |
paint/textures/clutter | texture-id slot width height mips used_by ecotypes; slot is diffuse, normal, pbr, opacity, or thickness. |
paint/textures/bindings | texture-id file mode, one row per desired binding — the same shape bind takes. |
paint/textures/applied | texture-id file state width height mips vram_bytes error; state is pending, applied, or failed. |
paint/textures/status | available bound applied catalog retiring vram_bytes revision error. |
paint/textures/info | File and byte usage against files_max, bytes_max, file_bytes_max, bindings_max, max_dimension. |
paint/textures/help | The grammar, the bind modes, and what the shader does, in the filesystem. |
paint/textures/bind | Write texture-id file [faithful|raw]; the mode defaults to faithful. |
paint/textures/unbind | Write texture-id, or all. |
paint/textures/clear | Write 1 for the global teardown: every stock texture restored, uploads kept. |
A binding is desired state → applied state: bind records it immediately, and the next frame
uploads the image and re-points the slot, which is when applied gains a row. Watch applied for
the outcome; a failed row carries the reason in its last column. Writing all to unbind is
exactly the clear trigger, so the two spellings cannot drift apart.
Uploads accept png, jpeg, bmp, hdr, dds, ktx, and ktx2 — the container is sniffed from the bytes, so
the file extension is for humans. Names are a single component of at most 64 characters from
[A-Za-z0-9._-]. Limits answer EFBIG for the per-file cap and ENOSPC for the file-count, total
byte, and binding caps, enforced mid-write so the failing write carries the errno. Binding a name
that has not finished uploading is EBUSY; an unknown texture id or file is ENOENT; an
unrecognized image container is EINVAL.
Texture uploads are one of only two binary surfaces in gatOS with dedicated HTTP routes (audio clips
are the other): PUT or POST /v1/paint/texture/file/<name> with offset and complete=0|1 for
ordered chunks, GET /v1/paint/texture/files, and DELETE /v1/paint/texture/file/<name>. Requests
are capped at 1 MiB and a larger declared Content-Length is refused with 413, so chunking is the
normal path for real PNGs. MQTT mirrors the text leaves but carries no binary upload.
Spray a sticker
Section titled “Spray a sticker”Stickers are the fourth paint mechanism, and the only one that puts your own artwork into the world rather than onto a material: upload a PNG, then spray it where the camera is pointing or place it at exact coordinates. A sticker is a projected decal — it sticks to the rocket as it flies and to the ground as the planet turns — and it stays until you remove it.
Images come from the same store the clutter overrides use. There is no second upload surface.
cat meow.png > /sim/paint/textures/file/meow.png # upload onceecho 'meow.png w=2 h=2' > /sim/paint/stickers/spray # 2 m decal where the camera lookscat /sim/paint/stickers/last # 0 body Kerth 12.03 -41.88 hit 84.2mecho 0.4 > /sim/paint/stickers/0/alpha # tune it livecat /sim/paint/stickers/status # 0 meow.png body Kerth live=1 texture=readyecho 1 > /sim/paint/stickers/0/remove # or: echo 1 > /sim/paint/stickers/clearH=http://127.0.0.1:4242/v1curl -T meow.png "$H/paint/texture/file/meow.png"curl -X POST --data 'meow.png w=2 h=2' "$H/fs/paint/stickers/spray"curl -s "$H/fs/paint/stickers/last"curl -X POST --data '0.4' "$H/fs/paint/stickers/0/alpha"curl -s "$H/fs/paint/stickers/status"curl -X POST --data '1' "$H/fs/paint/stickers/0/remove"Each create takes the lowest free id — 0, 1, 2, … — and remove/clear free ids for reuse, so
the numbering tracks what is live. A successful place or spray also emits a paint.sticker_placed
event, so a script can watch /sim/events instead of polling last.
The two grammars
Section titled “The two grammars”spray aims a ray; place states the anchor outright. Both take the image name first, and both
share the same optional key=value tail.
spray: <image> [aim=camera|cursor] [range=] [roll=] [w=] [h=] [d=] [alpha=] [brightness=]place: <image> vessel <vessel_id> <part_iid> <x> <y> <z> <nx> <ny> <nz> [roll=] [w=] [h=] [d=] [alpha=] [brightness=]place: <image> body <body_id> <lat> <lon> [heading=] [w=] [h=] [d=] [alpha=] [brightness=]| Key | Unit | Default | Range |
|---|---|---|---|
w, h | metres | 1, 1 | each (0, 1000] |
d | metres | 0.3 on a vessel, 1 on a body | (0, 100] |
roll / heading | degrees | 0 | any finite value |
alpha | — | 1 | [0, 1] |
brightness | — | 1 | (0, 8] |
range (spray only) | metres | 2000 | (0, 1e6] |
aim (spray only) | — | camera | camera, cursor |
aim=camera fires down the main camera’s forward axis, which works headless and lets a
/sim/camera track do the pointing for you; aim=cursor uses the mouse
cursor’s picking ray. The ray hits a vehicle part first and the terrain behind it; nothing hit is
ENOENT, and last records the miss. On spray, roll= is added to the orientation the picker
chose to make the image read upright from where you are standing, rather than replacing it — so
roll=0 is upright and roll=90 is a quarter turn from upright.
A duplicate key, an unknown key, a value out of range, or a malformed line fails the write(2) with
EINVAL before anything is queued. d is the one slot with a two-valued default: omit it on spray
and the anchor kind the ray resolved picks it.
The two anchors
Section titled “The two anchors”vessel <id> <part_iid>stores the decal in that part’s local frame and re-resolves it every frame, so it follows the part — including a sub-part, which is whatspraypicks, and which is why a gimballing nozzle carries its decal through the gimbal. Part instance ids come from/sim/vessels/by-id/<id>/parts/<n>/instance_id.body <id> <lat> <lon>stores it geodetically, so it rides the planet’s rotation for free and survives time warp.
Neither anchor is ever stored in world coordinates, which is what lets a sticker survive a scene
reload, a vessel switch, and a floating-origin shift. A vehicle that despawns — or a part that stages
away — makes its sticker dormant, not deleted: live reads 0 until the anchor comes back, the
entry stays in the registry, and <id>/spec stays readable. Only remove, clear, and mod unload
delete entries.
Read and control the sticker registry
Section titled “Read and control the sticker registry”| Leaf | Read / write |
|---|---|
paint/stickers/help | The grammars, the anchors, the tuning knobs, and worked examples, in the filesystem. |
paint/stickers/info | enabled= stickers= stickers_max= live= images= vram_bytes= patch=0|1 renderer= max_view_distance_m=. |
paint/stickers/status | One row per sticker: id image vessel|body target live=0|1 texture=ready|missing|uploading|failed. |
paint/stickers/last | The outcome of the last place/spray: id, anchor, target, and either placed or hit <distance>m. |
paint/stickers/last_error | Renderer or texture fault text; empty when healthy. |
paint/stickers/count | How many stickers exist. |
paint/stickers/place | Write an exact-anchor line. |
paint/stickers/spray | Write an aimed line. |
paint/stickers/clear | Write 1 for the global teardown: every sticker removed, uploads kept. |
paint/stickers/debug | Write 0|1. Draws every sticker as a magenta checker of its projection box instead of its image. |
paint/stickers/<id>/spec | The write-compatible place line. cat it into place to clone the sticker under a new id. |
paint/stickers/<id>/anchor | vessel <id> <iid> x y z nx ny nz, or body <id> lat lon. |
paint/stickers/<id>/live | 1 while the anchor resolves and the image is resident. |
paint/stickers/<id>/image | Write another uploaded image name to hot-swap what this sticker draws. |
paint/stickers/<id>/visible | Write 0|1. 0 hides it without removing the entry. |
paint/stickers/<id>/size | Write w h in metres. |
paint/stickers/<id>/depth | Write the projection-box depth in metres. |
paint/stickers/<id>/rotation | Write degrees — roll about the normal on a vessel anchor, compass heading on a body anchor. |
paint/stickers/<id>/alpha | Write [0, 1]. |
paint/stickers/<id>/brightness | Write (0, 8]. |
paint/stickers/<id>/remove | Write 1 to delete this one sticker. |
spec round-trips: it is exactly the form place accepts, so reading it and echoing it back
recreates the sticker as a new id — which is also how a guest-side save/restore script works, since
nothing about stickers is persisted.
# The unix toolbox is the save format.for d in /sim/paint/stickers/[0-9]*; do cat "$d/spec"; done > ~/specswhile read -r line; do echo "$line" > /sim/paint/stickers/place; done < ~/specsRe-uploading the same file name hot-swaps the image under every sticker using it; rm-ing it from
/sim/paint/textures/file/ makes those stickers dormant, and re-uploading brings them back. Images
are capped at 2048 px on the longest edge for stickers specifically — larger uploads are downscaled,
not rejected — because a decal a few metres across does not need a 4096² mip chain.
Sticker configuration
Section titled “Sticker configuration”| Key | Default | Effect |
|---|---|---|
paint_stickers_enabled | true | Whether /sim/paint/stickers exists at all. false removes the subtree. |
paint_stickers_max_count | 256 | Maximum simultaneous stickers, clamped to 1..4096. |
paint_stickers_max_view_distance_m | 5000.0 | Metres past which a sticker is not drawn, clamped to 10..1000000. |
Like clutter textures there is no runtime master: with nothing placed the feature is one
emptiness branch per frame, and the Vulkan pipeline and the render hook do not exist until the first
sticker goes live — they are torn down again when the last one goes away. The subtree also requires
paint_textures_enabled, because a sticker draws an uploaded image; with no image store there is
nothing to draw and the subtree is absent.
Other interfaces
Section titled “Other interfaces”- MQTT reads retained
gatos/sim/<path>topics and writes the same raw values togatos/sim/<path>/set. - MCP uses
gatos.paint_control. Read complete desired/runtime state withgatos.get_runtime_state({"feature":"paint"})orgatos://runtime/paint; requestinclude:["paint"]fromgatos.get_vesselfor one vessel. - Clutter textures use
gatos.paint_texturefor the store andgatos.paint_controloperationstexture_bind,texture_unbind, andtexture_clearfor the bindings. The bind mode ridesvalue:0isfaithful(the default) and1israw. Read complete texture state withgatos.get_runtime_state({"feature":"paint_textures"})orgatos://runtime/paint_textures. - Stickers use
gatos.paint_sticker, whoseoperationselectsplace,spray,set,remove,clear,list, ordebug; images still upload throughgatos.paint_texture. Read complete sticker state withgatos.get_runtime_state({"feature":"paint_stickers"})orgatos://runtime/paint_stickers. - Generic HTTP and MQTT command envelopes accept every canonical
paint.*action listed by capabilities.
All paint actions are Frame phase and honor control_enabled, but visual by-id operations do not
require the target to be the controlled vessel. Texture actions carry their own gate — the store has
to be enabled, not either paint master — and answer EOPNOTSUPP when paint_textures_enabled=false.
Sticker actions carry a third gate of their own and are vessel-agnostic: the registry id rides the
command ordinal, so they resolve before any vessel lookup. Rules are never written into a save game.
Mod unload clears them and restores stock rendering.