Skinned Meshes¶
Skinned meshes allow bone-based character animation on PS1. Attach a PSXSkinnedObjectExporter to any GameObject with a SkinnedMeshRenderer, assign animation clips, and play them at runtime via Lua or as timed events inside cutscenes and animations.
How It Works¶
At export time, SplashEdit:
- Creates a temporary bind-pose mesh proxy for the skinned object (this is what gets exported as normal geometry)
- Bakes every animation clip by sampling bone transforms at the configured FPS
- Stores per-frame bone matrices (3×3 rotation + translation) in the splashpack binary
- Records per-triangle bone indices (hard skinning — each vertex uses its highest-weight bone)
At runtime on PS1, the engine:
- Composes camera × object × bone matrices per bone
- Transforms each vertex individually using its assigned bone's matrix via the GTE (
rtps) - Interpolates between baked frames with sub-frame precision for smooth playback at any framerate
Hard skinning
The PS1 doesn't have the power for multi-bone blending per vertex. Each vertex is assigned to a single bone (the one with the highest weight). Design your meshes with this in mind — weight painting should use clean, hard transitions between bones.
Adding a Skinned Object¶
- Import your rigged model (FBX recommended)
- Place it in the scene — it should have a
SkinnedMeshRenderer - Add the
PSXSkinnedObjectExportercomponent to the same GameObject - Assign animation clips to the Animation Clips array. Remember if you can't preview them in Unity they won't work on the PSX.
PSXSkinnedObjectExporter Settings¶
| Field | Description | Default |
|---|---|---|
| Animation Clips | Array of AnimationClip assets to bake. Each becomes a named clip playable via SkinnedAnim.Play() |
Empty |
| Target FPS | Bone matrix sampling rate (1–30). Lower = less memory, higher = smoother. 15 is usually sufficient. | 15 |
| Is Active | Whether the object starts active in the scene | On |
| Lua File | Optional Lua script for this object's behavior | None |
| Bit Depth | Texture color depth: 4-bit, 8-bit, or 16-bit | 8-bit |
| Color Mode | Vertex color mode: Baked Lighting, Flat Color, or Mesh Vertex Colors | Baked Lighting |
| Flat Vertex Color | (Flat Color mode only) Solid color applied to all vertices | 128, 128, 128 |
| Smooth Normals | Average normals across shared vertices for smooth lighting. Disable for flat/faceted shading. | On |
Lua Playback¶
Use the SkinnedAnim API to control skinned animations from Lua scripts. See the Lua API Reference for the full API.
-- Play a clip by object name and clip name
SkinnedAnim.Play("MyCharacter", "walk", { loop = true })
-- Play one-shot with callback
SkinnedAnim.Play("MyCharacter", "attack", {
onComplete = function()
SkinnedAnim.Play("MyCharacter", "idle", { loop = true })
end
})
-- Stop playback
SkinnedAnim.Stop("MyCharacter")
-- Check if playing
if SkinnedAnim.IsPlaying("MyCharacter") then
Debug.Log("Animating")
end
-- Get current clip name
local clip = SkinnedAnim.GetClip("MyCharacter")
Skin Anim Events¶
Skinned animations can also be triggered at specific times during cutscenes and animations using Skin Anim Events. This works the same way as audio events - you specify a trigger time, target object, clip name, and whether it should loop.
See Cutscenes — Skin Anim Events and Animations — Skin Anim Events.
Export Details¶
During scene export, each PSXSkinnedObjectExporter goes through a special pipeline:
- A proxy
PSXObjectExporteris created from the bind-pose mesh - The original
SkinnedMeshRendererobject's polygons are "stolen" by the skinned mesh system - the normal renderer skips it - Per-bone matrices are baked for every frame of every clip at the configured FPS
- Bone indices (3 per triangle - one per vertex) are stored for the renderer to look up per-vertex bone transforms
The proxy is destroyed after export completes.
Memory Cost¶
Skinned mesh data uses significantly more memory than static objects due to the per-frame bone matrices:
Per frame per bone: 24 bytes (9×int16 rotation + 3×int16 translation)
Example: A character with 20 bones and a 2-second walk cycle at 15fps:
- 30 frames × 20 bones × 24 bytes = 14,400 bytes per clip
Total memory scales with: clips × frames × bones × 24 bytes + triangles × 3 bytes (bone indices)
Reduce memory
- Lower Target FPS (15 is visually smooth enough for most animations thanks to sub-frame interpolation)
- Use fewer bones (10–20 is typical for PS1 characters)
- Keep clips short (reuse idle/walk loops)
- The runtime interpolates between frames, so you can get away with surprisingly low sample rates
Limits¶
| Resource | Limit |
|---|---|
| Skinned meshes per scene | 16 |
| Bones per mesh | 64 |
| Clips per mesh | 16 |
| Clip name length | 24 characters |
| Frame count per clip | No hard cap (memory dependent) |
Tips¶
Weight painting
Since the PS1 uses hard skinning (one bone per vertex), make sure your weight painting has clean, clear bone assignments. Avoid soft blends across many bones — the engine picks only the strongest weight.
Animation clip names
Clip names are used in Lua to reference animations. They come from the Unity AnimationClip asset name. Keep them short and descriptive: idle, walk, attack, jump.