Asset

Load images, sounds, shaders, models, fonts, and arbitrary files. Three entry points: GetAsset (project bundle), ImportAsset (anywhere on disk), and FromString / FromPixels (memory).

local Asset = import("Asset")

local logo = Asset.GetAsset("Image", "ui.logo")         -- assets/ui/logo.png
local mesh = Asset.GetAsset("Model", "props.crate")     -- assets/props/crate.fbx
local snd  = Asset.GetAsset("Sound", "sfx.explode")

Asset kinds

KindReturnsExtensions probed
"Image"ImageAsset.png .jpg .jpeg .bmp .gif .webp
"Sound"SoundData.ogg .mp3 .wav .flac
"Shader"ShaderAsset.shader .glsl .wgsl .hlsl .vert .metal
"Fragment"FragmentAsset.frag .fragment .fs .glslf
"Model"ModelAsset.obj .fbx
"Font"FontAsset.ttf .otf
"File"stringany, raw UTF-8 contents

Path forms

"foo.bar.baz" Same package as the caller. Dots are folder separators , resolves to assets/foo/bar/baz.
"@PkgId/foo/bar" Cross-package lookup. Reads from another loaded .managed package by id.
"foo/bar.png" Literal extension. Loader uses it directly instead of probing the kind's extension list.

Asset API

Asset.GetAsset(kind, path) -> kind-specific

Load from your project's assets/ folder (during ruzit test) or the bundled .managed package (in a built game). Cached, the same path returns the same instance on subsequent calls.

Asset.FromString(kind, data, label?) -> kind-specific

Build an asset from a raw byte string. label is cosmetic (shown by :Source()). Useful for assets fetched over the network or assembled procedurally.

Asset.ImportAsset(kind, path) -> kind-specific

Load from any disk path, mod folders, Workshop items, user profile. Pass "Auto" or "" as the kind to infer it from the file extension.

Asset.FromPixels(width, height, rgba) -> ImageAsset

Build an ImageAsset from raw RGBA8 pixels. rgba must be exactly width * height * 4 bytes.

Bulk loading and unloading

Three companion APIs for managing groups of assets together: warm a cache before a level, block-load a manifest at a checkpoint, and forcefully unload everything when leaving the level. All three accept entries in "Kind:path" form (e.g. "Image:ui.logo", "Sound:sfx.boom", "Model:props.crate") so a single list can mix kinds.

The cache is process-wide: keyed by resolved target package + kind + path, not by which script made the call. Any script can preload, look up, or drop any asset, and the same userdata instance is shared across the whole game. Same-named bare paths in different packages stay separate (the resolved package id is part of the key), and "@PkgId/..." paths always name the explicit package.

Asset.PreloadAsync(entries) -> Signal<>

Kick off background reads (and zstd decompression for bundled packages) on a worker thread. Final parsing into the typed userdata happens on the next heart tick on the main thread, the returned signal fires once when every entry is ready. Cached after that, future Asset.GetAsset calls hit the cache without disk / package access.

Asset.BulkLoad(entries) -> { [string]: any }

Synchronous version. Blocks the caller until every asset is decoded, then returns a dictionary keyed by the original "Kind:path" string. Same cache-population effect as PreloadAsync, you also get the values directly without a follow-up GetAsset.

Asset.Drop(entry)

Forcefully unload a previously-loaded asset and destroy every live consumer of it. Pass the same "Kind:path" string used to load it. After Drop the next GetAsset for that path genuinely re-decrypts and re-decompresses from disk.

Asset.GetVideo(path) -> (Video, SoundData?)

Load a video plus its companion audio in one call. Always returns two values, the second is nil for sources that don't carry audio (animated GIFs). Hand the SoundData to SFX.LoadSound if you want it to play. Supported formats: .gif (no audio) and .ruzitvid (Ruzit's bundled- video format with frames + audio). See Video for usage.

-- Background-warm a level's assets while the loading screen plays.
local ready = Asset.PreloadAsync({
    "Image:levels.forest.skybox",
    "Image:levels.forest.terrain",
    "Sound:music.forest_theme",
    "Model:levels.forest.cabin",
})
ready:Connect(function() loading_screen:Hide() end)

-- Or block-load at a checkpoint.
local bag = Asset.BulkLoad({
    "Image:ui.menu_bg",
    "Font:ui.title",
})
local title = GUI.Basic.Font(bag["Font:ui.title"])

-- Leaving the forest level: free everything.
Asset.Drop("Image:levels.forest.skybox")
Asset.Drop("Image:levels.forest.terrain")
Asset.Drop("Sound:music.forest_theme")
Asset.Drop("Model:levels.forest.cabin")
Drop is destructive for content assets, detach-only for shaders. Content assets (Image, Sound, Model, Font) take their consumers with them. Shader assets only get detached, the parts and primitives that had them attached keep rendering with the engine default. For content assets, hold onto Lua references at your own risk, the underlying GPU / audio resources have already been released.

:Free() — per-instance free

Every asset userdata (ImageAsset, SoundData, ShaderAsset, FragmentAsset, ModelAsset, FontAsset) exposes a :Free() method. It does the same engine-side work as Asset.Drop, but you call it on the handle directly, no path required:

Effects are identical to Drop: every BasePart / Primitive / Sound currently using the asset is detached or destroyed, and the asset is removed from the cache. The only difference is that :Free() doesn't assume the asset came from disk, so the local Lua variable in your hand keeps pointing at the same userdata. The engine just stops tracking it. Memory is reclaimed once nothing in Lua holds it either.

