CARLA assets - SethKitchen/ThreeMojo GitHub Wiki

CARLA assets

The CARLA town can use real assets in place of its procedural ones: photoscanned road textures, sky panoramas and CARLA's own vehicle models. The manifest, assets/carla/manifest.json, lists each asset. The tool tools/carla_assets.mojo downloads the assets into .cache/carla-assets/ with curl and checks the SHA-256 sum of each file. Git ignores the cache. A build or a test does not need the assets: when an asset is not in the cache, the renderer uses its procedural asset.

Fetch the assets

Run the commands from the repository root. tools/tool.sh builds the tool once and then runs it.

  1. Check the manifest:

    sh tools/tool.sh .venv/bin/mojo carla_assets check
  2. Download each asset that has a URL and a sum:

    sh tools/tool.sh .venv/bin/mojo carla_assets fetch
  3. See which entries the cache holds:

    sh tools/tool.sh .venv/bin/mojo carla_assets status

To fetch one entry, give its id, for example fetch carla.vehicle.audi.a2. The tool writes a file only after its sum matches. It never writes over a file that is already verified.

Recover an offline cache

Use fetch --offline when a host is unavailable or this computer must not download files. It opens no URLs, including local file:// URLs. It verifies cached archives before extracting missing members. It never changes the manifest or replaces a mismatched file.

  1. Obtain the exact pinned archive from an existing backup or an approved source. A rebuilt archive can have a different checksum, even if its contents look the same. Do not use --pin or change the manifest checksum to accept it.

  2. Read the entry's files[].path and sha256 in assets/carla/manifest.json. Put the archive at that path under the cache root. For example, the Audi A2 archive belongs at .cache/carla-assets/carla/vehicles/vehicle.audi.a2.zip. Keep any existing mismatched file separately before placing a replacement.

  3. Verify the archive and extract missing members without downloading:

    sh tools/tool.sh .venv/bin/mojo carla_assets fetch --offline carla.vehicle.audi.a2
  4. Check that the selected archive and every named member are present and correct:

    sh tools/tool.sh .venv/bin/mojo carla_assets verify --strict carla.vehicle.audi.a2

Both commands return a nonzero exit code for missing, unpinned or mismatched selected files. An invalid entry id also fails. fetch --offline cannot be combined with --pin. An existing member with different bytes stops extraction. Keep that file separately, then repeat the offline command to restore it from the verified archive.

Omit the ids to process every entry. Use --cache PATH before the command to select a different cache root. Ordinary verify still reports missing and unpinned files without failing. Use --strict when a complete selected cache is required.

Keep manifest.json and ATTRIBUTION.md beside backup or downloadable archives. stage-archive packages them with the archives. See "Stage archives for sharing". Keep the applicable attribution with rendered distributions too. These files preserve the pinned sums, CARLA release, conversion changes, CARLA Team credit, source links and CC BY 4.0 link.

Preload texture maps

Call registry.preload(workers) before building a town to decode each bound, cached texture map once per color space. A worker count of one or less decodes maps in order. Higher counts limit active decodes to that many maps. A free worker reads the next image without waiting for slower images. CARLA and glTF use the same image decode queue.

Completed maps move into the registry without a second copy of their pixels and mipmaps. A failed preload adds no partial cache entries. texture_set returns independent maps, so changing a set cannot change the registry's cached maps. Texture sets share immutable payloads and copy on write.

Share cached vehicle resources

ActorVisuals reads each cached vehicle model once per geometry and texture store. Instances share geometry and image bytes. Each instance has its own nodes, paint, head lamps, tail lamps, and other materials. The cache keeps a private source graph. Changes to a live vehicle do not change that graph.

For direct model placement, create a ModelCache from extensions.carla.model_cache. Call cache.place(registry, index, scene, assets, parent, fit) for each instance. AssetRegistry.place_model remains the uncached operation. The cache uses the same fit, forward axis, node hierarchy, and material tags. It also copies any line, point, or punctual-light instances. Skinned and instanced models remain unsupported by these CARLA placement operations.

The key includes the cache root, entry id, and each declared file role, path, and digest. Blueprint aliases bound to the same entry share one model. The fit and forward axis are applied for each placement. They do not require another copy of the source bytes.

Resource lifetime and invalidation

Treat shared geometry and textures as immutable. Do not replace or edit their contents while the cached model or its instances are in use. Materials and nodes are independent and can change.

