Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

LIP (Lip Synching)

LIP files provide keyframed facial morph data directly bound to audio streams, instructing character models how to physically animate their mouths to match speech.

At a Glance

PropertyValue
Extension(s).lip
Magic SignatureLIP V1.0
TypeFacial Animation Keyframes
Rust ReferenceView rakata_formats::Lip in Rustdocs

File Layout

Two blocks and no offsets to follow: a 16-byte header, then the keyframe array immediately after it. Nothing in the file points anywhere, because nothing needs to.

OffsetFieldTypeNotes
0x00signaturechar[8]LIP V1.0, checked as one eight-byte run rather than as a magic and version pair.
0x08animation lengthf32Total duration in seconds.
0x0Centry_countu32
0x10keyframesarray5 bytes each, packed with no padding.

Keyframe Entry (5 bytes each)

OffsetFieldTypeNotes
0x00timef32Timestamp in seconds, from the start of the animation.
0x04shapeu8Viseme index 0–15. See below.

The 16 visemes

Every one of these occurs across a full install’s 1,073,460 keyframes, and no value outside the range appears anywhere. The names are phonetic groupings rather than individual sounds: one mouth shape serves every consonant made the same way.

#Shape#Shape#Shape#Shape
0Neutral, mouth closed4“oh”8“f”, “v”12“t”, “d”
1“ee”5“ooh”9“ng”13“sh”
2“eh”6“y”10“th”14“l”
3“ah”7“s”, “t”, “s” cluster11“m”, “p”, “b”15“k”, “g”

Warning

The on-disk layout is the in-memory layout The engine reads the keyframes straight out of the file buffer, five bytes at a time from 0x10, once, when a line starts. That makes the five-byte stride load-bearing in a way most formats’ are not: there is no padding to the natural four-byte alignment a f32 would normally want, and adding any would not produce a slightly different file, it would produce one the engine reads as garbage from the second keyframe onward.

How a dialogue line finds its lip file

A conversation entry’s lip file is the .lip with the same name as the line’s sound: its Sound, or VO_ResRef when that’s empty (see dlg.md). Unlike the voice, it isn’t streamed from a folder: the lip file is an ordinary resource lookup, so it can live in Override or a module like anything else (see the resource tiers). A file that doesn’t start with LIP V1.0 isn’t used.

When the voice opens, the mouth starts moving on the first frame the stream reports it’s playing. When no voice file opens, the lip file is applied straight away, so a line with a lip file and no voice still moves the speaker’s mouth, in silence. Either way it only drives the mouth. Nothing about a lip file plays audio.

The keys have to span the whole animation. The first key’s time must be exactly 0, and the last must land on the header’s animation length, to within about one part in a hundred thousand. A lip file that misses either moves nothing, and nothing says why. Every lip file vanilla ships passes both.

Engine Audits & Decompilation

Read from CLIP::LoadLip at 0x0070c590 and CSWCCreature::LipSync in swkotor.exe. Provenance: derived, not attested. The rows below have not been separately re-derived, so they sit on the reverse-engineering queue.

Pipeline EventGhidra Provenance & Engine Behavior
Loading Keeps a PointerLoadLip checks that the first eight bytes read LIP V1.0, keeps a pointer to the keys at +0x10, and returns. It never loops. The animation length at +0x08 and the key count at +0x0C are read from the buffer where the keys are used.
One Pass When a Line StartsCSWCCreature::LipSync walks every five-byte key in place, turning each time into a fraction of the animation length and each viseme into (viseme + 1) / 16. It then checks that the first fraction is exactly 0 and the last within about 1e-5 of 1. If either fails it applies nothing and returns failure. Otherwise it snaps the last time to exactly 1, clears any viseme animation already running, and hands both arrays to the speaker’s animation, which keeps its own copy. Nothing reads the file buffer again.