Creating themes - GeekRhapsody/OdysseyLauncher GitHub Wiki
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.
- Where themes go
- The smallest possible theme
- Theme folder layout
- The manifest,
theme.toml - Model rules that apply to everything
- Material names
- Animation names
- Making a game template
- Making a system model
- Keeping models simple: limits and advice
- Exporting from Blender
- Testing
- Troubleshooting
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.tomlyourself:[display] theme = "neon-arcade"
Things to know:
- The launcher's own themes (
console,slaband the hidden base themememory-card) live in thethemesfolder besideOdysseyLauncher.exe. A theme of yours with the same id asconsoleorslabreplaces it.memory-cardis 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.
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"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.tomlis 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,.jpegor.webp) is that system's logo, for its card. The ids are the ROM folder names, plusfavourites,recently_playedandall_gamesfor the Favourites, Recently played and All games cards.
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
#RRGGBBin 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,ambientorlightsblock 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 ofbackground,textandaccentand 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_swayis how far the focused item swings from side to side (showing its spine), andidle_bobhow 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"]
For each game, the launcher uses the first of these that exists and loads:
- The user's own model for that game (
%LOCALAPPDATA%\OdysseyLauncher\media\<system>\models\<game name>.glb). - The template the user picked for that game in its options.
- The user's template for the system (
%APPDATA%\OdysseyLauncher\models\templates\<system>.glb). - The user's
game_modelfor the system insystems.toml. - Your theme's
[systems.<id>] game_template. - Your theme's
[defaults] game_template. - Memory Card's template for that system.
- 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.
| 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.
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
.001suffix:Cover.001is the cover. -
No slot is required, not even
cover. A television with only ascreenshotslot is fine, and so is a model with no slots at all. - Any image can fill any slot (a
labelcan 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
videois 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
labelis a slot.
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 forcover,back,spineandlabel); -
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"]).
- 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 as1.333, and tick Custom Properties when you export (it is written asextras.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). Alogois 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.
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
focusedorlaunchclip is playing the item is blended back to its rest pose afterwards, so a clip that ends somewhere else will visibly jump. - Keep a
launchclip 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
focusedclip replaces the sway, and alaunchclip 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
idleclip can't be batched: every game plays its own, so each is drawn on its own, for up to 64 on screen. Give game templatesfocusedandlaunchclips only, and keepidleclips for system models. - A model with only
focusedandlaunchclips is still batched. The focused item is swapped onto its own copy while the clip plays.
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:
-
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.
-
Put the origin at the bottom centre, and apply rotation and scale (
Ctrl+A, All Transforms). -
Give the parts that should show art slot materials named
cover,back,spine,label,screenshot,logo,heroorbox_texture. A typical box needscover,backandspine, plus one plain material for the case. -
Unwrap each slot face so its UVs fill the whole 0–1 square, upright.
-
Set each slot's Base Color to white, and add an
aspectcustom property if the face is not flat and upright. -
Optionally add
focusedandlaunchclips (notidle). -
Export as
.glb(see Exporting from Blender). -
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.
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
coverimage (or, with nocovermaterial, itsscreenshot). - The depth comes from the game's
spineimage. 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
covermaterial, or else ascreenshotone, or the key is ignored with a warning. - Make
cover,backandspinewhole 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).
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 forvideo_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 anaspectproperty. - 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.
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 = truein[defaults](ortint = truefor one system), each plain material is mixed towards that system'scolour. 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]) hastintoff 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,generatedandauthored:-
logo: the system's image (logos/<system id>.pngin your theme). -
generated: the launcher draws it, forcover,back,spineandlabel(a card'slabelprints the system's name). -
authored: the material's own texture or colour.
Give your card a material named
label(orcover) 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 showsgeneratedwhere it can, elseauthored. -
-
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
authoredit 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 intologos/. -
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
videowith 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 = "..."andvideo = "videos/<id>.mp4". Avideowithout amodelin the same entry is ignored, with a warning. - The video plays without sound, starting with the card's
focusedclip and starting again each time the clip loops, so make it as long as the clip to keep the two in step. Without afocusedclip 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.
- Give the screen a material named
-
Clips are welcome here, because system models can have
idle(a game template should not). The Mega Drive card in the console theme has afocusedclip 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| 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.
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.pystripped a console from 13.2 MB to 5.4 MB). - Don't use idle clips on game templates. See Animation names.
-
Keep a
launchclip 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.
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.
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.tomlloads with no errors or warnings; - every model passes
inspect-modelwith no warnings; - each slot shows art the right way up and not stretched;
-
focusedandlaunchclips return to the rest pose; - scrolling a big system is as smooth as with Memory Card (
--theme=memory-card).
| 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 |