Skip to content

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.

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.

Terminal window
echo "0.12 0.55 0.95" > /sim/vessels/by-id/Hunter/paint/parts/color
echo 1 > /sim/vessels/by-id/Hunter/paint/parts/enabled
echo tint > /sim/paint/parts/blend
echo 1 > /sim/paint/parts/enabled
cat /sim/paint/status

multiply 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.

ScopePathMeaning
Globalpaint/parts/global/{enabled,color,clear}every paintable vehicle part
Templatepaint/parts/templates/<template>/{enabled,color,clear}every part with that raw Part.Template.Id
Vesselvessels/by-id/<id>/paint/parts/{enabled,color,clear}every current and future part in one live vessel
Partvessels/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.

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.

Terminal window
# Shared default for every live EVA.
echo "0.85 0.25 0.55" > /sim/paint/kittens/shared/color
echo 1 > /sim/paint/kittens/shared/enabled
echo 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/color
echo 1 > /sim/vessels/by-id/Valentina/paint/kitten/materials/visor/enabled

Semantic 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.

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.

Terminal window
cat /sim/paint/textures/clutter # texture-id slot w h mips used_by ecotypes
cat rock.png > /sim/paint/textures/file/rock.png # upload; commits on close
echo 'EarthGrassClutterDiffuse rock.png' > /sim/paint/textures/bind # renders as authored
echo 'EarthGrassClutterDiffuse rock.png raw' > /sim/paint/textures/bind # …or as KSA reads its own
cat /sim/paint/textures/applied # what actually reached the GPU
echo all > /sim/paint/textures/unbind # restore every stock texture
LeafRead / write
paint/textures/file/Writable upload directory. Chunked writes commit on close; rm evicts and unbinds first.
paint/textures/cluttertexture-id slot width height mips used_by ecotypes; slot is diffuse, normal, pbr, opacity, or thickness.
paint/textures/bindingstexture-id file mode, one row per desired binding — the same shape bind takes.
paint/textures/appliedtexture-id file state width height mips vram_bytes error; state is pending, applied, or failed.
paint/textures/statusavailable bound applied catalog retiring vram_bytes revision error.
paint/textures/infoFile and byte usage against files_max, bytes_max, file_bytes_max, bindings_max, max_dimension.
paint/textures/helpThe grammar, the bind modes, and what the shader does, in the filesystem.
paint/textures/bindWrite texture-id file [faithful|raw]; the mode defaults to faithful.
paint/textures/unbindWrite texture-id, or all.
paint/textures/clearWrite 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.

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.

Terminal window
cat meow.png > /sim/paint/textures/file/meow.png # upload once
echo 'meow.png w=2 h=2' > /sim/paint/stickers/spray # 2 m decal where the camera looks
cat /sim/paint/stickers/last # 0 body Kerth 12.03 -41.88 hit 84.2m
echo 0.4 > /sim/paint/stickers/0/alpha # tune it live
cat /sim/paint/stickers/status # 0 meow.png body Kerth live=1 texture=ready
echo 1 > /sim/paint/stickers/0/remove # or: echo 1 > /sim/paint/stickers/clear

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.

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=]
KeyUnitDefaultRange
w, hmetres1, 1each (0, 1000]
dmetres0.3 on a vessel, 1 on a body(0, 100]
roll / headingdegrees0any finite value
alpha1[0, 1]
brightness1(0, 8]
range (spray only)metres2000(0, 1e6]
aim (spray only)cameracamera, 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.

  • 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 what spray picks, 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.

LeafRead / write
paint/stickers/helpThe grammars, the anchors, the tuning knobs, and worked examples, in the filesystem.
paint/stickers/infoenabled= stickers= stickers_max= live= images= vram_bytes= patch=0|1 renderer= max_view_distance_m=.
paint/stickers/statusOne row per sticker: id image vessel|body target live=0|1 texture=ready|missing|uploading|failed.
paint/stickers/lastThe outcome of the last place/spray: id, anchor, target, and either placed or hit <distance>m.
paint/stickers/last_errorRenderer or texture fault text; empty when healthy.
paint/stickers/countHow many stickers exist.
paint/stickers/placeWrite an exact-anchor line.
paint/stickers/sprayWrite an aimed line.
paint/stickers/clearWrite 1 for the global teardown: every sticker removed, uploads kept.
paint/stickers/debugWrite 0|1. Draws every sticker as a magenta checker of its projection box instead of its image.
paint/stickers/<id>/specThe write-compatible place line. cat it into place to clone the sticker under a new id.
paint/stickers/<id>/anchorvessel <id> <iid> x y z nx ny nz, or body <id> lat lon.
paint/stickers/<id>/live1 while the anchor resolves and the image is resident.
paint/stickers/<id>/imageWrite another uploaded image name to hot-swap what this sticker draws.
paint/stickers/<id>/visibleWrite 0|1. 0 hides it without removing the entry.
paint/stickers/<id>/sizeWrite w h in metres.
paint/stickers/<id>/depthWrite the projection-box depth in metres.
paint/stickers/<id>/rotationWrite degrees — roll about the normal on a vessel anchor, compass heading on a body anchor.
paint/stickers/<id>/alphaWrite [0, 1].
paint/stickers/<id>/brightnessWrite (0, 8].
paint/stickers/<id>/removeWrite 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.

Terminal window
# The unix toolbox is the save format.
for d in /sim/paint/stickers/[0-9]*; do cat "$d/spec"; done > ~/specs
while read -r line; do echo "$line" > /sim/paint/stickers/place; done < ~/specs

Re-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.

KeyDefaultEffect
paint_stickers_enabledtrueWhether /sim/paint/stickers exists at all. false removes the subtree.
paint_stickers_max_count256Maximum simultaneous stickers, clamped to 1..4096.
paint_stickers_max_view_distance_m5000.0Metres 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.

  • MQTT reads retained gatos/sim/<path> topics and writes the same raw values to gatos/sim/<path>/set.
  • MCP uses gatos.paint_control. Read complete desired/runtime state with gatos.get_runtime_state({"feature":"paint"}) or gatos://runtime/paint; request include:["paint"] from gatos.get_vessel for one vessel.
  • Clutter textures use gatos.paint_texture for the store and gatos.paint_control operations texture_bind, texture_unbind, and texture_clear for the bindings. The bind mode rides value: 0 is faithful (the default) and 1 is raw. Read complete texture state with gatos.get_runtime_state({"feature":"paint_textures"}) or gatos://runtime/paint_textures.
  • Stickers use gatos.paint_sticker, whose operation selects place, spray, set, remove, clear, list, or debug; images still upload through gatos.paint_texture. Read complete sticker state with gatos.get_runtime_state({"feature":"paint_stickers"}) or gatos://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.