-- Generate a procedural texture, use it, then free engine refs.
local noise = Asset.FromPixels(256, 256, my_rgba)
part.Texture = noise
-- ...later...
noise:Free() -- detaches part.Texture, drops cache entry

-- Same effect on a file-loaded asset, no path string needed.
local mesh = Asset.GetAsset("Model", "levels.forest.cabin")
mesh:Free()

ImageAsset

img:Width()-> number
img:Height()-> number
img:Source()-> string

Origin string, file path or "<pixels:WxH>" for in-memory variants.

img:Pixels()-> string

Raw RGBA8 buffer (Width * Height * 4 bytes). Useful for inspection or hand-rolled image processing.

img:Free()

See :Free(). Detaches every Part / Primitive textured with this image and drops it from the cache.

ModelAsset

model:VertexCount()-> number
model:TriangleCount()-> number
model:Source()→ string
model:LocalBounds()→ { Min, Max, Size, Center }

AABB of the raw vertex positions in mesh-local space — BEFORE part.Size / part.CFrame is applied. Each field is a Vector. Exported model units vary wildly (some FBX exports are ±1, some are ±100), so use this to size DistortionBox volumes or DynMesh:Weld anchors against the mesh's actual extents instead of guessing.

local b = model:LocalBounds()
-- Cover the entire mesh with one box, centred where the mesh actually is.
Renderable.DistortionBox(part, CFrame.new(b.Center), b.Size * 1.05)
model:Free()

See :Free(). Destroys every BasePart spawned from this model (BaseModel / SimplifyMesh output) and drops it from the cache.

Models carry geometry only as of 1.2.8 — animation clips are loaded separately as an AnimationSet and bound to parts via part:GetAnimatedTrack(animation).

AnimationSetAsset

A bundle of vertex-deformation animation clips extracted from an FBX file. Models and animations live in separate assets so one FBX of clips (e.g. walk, run, jump) can drive any number of meshes that match the imported skeleton's vertex layout. Load with Asset.GetAsset("AnimationSet", "Animations/Walk.fbx").

local Asset = import("Asset")
local Renderable = import("Renderable")

local mesh   = Asset.GetAsset("Model", "chars.hero")
local anims  = Asset.GetAsset("AnimationSet", "chars.hero_anims")
print(anims:ListAnimations())  -- { "Idle", "Walk", "Run", ... }

local walk = anims:GetAnimation("Walk")

local hero = Renderable.BaseModel(mesh)
local track = hero:GetAnimatedTrack(walk)
track.Looped = true
track:Play()

-- Bind the same Animation to a second part -- they tick independently.
local sidekick = Renderable.BaseModel(mesh)
sidekick:GetAnimatedTrack(walk):Play()
set:ListAnimations()→ { string }

Names of every clip in the set, in FBX import order.

set:GetAnimation(name)→ Animation

Pull a single named animation. Errors if the name doesn't match. The returned Animation is shareable — pass it to part:GetAnimatedTrack(animation) on as many parts as you want; each call builds a fresh playback track using the same source keyframes.

set:Count()→ number
set:Source()→ string
set:Free()

See :Free(). Drops the set from the asset cache. Existing Animation handles (which hold their own Arc on the keyframes) keep working until they themselves are released.

Animation

A single named clip pulled out of an AnimationSetAsset. Cheap to clone, safe to share across parts. Bind with part:GetAnimatedTrack(animation) to get a playable AnimationTrack.

animation:Name()→ string
animation:Duration()→ number

Length of the clip in seconds.

animation:KeyframeCount()→ number
animation:Source()→ string

SoundData

snd:Source()-> string
snd:ByteCount()-> number

Encoded byte length (NOT decoded sample count). Decoding happens on SFX.LoadSound.

snd:Free()

See :Free(). Stops every Sound backed by this data; subsequent :Play() errors.

FontAsset

font:Source()-> string
font:Free()

See :Free(). Drops every Text Primitive using this font and clears the cache.

Pass to GUI.Basic.Font(font) to create a Text primitive. Glyphs are rasterised at runtime sized by the primitive's .TextSize.

ShaderAsset

Opaque handle wrapping a vertex-stage WGSL program. Load with Asset.GetAsset("Shader", path) and pass it to a shader- accepting method like part:AttachShader(shader) or GUI.SetSkybox(shader).

Most projects never override the vertex stage; the engine's default handles transform / projection / interpolation. See Shaders for the WGSL contract.

shader:Source()-> string
shader:Free()

See :Free(). Detaches this shader from every Part / Primitive / scene hook and drops it from the cache. Detach-only, the consumers keep rendering with the engine default.

FragmentAsset

Opaque handle wrapping a fragment-stage WGSL program. Loaded via Asset.GetAsset("Fragment", path) and passed to part:AttachShader(asset), primitive:AttachShader(asset), GUI.SetSkybox(asset), or GUI.SetPostEffect(asset).

frag:Source()-> string
frag:Free()

See :Free(). Detach-only, like ShaderAsset:Free(); clears any skybox / post-effect slot it was bound to as well.

This is where ~all custom shader work lives, surface lighting, dissolve effects, stylised lighting, scrolling textures, post effects. See Shaders for the prelude, parameter declarations, and helpers.