A cache retains each store's allocation identity, not its resource buffers. Moving a cache or store preserves its identity. These types cannot be copied.

A new geometry or texture store gets a new identity. Replacing either store clears the cache before reuse. Retained identities prevent an old allocation address from matching a new store after destruction. Replacing the material store needs no resource reload because each placement copies material values.

Call visuals.cached_models.clear() before a cache file changes in place. For a direct ModelCache, call cache.clear(). Changed declared paths or digests select a new template automatically. A byte change under an unchanged declaration requires clear(), even if the declaration includes a digest. Placement does not verify file bytes against the declared digest. Use the asset tool to verify downloaded files.

Clearing a cache removes its private templates. Existing instances remain valid. Geometry and textures stay in the append-only stores. Rebuild the scene and stores to reclaim their resources. Create a new ActorVisuals at the same time: its existing actor, node, and material ids belong to that scene and those stores. A direct ModelCache can place into several scenes that use the same resource stores.

A failed private model load commits no cache entry. It restores the resource counts from before the load. Retrying a failed cached vehicle spawn does not add unused actor nodes or paint and lamp materials.

Measure repeated spawns

Generate a synthetic load-size fixture:

mojo run -I . bench/carla_model_cache_fixture.mojo /tmp/carla-model-bench
.venv/bin/mojo build --Werror -I . bench/carla_model_cache_bench.mojo -o /tmp/carla-model-bench-run
/tmp/carla-model-bench-run /tmp/carla-model-bench 32 batch
/tmp/carla-model-bench-run /tmp/carla-model-bench 32 incremental

The fixture has 34,992 indexed triangles, six materials, and one 1024-square texture. It is a size proxy, not a CARLA source model. Run the same benchmark source on the base and candidate with the same fixture. Alternate their run order. Compare the checksums and resource counts before comparing times.

The timer measures ActorVisuals.sync. Actor creation and fixture generation are outside it. The byte totals count geometry attributes and indices, and texture pixels with mipmaps. They do not count object or allocator overhead. Use a process-memory tool for peak resident memory.

Materials and scene nodes still grow with the instance count. Scene updates still visit existing nodes, so total batch placement does not have a linear-time guarantee.

Credit the assets

The 41 vehicles and six towns are CC BY 4.0. Shared renders and asset packages must carry their applicable credits, source links, license link and conversion notices. The code license does not replace the asset license.

The committed attribution catalog lists all 47 entries and credits the 11 CC0 entries as a courtesy. It is a complete catalog, not a list of assets proven present in one image.

The three CARLA gallery targets write a .credits.md file beside the output. Keep it with the gallery and its component images when sharing them. For a direct example run, generate that file yourself:

sh tools/tool.sh .venv/bin/mojo carla_assets credits --all --output out/carla.credits.md

After editing the manifest, update the committed catalog:

sh tools/tool.sh .venv/bin/mojo carla_assets credits --all --output assets/carla/ATTRIBUTION.md

tests/test_carla_assets_tool.mojo checks that the catalog still matches the manifest. Google Drive hosts the archives. See "Publish the converted archives".

Stage renders for sharing

Use stage-render to copy named render files into a new distribution directory:

sh tools/tool.sh .venv/bin/mojo carla_assets stage-render --output /tmp/carla-distribution out/carla.png out/carla_rgb.png

The directory contains only the named files, copied by basename, manifest.json, and ATTRIBUTION.md. The render files and manifest keep their exact bytes. The attribution uses the same manifest snapshot and includes all entries. This catalog does not prove which assets appear in an image.

For historical renders, select the manifest that corresponds to those renders:

sh tools/tool.sh .venv/bin/mojo carla_assets --manifest /path/to/historical-manifest.json stage-render --output /tmp/historical-distribution /path/to/render.png

Keep the source files unchanged while the command runs. The manifest and render inputs must be regular files, not symbolic links. The output directory must not exist, even if it would be empty. Its parent directory must already exist. Duplicate input files and case-insensitive basename collisions are refused. The names manifest.json and ATTRIBUTION.md are reserved, regardless of case.

The command checks every input before it creates the output directory. It never replaces an existing output file. A successful command returns zero and creates the complete distribution.

