Lysa  0.0
Lysa 3D Engine
Assets Pack & Resources Pack

Lysa provides two complementary binary file formats for shipping game data:

  • Assets Pack (.assets) : a self-contained scene file that bundles meshes, materials, textures, and an optional animation library exported from a 3D tool such as Blender.
  • Resources Pack (.rpack) : a flat archive that replaces the entire app:// virtual file system at runtime, bundling shaders, scripts, fonts, and any other game resource into a single distributable file.

Both formats are read-only at runtime and accessed through the same lysa::VirtualFS API as plain files, so no loading code changes when switching between loose files and packed assets.


Assets Pack (.assets)

Overview

An assets pack is a binary file that represents a complete 3D scene or a reusable mesh library. It is designed for fast, copy-minimal GPU upload:

  • Binary, no deserialization : all structs are read directly into memory without text parsing.
  • BCn-compressed images : colour textures are pre-compressed to BC1/BC3/BC7, reducing VRAM usage and upload bandwidth.
  • Single staging buffer : all images are uploaded to the GPU in one transfer pass with no intermediate CPU atlas copy.
  • Pre-calculated mip levels : all mip chains are generated offline; the GPU receives ready-to-sample data.
  • Pre-calculated transforms : world transforms are baked at export time.
  • Pre-calculated animations : keyframe values are expressed relative to the original object, ready to apply at playback time.

Creating an assets pack

Assets packs are produced from glTF files by the gltf2lysa command-line converter, which is typically invoked automatically by the Lysa Blender add-on (tools/blender/blender_lysa_addon.py).

From Blender, the add-on exports the scene as a .glb file and then calls:

gltf2lysa -v -t <threads> -f <format> scene.glb scene.assets
Argument Description
-v Verbose output
-t <n> Number of parallel compression threads (0 = auto-detect)
-f <format> BCn compression format for colour textures: bc1, bc2, bc3, or bc7 (default)

The resulting .assets file is placed in the project's res/models/ directory and referenced via the app:// URI scheme.

Loading an assets pack

Call the template function lysa::AssetsPack::load with your node types. It returns a shared pointer to a synthetic root node containing the full scene tree.

The three template parameters let the engine instantiate the correct node classes for your scene graph layer:

// Define a thin wrapper in your SceneInstance module:
std::shared_ptr<SceneInstance> load(const std::string& fileURI) {
SceneInstance, // base node type
SceneMeshInstance, // mesh node type
DummyAnimationPlayer // animation player type
>(fileURI);
}
// Then in your scene constructor:
const auto model = load("app://res/models/sponza.assets");
root.addChild(model);
addInstance(root);
Note
The Lysa Nodes library provides a Node-bases scene graph with assets pack loading and animation support.

The lysa::AssetsPack::load function:

  1. Opens the file through the VFS (transparent to whether a resources pack is active).
  2. Reads all headers from the contiguous header block.
  3. Uploads all BCn images to the GPU in a single transfer command via AsyncQueue.
  4. Creates lysa::StandardMaterial, lysa::Mesh, and lysa::MeshInstance objects in the engine resource pools.
  5. Rebuilds the node tree from the stored parent/child index arrays.
  6. If animations are present, creates an AnimationLibrary and attaches an AnimationPlayer node to the root.
  7. Waits for the async transfer to complete before returning.
Note
Since the lysa::AssetsPack::load function waits for the transfer to complete, it is preferable to use it in a separate thread.

File format reference

Header global header (magic, version, counts, header block size)
array<ImageHeader + array<MipLevelInfo>> one entry per image, followed by its mip level descriptors
array<TextureHeader> sampler parameters for each texture slot
array<MaterialHeader> PBR material properties (albedo, metallic, roughness…)
array<MeshHeader + array<SurfaceInfo>
+ array<DataInfo>> mesh geometry descriptors
array<NodeHeader + array<uint32>> scene tree nodes with child index lists
array<AnimationHeader + array<TrackInfo>> animation clips with per-track keyframe descriptors
uint32 + array<uint32> index buffer
uint32 + array<float3> positions buffer
uint32 + array<float3> normals buffer
uint32 + array<float2> UV coordinates buffer
uint32 + array<float4> tangents buffer
per-track keyframe data times (float[]) + values (float3[]) per animation track
BCn image data compressed image blobs, one per image entry

Resources Pack (.rpack)

Overview

