At a glance

Texel
A 4-bit (or 8-bit) index. 4bpp: the low nibble is the left texel.
Palette
16 (or 256) 15-bit colours plus an STP bit, read from the VRAM cell the primitive names at draw time - not from the texture's own file.
Transparency
0x0000 is never drawn; 0x8000 is opaque black.
Shading
out = texel × colour / 128. 0x80 is neutral; most field surfaces sit below it, so they draw darker than their texture.
Light source
Only on the light-source mesh rows (TMD group flags 0x10..=0x17), shaded by the GTE light. Everything else uses its baked colour plus the depth cue.
Blending
Per texel: STP colours of a semi-transparent primitive blend with the screen in one of four modes.
Output
15-bit framebuffer, dithered by retail (on at boot, script-controlled).
In the port
Shading defaults to retail; rasterisation (vertex snap, dither) defaults to clean, one switch away.

The chain, step by step

what a texture export shows what the mesh adds 1. Texel index 4 or 8 bits from the page 2. Palette read from VRAM at the CBA cell 3. Transparency key 0x0000 skips the texel 4. Colour word baked; lit rows: GTE light 5. Depth cue toward far colour by IR0 Multiply texel x colour / 128 6. Blend STP texels, 4 modes 7. Dither + write 4x4 ordered, 5 bits
The texture half (left) is all a PNG export can show. The mesh half (right) is where "the game applies shading": the same palette entry lands darker, brighter or tinted depending on the primitive that draws it. The depth cue runs on the colour word before the multiply, so a textured primitive's far colour is texel × far / 128, not the far colour itself.
  1. IndexThe GPU fetches the texel's index from the texture page.
  2. Palette16 halfwords at VRAM ((CBA & 0x3F) × 16, CBA >> 6) - whatever the last upload left there.
  3. KeyA 0x0000 entry skips the texel, opaque primitive or not.
  4. Colour wordOne per primitive (flat) or per corner (gouraud), baked by the artists.
  5. Depth cuec + (far − c) × IR0 / 4096; unfogged field scenes use IR0 = 0.
  6. BlendSTP texels mix with the screen: B/2+F/2, B+F, B−F or B+F/4.
  7. Dither4×4 ordered offset, then each channel cut to 5 bits.

Why a texture's palettes can look wrong

An export decodes the image through one of the file's own palettes. Three Legaia habits make that differ from the game:

  • One sheet, many palettes. A 4bpp UI or character sheet is one grid of indices that every sprite or polygon reads through its own palette. Each palette recolours the whole image, so in any one palette most of the sheet shows colours it is never drawn with. The status badges (Venom, Toxic, Stone...) are all different colours in game, each through its own palette.
  • A palette from another file. A draw names a VRAM cell, and that cell can belong to a different texture: three of the status badges use palettes uploaded by a small palette-only file that sits just before the UI sheet on the disc.
  • A palette overwritten before use. Uploads are last-write-wins. The battle ASCII font's own palette is overwritten at boot by the menu glyph atlas's palette strip, so the game always draws it through a cell of that strip.

A multi-row palette block also lands in VRAM as one flat strip on a single row, so "palette N" of a TIM is the cell 16×N entries along that row, not row N.

The asset viewer's TIM catalog resolves these from your disc: its Palette list offers As the game draws it for the menu / battle UI sheet (every sprite through the palette the game uses for it, taken from the widget-class table), lists the palettes video memory really holds on the texture's row, and notes when a texture's own palette is never used or is all zeros. The ROM patcher's texture editor reads the same map.

Why the game looks darker (or brighter) than the texture

Every mesh polygon carries a colour painted by the artists, one per corner on smooth-shaded polygons. The GPU multiplies the texture by it, with 0x80 meaning "unchanged". A texel of (200, 100, 50) under 0x60 shows as (150, 75, 37); under 0xC0 it clamps to (255, 150, 75). Roughly four in five colour components in the field scenes sit below 0x80. Menu and HUD sprites are drawn at exactly 0x80, which is why a UI sheet decoded through the right palette matches the screen and a field texture does not.

The light-source rows are the exception. They carry a normal instead of a colour word, and the GTE computes their colour from the scene light: the back colour (0x20 a channel unless a script changes it) plus the light's intensity against the face, times the object colour. A face turned away from the light keeps about an eighth of its texel. cave01's rock walls are built entirely from such rows; most scenes hold few or none (formula).

Battles add two runtime terms: an ambient that the ground grid is shaded from (it dims while a summon is on screen and fades in at the start of every fight), and a per-actor tint that darkens distant bodies, colours status ailments and draws a rotted limb dark.

Palettes that move

MechanismWhat movesMore
Palette walk (frames copied into the live cell)water, waterfalls, the world-map oceanWorld map
Hue / brightness cycler"pulsating flesh", lightning flashesfield ambient FX
Scripted palette family (field-VM 4C 61)fades and one-shot palette stampsScript VM
Prologue palette collapseevery uploaded palette rewritten to gold sepiaCutscene

An export shows the disc state, so a palette that looks like a strip of near-identical ramps is often a set of animation frames waiting to be walked into the live cell.

Editing a texture

  • Edit indices, not one palette's colours. The same index means a different colour in every palette; check the region in the palette the game uses before repainting it.
  • Keep the transparency key. 0x0000 is "not drawn" everywhere; use 0x8000 for black.
  • Don't pre-brighten. A dark look in game is the mesh colour or the depth cue, applied on top of your texture; brightening the file brightens every polygon that uses it.
  • All-zero palettes are placeholders filled in video memory by another texture of the same scene.
  • Animated palettes overwrite the file's colours in the cells they walk.

Replacing a texture on your disc is done in the ROM patcher; the byte-level rules are on the TIM page.

In the port

The renderer's two halves point opposite ways. Shading defaults to retail: the baked colour word, the depth cue and the light-source rows, with no synthetic light on any game path (the play hosts layer the optional enhanced lighting over it). Rasterisation defaults to clean: vertex snap and the 15-bit dither are behind set_psx_mode. Affine UVs are not gated - they are always on, because they are the faithful behaviour.

Palette maps
legaia_ui_formats::tim_palette_context (which cell draws which texel; what the boot VRAM holds)
VRAM + palette decode
engine-render (1024×512 R16Uint texture, decode in the fragment shader)
Multiply, cue, blend, dither
psx_modulate / psx_depth_cue / psx_blend / psx_dither in the shader prelude, CPU mirror psx_light
Light-source rows
render-kernels::field_light, engine-field::field_lit_mesh
Battle colour
engine-battle-vm::battle_ground_grid, battle_actor_tint
Animated palettes
engine-field::clut_cell_fx, legaia_scene_formats::clut_walk

See also