If staging fails after directory creation, the command returns an error with the output path. It retains all partial output and does not delete any directory entries. This preserves files another process adds or replaces during the failure. Inspect the retained directory and remove unwanted files manually. Use a new directory for the next attempt.

This command does not render, export, download, or inspect the asset cache. It does not change archive checksums or create sidecars beside source renders. The existing credits command is unchanged.

Stage archives for sharing

Use stage-archive to package the pinned archives of named entries with their metadata:

sh tools/tool.sh .venv/bin/mojo carla_assets stage-archive --output /tmp/carla-archives carla.vehicle.audi.a2 carla.vehicle.tesla.model3

The new directory contains these files:

  • Each archive of the named entries, at its manifest path. For example, carla/vehicles/vehicle.audi.a2.zip.
  • manifest.json, with the exact bytes of the selected manifest.
  • ATTRIBUTION.md, with the credits of the named entries only.

The attribution uses the same manifest snapshot as manifest.json. It gives the CARLA Team credit, the source release, the CC BY 4.0 link and the conversion changes.

The command reads each archive from the cache. Use --cache PATH before the command to select a different cache root.

The command hashes each archive before it creates the output directory. Each archive must have a pinned sum, and its bytes must match that sum. The command hashes the bytes again while it copies them. If the archive changes during the copy, the command fails. The command never pins a sum and never changes the manifest.

A recipient can use the directory as a cache root. This command verifies the archives and extracts their members without a download:

sh tools/tool.sh .venv/bin/mojo carla_assets --manifest /tmp/carla-archives/manifest.json --cache /tmp/carla-archives fetch --offline carla.vehicle.audi.a2 carla.vehicle.tesla.model3

The rules of stage-render also apply:

  • The manifest and each archive must be regular files, not symbolic links.
  • The output directory must not exist. Its parent directory must exist.
  • The command checks every input before it creates the output directory.
  • It never replaces an existing output file.
  • If staging fails after directory creation, the command keeps all partial output and reports its path.

The command also refuses these selections:

  • An unknown entry id, or an id that you name more than once.
  • An entry that has no archive.
  • An archive with no pinned sum, a missing archive, or an archive with different bytes.
  • Two output paths that differ only in case, and the names manifest.json and ATTRIBUTION.md.

This command does not download, extract, upload or host files. To publish the package, see "Publish the converted archives".

Publish the converted archives

The CARLA archives are on Google Drive. The owner chose Google Drive as the maintainer-controlled destination. Seth Kitchen is the authorized publisher. Only he uploads archives, changes their sharing, and approves a manifest URL.

The folder holds 47 archives: 41 vehicles and 6 towns, 968,072,352 bytes in all. The largest is Town10HD, 204,022,674 bytes. The hosting check gives the size, sharing and checksum result of each archive.

To publish an archive, the publisher does these steps:

  1. Put the exact pinned archive in the cache. A rebuilt archive can have a different checksum. Do not use a rebuilt archive in place of a pinned one.

  2. Package every archive that the distribution will share with its manifest and attribution. For a distribution with one archive:

    sh tools/tool.sh .venv/bin/mojo carla_assets stage-archive --output /tmp/carla-archives carla.vehicle.audi.a2

    The command fails when the archive bytes do not match the pinned sum.

  3. Upload the archives, manifest.json and ATTRIBUTION.md from the staged package to the same Google Drive folder. Keep each archive's file name. For example, vehicle.audi.a2.zip. Do not upload only the zip. Keep both companion files available with the downloadable archives. If the folder shares several archives, stage all their entry ids together. Do not replace the folder's attribution with credits for only one of its archives.

  4. Set the link sharing of every archive and both companion files to "Anyone with the link" and the role to "Viewer".

  5. Put the share link in the manifest. For a vehicle, use carla_export_manifest_vehicles --urls. For a town, use carla_export_manifest_towns --urls. See "Host the vehicle zips". Do not change the sum. After URL edits, generate the attribution and stage the complete distribution again in a new output directory. Replace the published companion files with manifest.json and ATTRIBUTION.md from that new package. Keep the exact pinned archive bytes.

  6. Download every published entry into an empty cache and verify each checksum. For the one-archive example:

    sh tools/tool.sh .venv/bin/mojo carla_assets --cache /tmp/carla-check fetch carla.vehicle.audi.a2
    sh tools/tool.sh .venv/bin/mojo carla_assets --cache /tmp/carla-check verify --strict carla.vehicle.audi.a2

    Both commands must return 0. The /tmp/carla-check directory must not exist before step 6. Also check that a signed-out recipient can download both companion files. Their bytes must match the staged manifest and attribution.

  7. Run check and the tool tests. Then commit the manifest and ATTRIBUTION.md together.

