Creating themes - GeekRhapsody/OdysseyLauncher GitHub Wiki

Creating themes

A theme changes how Odyssey Launcher looks: the background, the lighting, the menus' colours and font, the 3D card for each system, and the 3D model each game is shown on (a case, a cartridge, a television, an arcade cabinet...). This page explains where themes go, how to write one, and how to build the two kinds of model a theme can contain:

  • System models (also called cards): what each system looks like on the systems screen.
  • Game templates: the model every game of a system is drawn on, with the game's artwork wrapped onto it.

The full reference is docs/THEMING.md in the repository. This page is the practical version, and the exact names and limits here are the ones to follow.

Contents

  1. Where themes go
  2. The smallest possible theme
  3. Theme folder layout
  4. The manifest, theme.toml
  5. Model rules that apply to everything
  6. Material names
  7. Animation names
  8. Making a game template
  9. Making a system model
  10. Keeping models simple: limits and advice
  11. Exporting from Blender
  12. Testing
  13. Troubleshooting

Where themes go

Each theme is a folder. The folder's name is the theme's id (lower-case letters, digits, _ and -). Put it in the themes folder of the launcher's config folder:

Install Themes folder
Normal %APPDATA%\OdysseyLauncher\themes\<id>\
Portable (an empty portable.txt beside OdysseyLauncher.exe) userdata\themes\<id>\, next to the executable

So a theme called neon-arcade has its manifest at %APPDATA%\OdysseyLauncher\themes\neon-arcade\theme.toml.

Then pick it:

  • Settings: press Menu on a controller (or Escape on the keyboard) on the systems screen, then choose it under Theme. It applies straight away and is remembered.

  • T on the keyboard switches to the next theme until the launcher closes, which is handy for a quick look while you work.

  • Or edit settings.toml yourself:

    [display]
    theme = "neon-arcade"

Things to know:

  • The launcher's own themes (console, slab and the hidden base theme memory-card) live in the themes folder beside OdysseyLauncher.exe. A theme of yours with the same id as console or slab replaces it. memory-card is reserved: a user theme with that id is ignored.
  • A good way to start is to copy the sample theme, samples/themes/retro-tv, into your themes folder and rename the copy. It has a CRT television as a game template, a console as a system model, and animation clips.

The smallest possible theme

