Assets¶
The AssetManager loads and caches textures, sprite atlases, MSDF fonts, JSON,
text, and optional FBX model assets. Paths are resolved relative to
AppContext::assetRoot() (default assets) through the active
IAssetSource.
For a generated game, put content in its top-level assets/ directory and use
paths relative to that folder:
Snowpulse packages the same logical paths for desktop, web, iOS, and Android.
The provider details below matter when implementing custom streaming or
diagnosing platform packaging; ordinary game code can stay with
AssetManager.
IAssetStream provides seekable read, seek, tell, and size operations.
IAssetSource provides exists, open, readAll, and directory enumeration;
access the current provider with AppContext::assetSource(). Desktop, iOS, and
web use FileSystemAssetSource. Android uses AndroidAssetSource over
AAssetManager, so packaged files are read without extraction or duplicate
storage. For portable assets, use UTF-8 paths relative to the active source,
forward slashes, and exact filename casing. The built-in sources reject
absolute paths, backslashes, drive prefixes, and .. segments; custom sources
should enforce an equivalent logical-path boundary.
Textures¶
Texture2D loads image files into GPU textures. If you call loadSprite() with a name that is not in any atlas, the engine treats it as a standalone texture.
Use TextureLoadOptions when the default decode behavior is not appropriate:
snowpulse::AssetManager::TextureLoadOptions options;
options.srgb = true;
options.premultiplyAlpha = false;
options.flipVerticallyOnLoad = true;
auto* texture = assets->loadTexture("images/surface.png", options);
The defaults are linear color (srgb=false), premultiplied alpha, and vertical
flip on load. Options are part of the texture cache key, so the same file may
be cached separately with different decode settings.
Sprite Atlases¶
SpriteAtlas is loaded from JSON. The loader supports:
- TexturePacker JSON
- Free Texture Packer JSON
Atlas JSON can be either:
- frames as an object keyed by sprite name
- frames as an array of entries with filename
Use the atlas-qualified loadSprite(atlasPath, spriteName, applySize) overload
when sprite names may collide across atlases. The one-path overload first
checks sprites from already-loaded atlases, then treats the path as a
standalone texture:
auto* atlas = assets->loadAtlas("atlases/interface.json");
auto icon = assets->loadSprite(
"atlases/interface.json", "icon_confirm.png", false);
auto standalone = assets->loadSprite("images/standalone.png");
Fonts¶
Font loads MSDF font data using JSON metadata and a PNG atlas. The engine supports the common JSON layout produced by MSDF generators (including the bundled msdf-atlas-gen tools).
JSON¶
loadJson() parses JSON files into nlohmann::json.
Tiled maps are typically loaded through TiledRenderer, which internally uses the Tiled loader/runtime:
auto* node = scene()->createNode("map");
auto* tiled = node->addComponent<snowpulse::TiledRenderer>();
tiled->load("maps/layout.tmj");
scene()->rootNode()->addChild(node);
Text¶
loadText() loads any text asset as a raw std::string. Use it for data files
that are not JSON, such as dictionaries, message tables, or custom config
formats. loadJsonText() remains available as a compatibility wrapper for
existing JSON workflows.
Bytes, Streams, and Asset Sources¶
Use loadBytes() when the complete binary asset should be held in memory.
Use openAsset() for seekable access without requiring callers to know whether
the active source is a directory, application bundle, or packaged archive.
Missing and empty assets both produce an empty vector/string from the whole-file
helpers, while openAsset() returns nullptr when it cannot open the path.
const std::vector<std::uint8_t> bytes = assets->loadBytes("data/payload.bin");
if (auto stream = assets->openAsset("data/records.bin")) {
std::array<std::uint8_t, 16> header {};
const std::size_t readCount = stream->read(header.data(), header.size());
const std::int64_t cursor = stream->tell();
const std::int64_t byteCount = stream->size();
stream->seek(0, snowpulse::AssetSeekOrigin::Begin);
}
Install sources through AppContext so the asset and audio managers receive
the same provider. Passing nullptr restores a FileSystemAssetSource rooted
at the current assetRoot(). A custom provider implements exists(), open(),
and list(); readAll() is inherited and reads through the returned stream.
This example replaces the default provider with another filesystem-backed
source and enumerates one logical directory:
auto source = std::make_shared<snowpulse::FileSystemAssetSource>(
"alternate-assets");
context()->assetSource(source);
const bool hasConfig = source->exists("config/defaults.json");
const std::vector<std::string> entries = source->list("config");
const std::vector<std::uint8_t> configBytes = source->readAll(
"config/defaults.json");
FBX Models¶
loadFbx() imports static/skinned model geometry, materials, embedded or
referenced textures, skeleton data, and animation clips when the target uses an
Assimp-enabled engine variant:
When SNOWPULSE_HAS_ASSIMP=0, FBX loads report an error and return nullptr.
Application code normally uses FbxRenderer::load(), which delegates to the asset
manager and retains the cached model.
Rooted Loads and Queries¶
The rooted variants bypass assetRoot for absolute or pre-resolved paths:
loadTextureRooted()(with or withoutTextureLoadOptions)loadAtlasRooted()andloadSpriteRooted()loadFontRooted()loadJsonRooted()andloadJsonTextRooted()loadTextRooted()loadFbxRooted()
spriteNames() returns the cached atlas sprite names in sorted order.
clear() releases all asset-manager caches; callers must stop using returned
pointers before clearing.
Rooted methods are filesystem-only compatibility APIs. Do not use them for assets packaged in an Android APK/AAB. Normal texture, font, JSON, atlas, particle, audio, Spine, Effekseer, and Assimp loads use the source boundary. Android base-module assets report ready through the asset-pack API; Play Asset Delivery is deferred behind the same provider boundary.
For example, desktop tooling may open an explicitly resolved external file:
#if defined(SNOW_PLATFORM_DESKTOP)
auto* externalTexture = assets->loadTextureRooted(
"/absolute/path/to/reference.png");
const std::string externalText = assets->loadTextRooted(
"/absolute/path/to/notes.txt");
#endif
Optional Web Asset Packs¶
Asset packs let a web build render with a small set of boot assets and make the remaining files available later. They stage asset files only; C++ code and the core Wasm module are not split into packs.
The feature is controlled at build time:
emcmake cmake -S . -B cmake-build-web-lazy \
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DSNOWPULSE_WEB_LAZY_ASSET_LOADING=ON
cmake --build cmake-build-web-lazy --target sampleapp -j4
The option defaults to OFF. When it is off, the manifest can remain in the
project but Snowpulse uses the original eager packaging and startup behavior.
Native builds also compile the same API but report every pack as ready.
Application code may keep calling requestAssetPack() when the option is off. In an
eager or native build, every requested name is treated as Ready and its
callback is queued for the next engine update; it is not called synchronously
inside requestAssetPack(). This lets the same scene flow work in both modes.
CMake options are cached per build directory. Omitting
-DSNOWPULSE_WEB_LAZY_ASSET_LOADING defaults to OFF only in a fresh build
directory. If that directory was previously configured with ON, explicitly
reconfigure it with -DSNOWPULSE_WEB_LAZY_ASSET_LOADING=OFF or use a new build
directory.
Recommended Setup Workflow¶
Create assets/asset-packs.json beside the application's existing asset folders.
For the first smoke test, keep everything in boot:
Enable lazy loading and confirm the application still starts normally. This first manifest does not improve first-frame time, but it verifies that the build flag, manifest location, and packaging pipeline are correct.
Next, replace the broad ** boot rule with explicit groups and move one
self-contained folder at a time into a named pack:
{
"version": 1,
"boot": [
"atlases/interface.*",
"fonts/interface.*",
"images/boot/**"
],
"packs": {
"content": ["content/**"],
"audio": ["audio/**"]
}
}
Use these rules when deciding where files belong:
- Put the loading screen and everything used during application or first-scene
initialization in
boot. - Put a self-contained feature in a named pack when application code can wait for its
Readyresult before opening those files. - Every file not matched by
bootor a named pack automatically belongs to the reservedgamepack. Request it withrequestAssetPack("game"). bootandgamecannot be used as custom pack names.- Pack names accept lowercase letters, digits, hyphens, and underscores.
- Patterns are case-sensitive, relative to the asset root, use
/, and support*within one path segment and recursive**. - Physical folders and runtime paths do not change. Do not move files merely to put them in a pack.
Patterns may not overlap. When moving audio/** from boot to an audio pack,
remove the matching boot pattern in the same change.
Requesting a Pack¶
Request a pack from the scene that needs it:
assetManager()->requestAssetPack("content", [this](const auto& status) {
if (status.state == snowpulse::AssetPackState::Ready) {
_contentAtlas = assetManager()->loadAtlas(
"content/interface.json");
}
});
The current scene keeps running while the request is active. A Ready result
means the bytes are mounted at their normal /assets/... paths. Texture,
audio, font, and Spine decoding still happens through their existing APIs, so
only call those APIs after the pack is ready.
The same pattern applies to the automatic game pack:
assetManager()->requestAssetPack("game", [this](const auto& status) {
if (status.state != snowpulse::AssetPackState::Ready) {
return;
}
auto* deferredAtlas = assetManager()->loadAtlas("atlases/content.json");
audioManager()->playBgm("audio/ambience.ogg");
});
Poll status when a loading UI needs progress:
const auto status = assetManager()->assetPackStatus("content");
_loadingProgress = status.progress;
if (status.state == snowpulse::AssetPackState::Failed) {
_loadingError = status.error;
}
Use retryAssetPack() only after displaying or otherwise handling the
failure:
Duplicate requests made while a pack is loading are coalesced, but applications should normally request each pack once rather than call the method every frame. All registered completion callbacks run from the engine update loop before scene update. If a callback captures a scene or component pointer, that object must remain alive until the callback runs.
Deferred packs are processed one at a time in request order. Request the most important pack first. Packs stay mounted for the rest of the application; unloading is not supported in v1.
Output Behavior¶
For normal multi-file web applications, each deferred pack is emitted as a separate
.data file and is not downloaded before its request. For single-HTML
playables, deferred packs stay inside the HTML as inert base64 blocks and
begin incremental decoding only after the first rendered frame.
For single-HTML builds, packs improve when bytes are decoded and mounted; they
do not make the final HTML smaller or turn its contents into network
downloads. The post-build report shows the final HTML size and raw/base64 size
of every deferred pack. SNOWPULSE_WEB_UTF8_BINARY_ENCODING still applies
only to the core Wasm.
If lazy loading is enabled without a manifest, CMake warns and retains eager
packaging. Validate lazy single-HTML artifacts in every target ad-network
preview; switching SNOWPULSE_WEB_LAZY_ASSET_LOADING back to OFF is the
production fallback for a network that rejects the staged artifact.
Validation and Common Errors¶
The manifest compiler fails the build for unmatched globs, overlapping
assignments, invalid names, missing linked files, or loader-linked assets in
incompatible packs. It recognizes TexturePacker meta.image, MSDF font atlas
images, Spine atlas page textures, and the automatic Tiled JSON dependency
graph. For .tmj, .tsj, .tj, and recognized legacy .json exports, that
graph includes external tilesets, object templates, tileset/per-tile images,
image-layer images, and transitive references. A linked file may share its
owner’s deferred pack or live in boot; it may not live in a different
deferred pack. Tiled paths are resolved relative to the declaring file and
retain their full directory structure.
The Tiled graph itself is validated for every snowpulse_add_game() target,
including eager packages and native builds; the lazy-pack compiler adds the
pack-co-location rule. Run the reusable validator manually with
python3 snowpulse/tools/tiled_asset_graph.py validate --assets-dir <assets>.
Python 3 is therefore a build prerequisite for game targets containing Tiled
JSON (including recognized compatibility .json exports).
Keep independently paired assets that application code opens separately, such
as a Spine skeleton and atlas or a path stored in a Tiled file property,
together with a folder glob. They are not automatic loader dependencies.
Common setup errors:
- The application loads a deferred file during scene initialization. Keep that file
in
boot, or move the load call into the pack'sReadycallback. - An atlas descriptor and its texture are assigned to different packs. Group the complete atlas folder or export separate source atlases.
- A Tiled map, tileset, template, or referenced image is assigned to a
different deferred pack. Put its complete automatic dependency graph in one
pack, or put shared sources in
boot. - A Tiled reference uses an absolute path, a backslash, a URI, or escapes the
asset root. Re-export it as a portable
/-separated relative path. - A glob matches no files because its capitalization is wrong. Asset-pack matching is case-sensitive on every platform.
- A file is matched by both
bootand a named pack. Remove one assignment; overlaps intentionally fail the build. - The manifest exists somewhere other than the asset root. It must be exactly
<ASSETS_DIR>/asset-packs.json.
Sample Usage¶
context()->assetRoot("assets");
auto* assets = context()->assetManager();
// Atlas-qualified and cached-atlas sprite lookup.
assets->loadAtlas("atlases/interface.json");
snowpulse::SpriteRegion button = assets->loadSprite(
"atlases/interface.json", "button_primary.png");
snowpulse::SpriteRegion icon = assets->loadSprite("icon_confirm.png");
// Standalone texture and sprite.
auto* texture = assets->loadTexture("images/background.png");
snowpulse::SpriteRegion logo = assets->loadSprite("branding/logo.png");
// Font, parsed JSON, raw JSON text, arbitrary text, and binary data.
snowpulse::Font* font = assets->loadFont("fonts/interface.json");
auto settings = assets->loadJson("config/settings.json");
std::string rawJson = assets->loadJsonText("config/settings.json");
std::string terms = assets->loadText("data/terms.txt");
std::vector<std::uint8_t> bytes = assets->loadBytes("data/payload.bin");
// Cached atlas names and an optional FBX model.
auto names = assets->spriteNames();
auto* model = assets->loadFbx("models/character.fbx");