check refuses a CARLA entry, an id that starts with carla., that loses one of these:

  • The CC-BY-4.0 license, which credits writes with its link, https://creativecommons.org/licenses/by/4.0/.
  • The author CARLA Team (Computer Vision Center).
  • A source that is a CARLA release page, such as https://github.com/carla-simulator/carla/releases/tag/0.9.16.
  • A provenance note that names the same release, such as CARLA 0.9.16.
  • A changes note.
  • A pinned SHA-256 sum for each file.

The tool stops, and keeps no bytes, when Drive does not send the file:

  • An HTTP error, such as 403 for a file that is not shared, or 429 for too many requests.
  • A web page in place of the file, such as a sign-in page, a quota page or a virus-scan page. The tool asks for confirm=t, which skips the virus-scan page.
  • A proxy that refuses the connection. The tool does not try again.
  • Bytes whose sum differs from the manifest.

A dropped connection is tried again, up to five times.

The CARLA vehicles

The manifest has 41 vehicles from the CARLA 0.9.16 release. Each vehicle is one zip of about 1 to 8 MB, 125 MB in total. The zip holds a glTF file, its buffer and its textures. The model faces plus x, stands on y = 0 and is in meters.

Each model is simplified to about 35,000 triangles. CARLA's own models have 5,000 to 140,000. The renderer draws each triangle into the frame and into every shadow cascade, so this halves the time of a view with seven cars.

Each model tags some of its materials in its extras, as {"carla": "paint"}, "heads" or "tails". The town gives the paint the color of the blueprint's color attribute, as CARLA does. The head lamps and the tail lamps glow with the vehicle's light state.

The entry id is the blueprint id with the prefix carla., for example carla.vehicle.audi.a2. The binding table maps each blueprint id to its entry. It also maps the older names and the Unreal 5 names to the 0.9.16 vehicle that they name:

Blueprint id Entry
vehicle.dodge.charger carla.vehicle.dodge.charger_2020
vehicle.dodgecop.charger carla.vehicle.dodge.charger_police_2020
vehicle.taxi.ford carla.vehicle.ford.crown
vehicle.lincoln.mkz carla.vehicle.lincoln.mkz_2020
vehicle.mini.cooper carla.vehicle.mini.cooper_s_2021
vehicle.sprinter.mercedes carla.vehicle.mercedes.sprinter
vehicle.carlacola.actors carla.vehicle.carlamotors.carlacola
vehicle.firetruck.actors carla.vehicle.carlamotors.firetruck
vehicle.ambulance.ford carla.vehicle.ford.ambulance
vehicle.fuso.mitsubishi carla.vehicle.mitsubishi.fusorosa

CARLA 0.9.16 has no mining truck. vehicle.miningtruck.miningtruck uses the procedural vehicle.

The tree key is bound to null. The Poly Haven tree has 1.6 million triangles, which is too many to plant along a street.

Host the vehicle zips

The zips are too large to commit. Each one is a file in a shared Google Drive folder, and the manifest holds its share link and its sum. fetch downloads a zip, checks its sum and extracts it. A zip without a URL has the URL null, and fetch reports it as unhosted. You can put a zip at its cache path by hand, and the next fetch verifies it and extracts it.

To add the URLs after you rebuild the zips:

  1. Upload each zip to the Google Drive folder, as "Publish the converted archives" tells. Only the authorized publisher uploads.

  2. Write a JSON file that maps each zip name to its share link:

    {"vehicle.audi.a2.zip": "https://drive.google.com/file/d/FILE_ID/view?usp=sharing"}
  3. Write the URLs into the manifest:

    sh tools/tool.sh .venv/bin/mojo carla_export_manifest_vehicles --urls urls.json

The fetch tool changes a Google Drive share link into Drive's download link. When a host sends a web page in place of the file, the tool stops with an error. Drive does this for a file that is not shared, and for a file whose download quota is used up.

Rebuild the vehicle zips