Every theme builds on the hidden base theme, Memory Card. Whatever your theme leaves out comes from it: the background and lights, the colour of each system, the card for each system, a game template for each system, the menus (the launcher's own deep blue, and its font), and how much the grid's items sway and bob. So this is a valid theme:

name = "My theme"

Make it your own a piece at a time. Memory Card's game templates are free for you to use by name, so you can restyle the look without modelling anything:

dvd_case, jewel_case, psx_jewel_case, cartridge_box, gameboy_box, generic_box_spine, generic_box_top_spine, generic_box_logo, snes_box_horizontal, snes_box_vertical, clamshell, megadrive_clamshell, umd_case, switch_case, arcade_cabinet, crt_tv_90s, flat_cover, flat_screenshot.

name = "Dark mode"

[look.background]
top_left     = "#101018"
top_right    = "#101018"
bottom_left  = "#000000"
bottom_right = "#08080C"

[defaults]
game_template = "flat_cover"      # every system without its own template shows plain covers

[ui.menu]
background = "#141414"            # dark grey menus, as the Slab theme has
text = "#ECECEC"
accent = "#C8C8C8"

Theme folder layout

neon-arcade/                      the folder name is the theme's id
  theme.toml                      the manifest (the only required file)
  models/
    templates/crt_tv.glb          game templates
    systems/console.glb           system models
  logos/
    ps2.png                       a system's image for its card, named after the system's id
  videos/
    ps2.mp4                       a video a system's card plays on its screen while it's focused
  • Only theme.toml is required. Put models in whatever folders you like; the manifest names each one by a path relative to the theme folder, with / separators. A path can't leave the folder.
  • Models are glTF 2.0 binary (.glb) only. Convert anything else first.
  • logos/ is the one folder found by name. An image there called after a system's id (logos/ps2.png, .jpg, .jpeg or .webp) is that system's logo, for its card. The ids are the ROM folder names, plus favourites, recently_played and all_games for the Favourites, Recently played and All games cards.

The manifest, theme.toml

Here is a manifest using most of what exists. Every key is optional apart from name, and a mistake never stops the launcher: each problem is reported with its file, line and key, and the launcher uses the next thing in line.

format = 1
name = "Neon Arcade"
author = "Your name"
look_transition_ms = 300                 # cross-fade between looks, 0 to 5000

[look.background]                        # one colour per screen corner, all four required
top_left     = "#1B1F4A"
top_right    = "#1B1F4A"
bottom_left  = "#04040C"
bottom_right = "#0B0B24"

[look.ambient]
colour = "#303038"
energy = 1.0                             # 0 to 16

[[look.lights]]                          # 1 to 3 directional lights
direction = [-0.5, -0.4, -0.75]          # the way the light travels, in view space: +X right, +Y up, +Z towards you
colour = "#FFFFFF"
energy = 1.0

[defaults]
system_model = "models/systems/console.glb"   # the card for systems with no model of their own
tint_system_model = true                      # its plain materials take each system's colour
game_template = "crt_tv"                      # the template for systems the theme doesn't assign one

[defaults.system_slots]                  # what the cards' slots show (see "Making a system model")
label = ["logo", "generated"]

[templates.crt_tv]                       # a game template: an id, a model, optional slot chains
model = "models/templates/crt_tv.glb"
shape = "model"                          # "model" (its own shape, the default) or "media" (shaped by each game's art)
video = "screenshot"                     # the slot whose face plays the focused game's video, with sound
video_delay_ms = 1000                    # how long the game is focused before the video starts (default 1000)

[templates.crt_tv.slots]
screenshot = ["screenshot", "hero", "cover", "authored"]
label = ["logo", "generated"]

[templates.crt_tv.fit]
screenshot = "whole"                     # "crop" (fill the face, the default) or "whole" (never cropped)

[systems.ps2]                            # per system, by system id; every key optional
model = "models/systems/ps2.glb"         # this system's card
tint = false
colour = "#2B4C9A"                       # the card's tint and the colour of its games' plain boxes
game_template = "crt_tv"
logo = "art/ps2.png"                     # default: logos/ps2.png (.jpg, .jpeg, .webp)
video = "videos/ps2.mp4"                 # an MP4 the card plays on its `video` material while focused; needs model

[systems.ps2.look.background]            # the look while ps2's games are shown; replaces the theme's block whole
top_left     = "#1E2A4E"
top_right    = "#3A2418"
bottom_left  = "#06080E"
bottom_right = "#100A08"

[ui.menu]                                # the menus' look; every key optional
background = "#141414"                   # the panels; rows, frames and borders are mixed from it and accent
text = "#ECECEC"                         # the text; dim and faint text are mixed from it towards background
accent = "#C8C8C8"                       # the focused row's border and glow, section headings, values
font = "fonts/menu.ttf"                  # a TTF, OTF, WOFF or WOFF2 in the theme folder, at most 4 MB

[motion]                                 # the grid's own motion; every key optional
focus_sway = 1.0                         # the focused item's sway: 0 to 3 times the launcher's own; 0 faces you
idle_bob = 1.0                           # the other items' bob: 0 to 3 times the launcher's own; 0 holds them still
  • Colours are #RRGGBB in sRGB.

  • Lights are in view space, so they stay put while the grid scrolls. Lights you don't define are off.

  • A per-system background, ambient or lights block replaces the theme's block of that name whole. Entering a system cross-fades to its look.

  • [ui.menu] styles the menus: the settings, the options and details screens, the power menu, dialogs, the on-screen keyboard, the file pickers and the progress cards. The names and hints over the grids, the status icons and the titles on cards keep the launcher's look. Set any of background, text and accent and every other menu colour is mixed from those three; set none and the menus stay exactly as the launcher draws them. Keep the text light on a dark background (or dark on a light one). Green, amber and red mean the same in every theme.

  • [motion] sets how lively the grid is, for models without their own clips: focus_sway is how far the focused item swings from side to side (showing its spine), and idle_bob how far every other item floats up and down. 1 is the launcher's own, 0.5 half, 2 twice, and 0 none (the focused item then faces you). A calmer or livelier theme runs just as fast.

  • A menu font loads in the background when the theme is chosen, so the menus switch to it a moment later. The menus are laid out for the default font, so a much wider font can cut titles short; check with --open=settings (see Testing). A font over 4 MB (one with every Chinese, Japanese and Korean character, say) is refused, and the default font is used. Characters your font lacks are drawn in a system font.

  • A [templates.<id>] table that uses the id of one of Memory Card's templates extends it: each key you leave out is Memory Card's. This keeps the DVD case and changes only its back:

    [templates.dvd_case.slots]
    back = ["screenshot", "generated"]

Which model is used

For each game, the launcher uses the first of these that exists and loads:

  1. The user's own model for that game (%LOCALAPPDATA%\OdysseyLauncher\media\<system>\models\<game name>.glb).
  2. The template the user picked for that game in its options.
  3. The user's template for the system (%APPDATA%\OdysseyLauncher\models\templates\<system>.glb).
  4. The user's game_model for the system in systems.toml.
  5. Your theme's [systems.<id>] game_template.
  6. Your theme's [defaults] game_template.
  7. Memory Card's template for that system.
  8. Memory Card's default template.

For a system's card: the user's %APPDATA%\OdysseyLauncher\models\systems\<system>.glb, then your [systems.<id>] model, then your [defaults] system_model, then Memory Card's generic slab. Favourites, Recently played and All games work the same way with the ids favourites, recently_played and all_games: [systems.favourites] model = "..." gives that card a model of its own (the Console theme's Power Star), and their games keep their own systems' templates.

