Defining Bundle Visuals
Defining Bundle Visuals
Visual definitions describe how Selene turns bundle assets into renderable client visuals. They live in client-side bundle data, usually next to the textures they reference, and are loaded into the visuals registry.
Use this guide when you are creating a new bundle that provides graphics, adding visuals for tiles or entities, or overriding existing visuals in a resource pack.
Where visuals live
Visuals are client data. Put them under client/data/<namespace>/, where <namespace> becomes the namespace part of each visual identifier.
You can define multiple visuals in one registry file:
my-bundle/
bundle.json
client/
data/
myworld/
visuals.json
textures/
myworld/
terrain/
grass.png
Or split each visual into its own JSON file:
my-bundle/
client/
data/
myworld/
visuals/
terrain/
grass.json
The merged visuals.json form stores entries by local name. This defines myworld:terrain/grass:
{
"entries": {
"terrain/grass": {
"type": "simple",
"texture": "client/textures/myworld/terrain/grass.png"
}
}
}
The split-file form uses the file path as the local name. For example, client/data/myworld/visuals/terrain/grass.json also defines myworld:terrain/grass.
Visual identifiers
Visual identifiers use the same namespace:name format as other Selene registries:
myworld:terrain/grass
myworld:characters/guard
illarion:tile_cursor
The namespace comes from the directory below client/data/. The name comes from either the key inside visuals.json or the relative path of a file below visuals/.
Use stable, descriptive names. Tiles, entities, and scripts refer to these identifiers, so renaming a visual means updating every reference to it.
Texture paths
Texture paths are bundle-relative paths, such as:
client/textures/myworld/terrain/grass.png
The path is resolved from the loaded bundle files. If you are overriding an existing texture in a resource pack, keep the same internal path as the original bundle. If you are defining a new visual, choose a path that matches your bundle's own texture layout.
Simple visuals
Use simple for one static texture.
{
"type": "simple",
"texture": "client/textures/myworld/terrain/grass.png",
"offsetX": 0,
"offsetY": 0,
"surfaceOffsetY": 0,
"sortLayerOffset": 0
}
Common fields:
textureis the PNG or other supported texture asset to draw.offsetXandoffsetYmove the texture relative to its render position.surfaceOffsetYraises the visual surface for isometric stacking.sortLayerOffsetadjusts render order when the default sort is not enough.flipXandflipYmirror the texture.metadatastores custom data that systems or Lua code can inspect.
Most numeric and boolean fields have defaults in the engine, so you can omit them when the default value is correct:
{
"type": "simple",
"texture": "client/textures/myworld/terrain/grass.png"
}
Variant visuals
Use variants when several textures are equivalent alternatives. Selene picks a variant from the render context, which is useful for repeating terrain where identical tiles should not all look the same.
{
"type": "variants",
"textures": [
"client/textures/myworld/terrain/grass_0.png",
"client/textures/myworld/terrain/grass_1.png",
"client/textures/myworld/terrain/grass_2.png"
]
}
Variant visuals support the same offset, sorting, flipping, and metadata fields as simple visuals.
Animated visuals
Use animated for a fixed sequence of frames.
{
"type": "animated",
"textures": [
"client/textures/myworld/effects/sparkle_0.png",
"client/textures/myworld/effects/sparkle_1.png",
"client/textures/myworld/effects/sparkle_2.png"
],
"duration": 0.1,
"instanced": true
}
duration is the frame duration in seconds. If instanced is false or omitted, Selene may share the animation state between visuals with the same identifier. Set instanced to true when each visual should animate independently.
Animated visuals support the same offset, sorting, flipping, and metadata fields as simple visuals.
Animator visuals
Use animator when a visual has named animations controlled by an animator component, such as idle, walk, attack, or death.
{
"type": "animator",
"animator": "character",
"offsetX": 0,
"offsetY": -24,
"animations": {
"idle": {
"textures": [
"client/textures/myworld/characters/guard_idle_0.png",
"client/textures/myworld/characters/guard_idle_1.png"
],
"duration": 0.2
},
"walk_south": {
"textures": [
"client/textures/myworld/characters/guard_walk_south_0.png",
"client/textures/myworld/characters/guard_walk_south_1.png"
],
"duration": 0.08
}
}
}
Each animation entry has:
texturesfor the animation frames.durationfor frame duration in seconds. If omitted, Selene uses a fast default.offsetXandoffsetYto override the base visual offsets for that animation.flipXandflipYto mirror that animation's frames.
Animator visuals require a render context with an animator controller. They are usually attached through entity rendering components rather than created as standalone decorative visuals.
Text visuals
Use text for a visual backed by rendered text instead of a texture.
{
"type": "text",
"text": "Hello",
"align": "CENTER"
}
align can be LEFT, CENTER, or RIGHT. Entity visual components can override the text value through their visual creation context.
Referencing visuals from data
Bundle data refers to visuals by identifier. For example, a tile definition can point at a visual:
{
"entries": {
"terrain/grass": {
"visual": "myworld:terrain/grass"
}
}
}
Entity definitions and components follow the same pattern when they need a client visual. Keep the visual identifier in client-safe data when possible, and keep server-only gameplay data separate from client assets.
Creating visuals from Lua
Client Lua can create a visual by identifier through selene.visuals:
local visuals = require("selene.visuals")
local cursor = visuals.Create("illarion:tile_cursor")
This is useful for UI cursors, overlays, and custom client effects. For tiles and entities, prefer data-driven visual references so reloads and resource-pack overrides remain straightforward.
Resource pack overrides
Resource packs can override visuals in two ways:
- Replace the texture file at the same internal path.
- Replace the visual definition JSON and point it to a different texture or change offsets, animation frames, or sorting.
For example, to override myworld:terrain/grass from a merged registry file, mirror the original path:
global_client_bundles/
my-resource-pack/
client/
data/
myworld/
visuals.json
If the original visual was split into client/data/myworld/visuals/terrain/grass.json, mirror that file path instead.
See Making Resource Packs for the broader override workflow.
Checklist
- Put visual definitions under
client/data/<namespace>/visuals.jsonorclient/data/<namespace>/visuals/. - Put referenced texture assets under
client/textures/. - Use
simplefor static textures,variantsfor interchangeable texture choices,animatedfor fixed frame loops,animatorfor named controlled animations, andtextfor rendered text. - Keep visual identifiers stable once other data references them.
- Tune
offsetX,offsetY,surfaceOffsetY, andsortLayerOffsetin game, because these fields depend on the asset's pixel dimensions and the intended isometric footprint. - Test reload behavior with
F5orhotReload=truewhen iterating on bundle assets.