CARLA's vehicles are Unreal Engine 4.26 assets. The scripts in assets/carla/tools/export/ change them to glTF. They run on Windows, because UE Viewer (UModel) is a Windows program. The zip and manifest tools are Mojo programs in tools/. Run them in the Linux environment, for example WSL, from the repository root.

  1. Make a work folder. Set CARLA_EXPORT_DIR to it.

  2. Put UModel build 1590 at umodel/umodel_64.exe.

  3. Extract CarlaUE4/Content/ from CARLA_0.9.16.zip into cooked/. You need Carla/Static/Car, Carla/Static/Vehicles, Carla/Static/GenericMaterials, Carla/Static/Dynamic and Carla/Blueprints/Vehicles.

  4. Run the export script on Windows:

    python export_vehicles.py
  5. Zip the packages and write the manifest. Set CARLA_EXPORT_DIR to the absolute path of the same work folder:

    sh tools/tool.sh .venv/bin/mojo carla_export_zip_packages
    sh tools/tool.sh .venv/bin/mojo carla_export_manifest_vehicles

export_vehicles.py reads each vehicle blueprint for the meshes it names. It exports the skeletal body and the static glass and lights, then merges them into one glTF file. carla_gltf_fix.py then does these steps:

  1. It rebuilds each material from the parameters that UModel writes beside the mesh. It uses the base color, normal and ORM maps, the car paint's color and translucent glass. It scales each texture to 1024 pixels or less.
  2. It removes the skin, because the town takes plain meshes. It removes Unreal's vertex colors, because they are masks and not colors.
  3. It splits the lamps into head lamps and tail lamps. CARLA's lamp material reads an eight-color mask, and UModel does not export what the colors mean. So a lamp in front of the model's middle is a head lamp, and a lamp behind it is a tail lamp.
  4. It simplifies the model to --budget triangles, 35,000 by default, one primitive at a time. The seams stay where they are, so the textures do not tear. The normals are made again from the simplified faces.

The simplification needs the fast-simplification Python package.

Use the cooked release, not the carla-content repository. The repository holds uncooked assets, and UModel cannot read an uncooked mesh.

The CARLA towns

Each town package holds one CARLA town as CARLA builds it. It has the town's buildings, streets, sidewalks, plants, poles, fences, props and parked vehicles, each where the town puts it. The packages are Town01, Town02, Town03, Town04, Town05 and Town10HD, as carla.town.<town>.zip.

A package is two files: one binary glTF file, carla.town.<town>.glb, with its buffer and its textures inside, and the town's OpenDRIVE map, <town>.xodr. The positions are in the renderer's frame, in meters: three.js x, y and z are CARLA's x, z and y. So the package goes at the origin, with no turn and no scale.

The meshes are merged by tile, by kind and by material. A tile is 32 meters square. Each node has two tags in its extras:

Tag Values
carla_kind building, road, road_line, sidewalk, ground, terrain, water, rail, wall, fence, vegetation, pole, traffic_light, traffic_sign, parked_vehicle, prop
carla_lod 0 for near, 1 for far

The near level keeps the detail that a camera sees close up. The far level keeps what shows at a distance. It leaves out the grass, the bushes, the props, the parked vehicles, the signs and the lamps' glass. A far tree is an impostor: two quads that cross on its trunk. Each quad shows a picture of the tree, drawn from its near level from the front or from the side, with its leaves cut out.

A far building is a baked proxy. Its corners are gathered into cells a 200th of its size, and the joined mesh is simplified. Its near level is drawn from each side and from above into one picture, and each far triangle shows the view that faces it. So its windows and its trim are in the picture, not in the geometry. The picture holds no light: the renderer lights the proxy.

A town's glass is a dark, smooth, partly metal pane that reflects its surroundings. A see-through window has nothing behind it, so a tower would show the sky through its windows.

The scene's extras list the head of each street lamp, as carla_lamps: three numbers for each lamp, in the renderer's frame. The lamps' glass has the material tag {"carla": "lamp"}. A renderer draws each tile at one level, chosen by the tile's distance to the camera.

Each mesh keeps a share of its triangles, by its kind. Each town also has a cap for each kind at each level. A town can have more than its cap, as Town04 has with its eight thousand pines. Then every mesh of that kind gives up the same share. A tree gives up leaf cards, and each card that stays grows, so the crown stays full. Other meshes are simplified, and their texture seams stay where they are.