A resources pack replaces the app:// virtual file system at runtime. When configured, every VFS read : shaders, fonts, scripts, .assets files, images : is transparently served from the pack instead of the host file system. The application code does not change at all.

The format is a simple flat archive: a fixed binary header, a directory of path/offset/size entries, and a data blob containing the raw resource bytes.

Magic : "LYPACK" (6 bytes)
Version : uint32
Count : uint32
─────────────────────────────────────
Directory [Count entries]
path : char[256] virtual path (e.g. "shaders/forward.frag.spv")
offset : uint64 byte offset into the data blob
size : uint64 byte size of the resource
─────────────────────────────────────
Data blob
raw resource bytes

Creating a resources pack

Resources packs are built with the rpack_builder command-line tool, which reads a plain-text file list and writes a .rpack binary:

rpack_builder -o <output.rpack> -l <filelist.txt> [-b <base_dir>] [-v]
Option Description
-o, --output Path of the .rpack file to create (required)
-l, --list Text file containing resource paths, one per line, relative to --base (required)
-b, --base Base directory on disk that all listed paths are relative to (default: .)
-v, --verbose Print each resource being added and a final size summary

Lines starting with # and empty lines in the file list are ignored. Paths must be shorter than 256 characters.

Step 1 : generate the file list with the provided Python helper tools/gen_pack_list.py:

# From the project build / output directory:
python3 tools/gen_pack_list.py > filelist.txt

gen_pack_list.py walks the lib/, shaders/, and res/ directories and emits one path per line for every file matching the extensions .lua, .spv, .dxil, .jpg, .png, .json, .ttf, and .assets. Edit the script or write your own if your project uses a different layout.

Step 2 : build the pack:

rpack_builder --output game.rpack --list filelist.txt --base . --verbose

Verbose output lists every resource added and prints a final summary:

Base directory : /project/build
File list : filelist.txt
Output : game.rpack
Adding 142 resource(s):
+ shaders/forward.frag.spv (41240 bytes)
+ shaders/gbuffers.frag.spv (12708 bytes)
+ res/models/sponza.assets (48302156 bytes)
...
Pack written: game.rpack
Resources : 142
Total size: 56.12 MiB (58843648 bytes)

Activating the resources pack at runtime

Set VirtualFSConfiguration::appPackFile in the engine configuration before constructing lysa::Lysa:

config.virtualFsConfiguration.appPackFile = "game.rpack"; // path to the pack file
engine.run();

When appPackFile is set, every app:// read is transparently redirected to the pack. The pack is opened once at startup; reads stream directly from the file via ResourcesPackStreambuf without loading resources into a temporary CPU buffer.

When appPackFile is empty (the default), the VFS reads from the host file system relative to VirtualFSConfiguration::appDirectory (defaults to .), which is the normal workflow during development.

Streaming from a pack

ResourcesPack exposes a standard stream interface through openStream(), which returns a ResourcesPackStreambuf backed by a bounded region of the pack file. This is the same interface used internally when loading assets packs from within a resources pack:

// The VirtualFS handles this automatically, but you can also use it directly:
auto streambuf = pack.openStream("res/models/sponza.assets");
std::istream stream(streambuf.get());
// … read from stream …

The streambuf uses a 4 KB read buffer by default and seeks the underlying file on demand, so large resources are never fully loaded into memory unless the caller explicitly reads them all.

Checking pack contents

// Test whether a resource exists
if (pack.contains("shaders/forward.frag.spv")) { … }
// Test whether any resource lives under a path prefix
if (pack.containsPrefix("res/models/")) { … }
// Load a resource into a buffer
std::vector<char> buffer;
pack.load("res/fonts/Signwood.ttf", buffer);

Typical workflow

Development : use loose files for fast iteration:

project/
shaders/ ← compiled .spv / .dxil
res/
models/ ← .assets files exported from Blender
fonts/
// No pack configured : reads from disk
// config.virtualFsConfiguration.appPackFile is empty by default

Distribution : bundle everything into a single resources pack:

cmake --build build # compile shaders, build game
python3 tools/gen_pack_list.py > filelist.txt # generate the file list
rpack_builder -o game.rpack -l filelist.txt -b . -v # build the pack
// Ship with pack enabled
config.virtualFsConfiguration.appPackFile = "game.rpack";

No other code changes are needed: the VFS, AssetsPack::load, and all resource managers work identically in both modes.