A model that is missing, broken or too heavy is skipped, and the next one is used.

Model rules that apply to everything

Rule Value
Format glTF 2.0 binary .glb: one scene, everything embedded. Images are PNG or JPEG. No Draco compression, and no files kept outside the .glb.
Units and axes glTF's: metres, +Y up, the front of the model facing +Z (towards the viewer).
Origin The bottom centre: the model stands on y = 0, and spins about the vertical axis through the origin.
Size About 1 m on its largest side. The launcher fits every model to its cell, so you needn't be exact, but anything more than 10× off gets a "wrong units?" warning.
Drawn today Base colour (colour and texture), roughness, vertex colours, and the slot names.
Ignored today Metallic, normal maps, emissive, unlit, alpha (transparency), texture transforms and double-sided. The model looks the same without them, just flatter, so don't rely on them.
Ignored always Cameras, lights in the file (the theme's lights are the only lights), extra scenes, and animation clips with other names.
Textures Base colour textures only. Every texture is drawn at most 1024 pixels on its longer side; larger ones are scaled down when loaded.

Every model is drawn with the launcher's one item shader, so nothing a theme ships can slow the renderer beyond its triangles and textures.

Models are fitted to a bottom-centre origin with their largest side about 1 m, then scaled to the cell. In a grid, each row is as tall as its tallest model and a shorter model is centred in the row, so a landscape console sits level with the middle of the cards beside it.

Material names

A material whose name is a media kind is a slot: the launcher puts that game's artwork on it. Every other material is a plain material and shows what you made.

Slot (material name) Shows If the game has none
cover the front cover generated: a title card in the game's colour
back the back cover generated: a darker panel with the title and a strip of the cover
spine the spine generated: the cover's main colour with the title
label a disc or cartridge label generated: a paper label with the title
screenshot a screenshot your authored texture
logo the game's logo your authored texture
hero a wide banner your authored texture
  • Names ignore case and Blender's .001 suffix: Cover.001 is the cover.
  • No slot is required, not even cover. A television with only a screenshot slot is fine, and so is a model with no slots at all.
  • Any image can fill any slot (a label can show the logo, a screen can show the cover) using a slot's chain in the manifest.
  • One more name, for system models only: a material named video is the card's screen for its theme's video (see Making a system model). It isn't a slot.
  • Use any other name for plain parts. Memory Card's models call their body material case.
  • Don't use a slot name for a plain part by accident: a material called label is a slot.

Chains

A slot's chain lists the sources it tries, in order, for each game:

  • a media kind (cover, screenshot, logo, hero, ...): that game's art of the kind, if it has any;
  • generated: the face the launcher draws (only for cover, back, spine and label);
  • authored: the material's own texture, or its base colour if it has none.

The first source that exists wins. generated and authored always succeed, so they end the chain, and every chain implicitly ends with authored. While the art loads, the next generated or authored entry shows and the art fades in over it. A slot you don't list gets its own kind then generated where the launcher can draw it (cover = ["cover", "generated"]), otherwise just its own kind (screenshot = ["screenshot"]).

Preparing a slot's face

  • UVs: the first UV map must run from 0 to 1 across the face, upright as seen from outside. Sampling is clamped.
  • Base colour: set the slot material's base colour to white, because it multiplies the art. To give the slot a fallback picture (authored), plug an image into its Base Color.
  • Aspect ratio: art is centre-cropped to fill the face. The launcher measures the face's width over height from the mesh, which works for flat, upright faces. For a face that is curved or tilted (a TV screen tipped back), add a material custom property called aspect, a float such as 1.333, and tick Custom Properties when you export (it is written as extras.aspect).
  • Fit: fit = "whole" for a slot in the manifest stops the art being cropped; it is fitted inside the face at its own proportions (a tall game gets bars at the sides). A logo is always drawn whole, with a margin.
  • Resolution: a game's cover is streamed at 512², and every other slot at 256², whatever the source size. When a game is inspected (R3, or Z: large in the middle of the screen), every slot's art is read again at 1024², so a back or label is as sharp as its image allows, up to that size. A slot your chains don't name costs nothing.

Animation names

A model can have up to three animation clips. The names matter: they are how the launcher knows when to play each one. Case, a .001 suffix, and an object name before a | are ignored, so Armature|Idle.001 counts as idle.

Clip name Plays Loops
idle while the item is not focused yes
focused while the item is focused. It blends in and out over 0.25 s, and keeps playing when the player brings the item up close (R3) or opens a system's details yes
launch once, when a game starts. The emulator starts when the clip ends, or after 2 seconds, whichever comes first no
  • Any other clip name is ignored (and listed in the log).
  • Clips can move, rotate and scale nodes. To animate a part, make it a separate object: the sample TV's aerials are separate objects, and the console theme's Mega Drive cartridge and the Switch's Joy-Con slide the same way. Skins (at most 64 joints) and shape keys (at most 8 per mesh) are carried through too but are untested on a real model, so check them in the launcher.
  • Start and end near the rest pose. While a focused or launch clip is playing the item is blended back to its rest pose afterwards, so a clip that ends somewhere else will visibly jump.
  • Keep a launch clip under 2 seconds; the emulator starts then regardless.
  • If a state has no clip, the grid adds its own motion: a gentle bob for idle, a sway for focused, and a spin towards the camera for launch. The grid always lifts and enlarges the focused item as well. A focused clip replaces the sway, and a launch clip replaces the spin. Your theme's [motion] sets how far the sway and the bob go.
  • Idle clips cost performance on game templates. Game templates are drawn in batches (one draw call for every game using the template). A template with an idle clip can't be batched: every game plays its own, so each is drawn on its own, for up to 64 on screen. Give game templates focused and launch clips only, and keep idle clips for system models.
  • A model with only focused and launch clips is still batched. The focused item is swapped onto its own copy while the clip plays.

Making a game template

A game template is the model every game in a system is drawn on. It is built once and re-used for every game, with each game's art wrapped onto its slots.

The recipe:

  1. Model it about 1 m tall (or wide, for a landscape item), with its front facing Blender's front view (numpad 1, looking along +Y). With the exporter's +Y Up option, Blender's −Y becomes glTF's +Z, the front.

  2. Put the origin at the bottom centre, and apply rotation and scale (Ctrl+A, All Transforms).

  3. Give the parts that should show art slot materials named cover, back, spine, label, screenshot, logo, hero or box_texture. A typical box needs cover, back and spine, plus one plain material for the case.

  4. Unwrap each slot face so its UVs fill the whole 0–1 square, upright.

  5. Set each slot's Base Color to white, and add an aspect custom property if the face is not flat and upright.

  6. Optionally add focused and launch clips (not idle).

  7. Export as .glb (see Exporting from Blender).

  8. Register it in theme.toml, and assign it to systems:

    [templates.my_box]
    model = "models/templates/my_box.glb"
    
    [templates.my_box.slots]
    spine = ["spine", "logo", "generated"]   # the spine art, else the logo, else a printed title
    
    [systems.megadrive]
    game_template = "my_box"

Material count and textures: at most 4 materials and 2 base colour textures. For a box that is typically cover, back, spine and case.

Where a slot is shown larger than usual: the player can bring a game up close (R3), when its front is about 90% of the window's height. Any texture you author on the template, and the front's art, should hold up at that size.

Boxes shaped by their art (shape = "media")

Box art isn't one size: PC big boxes range from tall to square to landscape, thin to deep. With shape = "media", each game's box takes the proportions of its own art, so nothing is cropped:

  • The front has the proportions of the game's cover image (or, with no cover material, its screenshot).
  • The depth comes from the game's spine image. A spine taller than wide is the box's side; one wider than tall is its top and bottom (US and European SNES and N64 boxes).

To make a model for it:

  • It must have a cover material, or else a screenshot one, or the key is ignored with a warning.
  • Make cover, back and spine whole faces of the box.
  • The launcher moves each half of the model (left and right, top and bottom, front and back) outward or inward. Nothing is stretched, so corners and bevels keep their size. Keep vertices off the centre planes (x = 0, z = 0 and half the height), and keep corner and bevel detail near the edges.
  • A spine on the top and bottom: map the top's UVs left to right with the front at the image's bottom edge, and the bottom's with the front at its top edge.
  • A model with animation clips keeps its own shape (with a warning).

Playing the game's video (video)

A template can play the focused game's own video (the MP4 its scraper fetched) on one of its faces, with the video's sound, like a television: Memory Card's crt_tv_90s does.

  • Name the face's slot: video = "screenshot". The face shows its art as usual; once the game has been focused for video_delay_ms (default 1000, a second), the video fades in over it, plays with its sound, and loops.
  • Moving to another game, launching, or opening the settings or a game's details or options stops it and shows the art again.
  • It fits like the slot's art: cropped, or with fit = "whole" boxed inside the face over the material's own texture. Give a curved screen an aspect property.
  • Games without a video keep their art. Only the focused game plays, so scrolling costs nothing, and the template stays batched.
  • Videos play on Windows; elsewhere the face shows its art.

The built-in generic_box_spine, generic_box_logo, generic_box_top_spine, flat_cover and flat_screenshot do this. Their models are good ones to open in Blender.

Making a system model

A system model is the card a system is shown as on the systems screen, and it is shown large on the system's details screen (Y), where the player can turn it with the right stick, so its back and underside are seen too. It can be a console, a slab, a poster, anything.

The recipe is the same as a game template (about 1 m, origin at the bottom centre, front facing +Z), plus:

  • Colour (tinting). With tint_system_model = true in [defaults] (or tint = true for one system), each plain material is mixed towards that system's colour. Make the parts you want tinted light and neutral, and put fixed colours (a black screen, a red button) on parts that shouldn't change. A model made for one system only (a Mega Drive model for [systems.megadrive]) has tint off by default.

  • Showing the system's logo. A card can have slot materials too, but a system's only art is its logo. A card's chains can name only logo, generated and authored:

    • logo: the system's image (logos/<system id>.png in your theme).
    • generated: the launcher draws it, for cover, back, spine and label (a card's label prints the system's name).
    • authored: the material's own texture or colour.

    Give your card a material named label (or cover) on the face that should show the logo, then in the manifest:

    [defaults.system_slots]
    label = ["logo", "generated"]       # the logo, else the system's name printed on a label

    A system can override a slot's chain in [systems.<id>.slots]. A slot no chain lists shows generated where it can, else authored.

  • A logo is drawn whole, never cropped: within a margin of at most 88% of the face's width and 84% of its height, over the slot's fallback. Over authored it is not multiplied by the material colour, so a dark slot material shows the logo on its own colour. Crop each logo image to what it shows, because the transparent border counts as part of it. PNG, JPEG or WebP; larger than 512 px gains nothing.

  • One model for every system. Slab does this: one slab model, and 169 logos in logos/. Set [defaults] system_model, add [defaults.system_slots], and drop images into logos/.

  • The first time a theme with logos is used, its logos are baked into textures in the background (Slab's 169 take about 7 s), and a card shows its fallback until its own is ready. Only the most recent theme with logos is kept baked.

  • Playing a video on the card. A card can show a video on a screen while it's focused, as the console theme's Amstrad CPC monitor does:

    • Give the screen a material named video with a base colour texture of its own (white base colour, UVs spanning 0 to 1 across the screen, upright). That texture shows whenever the video isn't playing, so make it the video's first frame.
    • Name the video beside the model: [systems.<id>] model = "..." and video = "videos/<id>.mp4". A video without a model in the same entry is ignored, with a warning.
    • The video plays without sound, starting with the card's focused clip and starting again each time the clip loops, so make it as long as the clip to keep the two in step. Without a focused clip it simply loops.
    • Encode it as H.264 4:2:0, no sound, no bigger than the screen needs (textures are drawn at most 1024 pixels wide): ffmpeg -i in.mp4 -c:v libx264 -pix_fmt yuv420p -g 30 -an out.mp4. Windows decodes it; elsewhere the card shows the texture.
    • Only the focused card plays, while the systems screen is showing. The decoder adds about 50 MB of memory while it does.
  • Clips are welcome here, because system models can have idle (a game template should not). The Mega Drive card in the console theme has a focused clip that inserts a cartridge, and the Switch's slides a Joy-Con up its rail.

System cards share the same material and animation names as game templates.

Assigning it:

[defaults]
system_model = "models/systems/slab.glb"   # all systems
tint_system_model = true

[systems.gamegear]
model = "models/systems/gamegear.glb"      # one system's own card

Keeping models simple: limits and advice

The limits

Model Triangles Base colour textures (not counting slot art) Materials
Game template 2,000 2, each up to 1024² 4
A user's per-game model 5,000 2, each up to 1024² 4
System model 30,000 4, each up to 2048² 8

Skins have at most 64 joints and meshes at most 8 morph targets.

What happens when you go over:

  • Over a limit: the model is used, with a warning in the model log.
  • More than twice over (triangles, textures, materials, joints or morph targets): the model is rejected, and the next model in line is used.
  • A texture larger than its limit is scaled down. Its size never rejects a model.
  • A broken file (bad data, an index out of range, a missing image) is rejected with the reason; it never crashes the launcher.

Normal, metallic-roughness, occlusion and emissive maps don't count (they are not drawn yet).

A menu font ([ui.menu] font) can be at most 4 MB.

Sensible targets

The limits are ceilings, not targets. The launcher is built to hold the display's refresh rate while scrolling thousands of games on a Steam Deck, so spend the budget carefully. These are recommendations:

Aim for For reference
Game template a few hundred to about 1,000 triangles; no more than 2 or 3 materials The built-in cases and boxes are about 220 triangles. The arcade cabinet, the most detailed, is 1,402 with three materials.
System model 5,000 to 15,000 triangles, 3 to 6 materials, 1 or 2 textures The limit is 30,000, but a card is drawn many times on the systems screen, and again on the details screen.
Textures one 1024² atlas is better than several Textures are held uncompressed on the graphics card: a 1024² texture takes about 5 MB, and a 2048² one 21 MB.
Clips few, short, simple A clip only moves node transforms.

Advice:

  • Mind the triangles on a game template, because a grid shows many at once. One draw call covers every game using a template, but the triangles are still drawn for each game on screen, and a whole screenful of games is drawn every frame.
  • Don't author 2048² textures. Every texture is drawn at 1024 pixels at most; a larger image only makes a larger file and a slower load. Lay out several parts in one 1024² image, but give each part room at that size. In the console theme, cutting textures from 2048 to 1024 took video memory from 137 MB to 37 MB, with no visible difference at 1080p.
  • Merge materials. Every material is a cost. Parts that are one colour can share a material. For a model with many small colours, use a small palette texture (the arcade cabinet uses 64² with each part's UVs in the middle of its colour's swatch), or vertex colours, instead of a material per colour.
  • Delete what nobody sees. Faces on the underside or back of a game template (the player only sees a game's back in close-up), interior geometry, and the insides of closed boxes.
  • Don't subdivide flat surfaces. Use bevels only where the silhouette needs them, and run Merge by Distance and Decimate to finish.
  • Don't bake strong shadows or reflections into textures. The theme's lights move the shading, and the base colour is multiplied by them. Roughness is honoured, but metallic, normal and emissive are not (yet), so don't plan on them.
  • Strip unused maps. Normal, roughness, occlusion and emissive maps aren't drawn, so exporting them only bloats the file (the repository's tools/console-models/strip_maps.py stripped a console from 13.2 MB to 5.4 MB).
  • Don't use idle clips on game templates. See Animation names.
  • Keep a launch clip short (under 2 seconds) and ending near the rest pose.
  • Keep models roughly 1 m, with the origin at the bottom centre. Anything off by more than 10× is flagged.
  • Weigh the front's art. A game's cover is streamed in at 512², so there's no point authoring a larger fallback on the front face.

Exporting from Blender

Blender 4.x's glTF exporter makes everything the launcher needs. In File > Export > glTF 2.0:

Setting Value
Format glTF Binary (.glb)
Include Limit to Selected Objects if the file has anything else; tick Custom Properties (this writes the aspect property)
Transform +Y Up (the default)
Data > Mesh Apply Modifiers, UVs, Normals; vertex colours if you use them
Data > Material Export; Images: Automatic (PNG or JPEG)
Animation Tick Animation; mode Actions (each action becomes a clip named after it)
Compression Off (Draco isn't supported)

For clips, make one action per clip named idle, focused or launch, and push each onto the object's NLA stack so the exporter finds it.

You can also let users add their own models without a theme: a game's options (X) can import a .glb (or an OBJ model as a zip of the .obj, its .mtl and its textures; material names such as cover still work as slots). A PS2 save icon with its .anim file gets a focused clip.

Testing

Look at it. Put the theme in your themes folder and switch to it (Menu, Theme, or T). To keep your own library untouched, use a separate user folder:

OdysseyLauncher.exe -- --user-dir=C:\ThemeTest --theme=neon-arcade

(Copy the theme into C:\ThemeTest\themes\ first.) Some more options, all after the --:

Option Does
--start-system=ps2 Opens that system straight away.
--layout=single/list Shows a system's card, and a game's model, about half the screen high, so it's the closest look at your detail.
--layout=carousel/carousel Turns the items beside the focused one towards it.
--no-overlay Hides the text, so you can check the background corners.
--open=settings Opens the settings straight away, to check your menus (also power, keyboard, confirm, folder-picker).
--capture=C:\ThemeTest\shot.png --capture-frame=150 Saves a screenshot and quits.

Check it with games that have art and games that have none. A scraped library has both.

Read the model log. Every warning and rejection is written, with what was used instead, to %LOCALAPPDATA%\OdysseyLauncher\logs\models.log (portable: userdata\logs\models.log). Manifest problems are shown in the console as the theme loads, with file, line and key.

Check a model before using it (if you have the repository checked out and built, with dotnet build):

.\tools\scrape-cli\bin\Debug\net8.0\odyssey-scrape.exe inspect-model --kind=template path\to\my_box.glb

It prints the triangle, material and texture counts against the budget, the slots and clips it found, the size, and every warning and error, without changing anything. Use --kind=system for a system model.

Before you share a theme, check that:

  • theme.toml loads with no errors or warnings;
  • every model passes inspect-model with no warnings;
  • each slot shows art the right way up and not stretched;
  • focused and launch clips return to the rest pose;
  • scrolling a big system is as smooth as with Memory Card (--theme=memory-card).

Troubleshooting

You see Why Fix
The theme isn't in the list It's not in the themes folder, or theme.toml has a syntax error (the console names the line) Check the path and the first error
A system shows Memory Card's box Your theme doesn't assign it a template, the template's model is missing or rejected Read the console and models.log
Art is stretched or cut off The face's aspect ratio is wrong Add an aspect custom property and export Custom Properties
Art is upside down or mirrored The slot's UVs aren't upright from outside Flip the UV island
A slot shows only a plain colour The material isn't named after a slot, or its chain names kinds the game has no art for Check the name, then the chain
A card's video doesn't play No material named video, it has no texture of its own, the video key isn't in the same entry as the card's model, or the file is 4:4:4 or not H.264 Check the console and models.log, and re-encode with -pix_fmt yuv420p
A game's video doesn't play The template has no video, the slot it names isn't one of the model's materials, the game has no video, or a settings screen or panel is open Check the console's warnings and the game's media (Y on the game)
A card shows no logo The chain doesn't name logo, the image isn't named after the system's id (megadrive, not "Mega Drive"), or it is still being baked Check [defaults.system_slots], the id and the console
The model is tiny, or huge, with a units warning Modelled in centimetres or millimetres Scale it to about 1 m and apply the scale
A clip doesn't play Not named idle, focused or launch, or not exported Rename the action and push it onto the NLA stack
"more than twice the ... may have" in the log The model is too heavy, so it was rejected Decimate it or merge materials
⚠️ **GitHub.com Fallback** ⚠️