Some textures in CARLA's content show a company's mark, a real person, a real institution or a poster of unclear origin. CARLA's license covers CARLA's own work, not these. Each one becomes one flat color, its average. NEUTRAL in carla_gltf_fix.py names them, and the vehicle packages use the same list.

A package names no engine and no engine path. build_towns.py stops with an error when it finds one.

Rebuild the town packages

The towns are made in three steps. The scripts run on Windows, like the vehicle scripts. The zip and manifest tools run in the Linux environment, from the repository root.

  1. Dump each town's layout from a running server:

    python dump_layout.py

    The Python API does not say which mesh each object uses, and UModel cannot read the cooked levels. So dump_layout.py runs CARLA with UE4SS, a scripting runtime, and a Lua mod, layout_dump/main.lua. The mod writes each static mesh component: its mesh, its materials, its transform and its instances. The server runs with -nullrhi, so it does not use the GPU. Each run stops after --limit seconds, 60 by default.

    Use UE4SS experimental build 3.0.1-1152 or later. Version 3.0.1 stops CARLA 0.9.16 at start.

  2. Export every mesh and material that the layouts name:

    python export_towns.py

    It reads the release's own CarlaUE4/Content, not cooked/. It does not export the engine's own content, which is licensed for use in the engine only. build_towns.py draws its own box for the engine's cube.

  3. Build and zip the packages:

    python build_towns.py [--top 20]

    Then, with CARLA_EXPORT_DIR set to the absolute path of the work folder:

    sh tools/tool.sh .venv/bin/mojo carla_export_zip_packages --packages town_packages --index towns.json
    sh tools/tool.sh .venv/bin/mojo carla_export_manifest_towns [--urls urls.json]

    carla_export_manifest_towns writes each town's entry and its binding, town.<town>. The URLs file is the same form as the vehicles'.

    --top lists the meshes that add the most triangles to each town.

The build needs the numpy, scipy, fast-simplification and Pillow Python packages.

Far buildings and tree impostors share a bake only when their ordered geometry and resolved materials match. The cache key and atlas name include the texture file contents, material parameters and bake settings. A component with different material overrides keeps its own far appearance. Each build starts a new cache. Its geometry, materials and texture files must stay fixed until that build ends.

Packaged texture names use a hash of the encoded image bytes. Two material overrides can share a color map with different masks without replacing each other's image. Source images with the same filename also stay distinct. Equal encoded images can share one packaged file.

Test the town exporter

The standard-library identity checks run with make test-tools. The synthetic bake checks need only NumPy and Pillow. They use two small meshes and two colors. They need no CARLA release, server or GPU.

Only mesh simplification is substituted. The tests use the real material reconstruction, texture conversion, bake rasterizers and GLB writer. They check the packed pixels, shared bakes and byte-identical repeated builds.

Use a separate test environment with Python 3.11 or later:

uv venv .venv-carla-export --python "$(command -v python3)"
uv pip install --python .venv-carla-export/bin/python --only-binary :all: "numpy==2.3.5" "Pillow==12.3.0"
.venv-carla-export/bin/python -m unittest discover -s assets/carla/tools/export -p 'test_build_towns.py'

The existing Linux and macOS lint jobs run this command. They keep the Mojo environment unchanged. A missing dependency or wheel fails the check.

Why not Poly Haven

Poly Haven has no CARLA vehicles or towns. CARLA's own models are in the CARLA release, in Unreal's format, under CC BY 4.0. The manifest still uses Poly Haven and ambientCG for the procedural town's textures and skies.

Verify extracted files

Run sh tools/tool.sh .venv/bin/mojo carla_assets verify to check the cache. For each verified archive, the tool also compares its extracted files with its members. A changed extracted file is reported as a mismatch. fetch refuses to overwrite it. Remove that file explicitly, then run fetch to restore the verified member. Do this after a package update if an old extracted file remains in the cache.

Decoded texture ownership

A preloaded registry owns each decoded image and its mip chain. Texture sets and materials made by TextureSet.dress share those payloads until a write. Each dressed material still gets separate texture IDs and independent repeat and sampling state. Albedo and linear-data decode keys remain distinct.

Call registry.clear_texture_cache() to release the registry's decoded-image shares and keys. Existing texture sets and dressed materials keep their bytes. The next preload reads the source files again. A write to a source set or one material's texture does not change another set, material, or cached image.

⚠️ **GitHub.com Fallback** ⚠️