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
| Property | Value |
|---|---|
| Extension(s) | .lip |
| Magic Signature | LIP V1.0 |
| Type | Facial Animation Keyframes |
| Rust Reference | View 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.
| Offset | Field | Type | Notes |
|---|---|---|---|
0x00 | signature | char[8] | LIP V1.0, checked as one eight-byte run rather than as a magic and version pair. |
0x08 | animation length | f32 | Total duration in seconds. |
0x0C | entry_count | u32 | |
0x10 | keyframes | array | 5 bytes each, packed with no padding. |
Keyframe Entry (5 bytes each)
| Offset | Field | Type | Notes |
|---|---|---|---|
0x00 | time | f32 | Timestamp in seconds, from the start of the animation. |
0x04 | shape | u8 | Viseme 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 |
|---|---|---|---|---|---|---|---|
| 0 | Neutral, mouth closed | 4 | “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” cluster | 11 | “m”, “p”, “b” | 15 | “k”, “g” |
Warning
The on-disk layout is the in-memory layout As the audit below records, the engine does not parse the keyframe array at all. It points an internal pointer at file offset
0x10and animates directly off the raw buffer. 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 af32would 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.
Engine Audits & Decompilation
Read from CLIP::LoadLip at 0x0070c590 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 Event | Ghidra Provenance & Engine Behavior |
|---|---|
| Zero-Copy Loading | The engine handles LIP files as completely flat structures. Instead of parsing the variables out individually, it simply verifies the "LIP V1.0" signature and pulls the animation length and entry count directly from offsets +0x08 and +0x0C. |
| Direct Array Assignment | The keyframes are packed into identical 5-byte chunks (a 4-byte float for the timestamp, and a 1-byte integer determining the mouth shape). Because of this flat layout, the engine never loops through the data to read it. It simply points its internal animations memory pointer perfectly to file offset +0x10 and natively runs the animation straight off the raw file buffer. |