18 KiB
Avatar Component System & glTF Attachment Spec
Status: Proposed Owner: Platform team Related: Server authority + client prediction, world chunk data format
1. Overview
This document specifies the avatar component system for agp-game, a Godot-based multiplayer game. It defines:
- How an avatar is decomposed into body parts and attachment slots.
- How cosmetic attachments (clothing, accessories, gear) mount onto the base body.
- The glTF/GLB format requirements that all avatar assets must satisfy so they can be loaded, skinned, and networked consistently.
The goal is a modular, data-driven avatar pipeline: a single base body rig plus swappable attachments, all authored in glTF and consumed by a shared Godot runtime. This enables player customization, cosmetic monetization, and server-authoritative validation of what a player is wearing.
1.1 Design Goals
- Modularity: Body parts and attachments are independent assets that can be combined at runtime without re-exporting.
- Consistency: A single, well-defined skeleton and slot convention so any attachment fits any base body.
- Networkability: The avatar's composition is expressed as a compact, serializable loadout that the server can validate and replicate.
- Performance: Attachments are skinned to the same skeleton and share materials where possible to minimize draw calls and skinning cost.
- Tooling-friendly: Assets are authored in standard glTF 2.0 and validated by a build-time checker.
2. Avatar Decomposition
2.1 The Base Body
Every avatar is built on a base body — a single glTF asset containing:
- A skeleton (the canonical rig, see §3).
- A base mesh for the skin/body (head, torso, arms, legs).
- Attachment points (empty nodes) that define where accessories mount.
The base body is the only asset that carries the skeleton. Attachments reference the skeleton by bone name and are skinned to it; they never carry their own rig.
2.2 Body Parts vs. Attachments
Two distinct concepts:
| Concept | Definition | Example |
|---|---|---|
| Body part | A mesh that replaces part of the base body's skin (e.g., a different head, a tail, wings). | Alternate head, cybernetic arm |
| Attachment | A mesh that mounts on top of the body without replacing it (clothing, accessories). | Shirt, hat, glasses, backpack |
The distinction matters for slot exclusivity and layering:
- Body parts are exclusive per slot — only one head can be equipped.
- Attachments are layered — a hat and glasses can coexist, and both sit above the head mesh.
3. Skeleton & Slot Convention
3.1 Canonical Skeleton
All avatars share a canonical skeleton with a fixed set of bone names. This
is the contract that lets any attachment fit any body. The skeleton follows a
humanoid convention (Godot's Humanoid profile / glTF VRM-style naming).
Required bones (subset of the full rig):
Root
├── Hips
│ ├── Spine
│ │ ├── Chest
│ │ │ ├── Neck
│ │ │ │ └── Head
│ │ │ │ ├── HeadTop # hat / hair mount
│ │ │ │ ├── Jaw # face accessories
│ │ │ │ └── LeftEye / RightEye
│ │ │ ├── LeftShoulder
│ │ │ │ └── LeftUpperArm
│ │ │ │ └── LeftLowerArm
│ │ │ │ └── LeftHand
│ │ │ └── RightShoulder
│ │ │ └── RightUpperArm
│ │ │ └── RightLowerArm
│ │ │ └── RightHand
│ │ └── (Chest children for torso attachments)
│ ├── LeftUpperLeg
│ │ └── LeftLowerLeg
│ │ └── LeftFoot
│ └── RightUpperLeg
│ └── RightLowerLeg
│ └── RightFoot
Rules:
- Bone names are case-sensitive and must match exactly.
- The skeleton is T-pose at rest for consistent skinning.
- Attachments must be skinned to at least the bones they deform; static
accessories may be parented to a single bone (e.g., a hat parented to
Head).
3.2 Attachment Slots
Slots are the logical mount points for body parts and attachments. Each slot maps to one or more bones and defines what can be equipped there.
| Slot ID | Category | Mount bone(s) | Exclusivity | Example assets |
|---|---|---|---|---|
head |
body part | Head |
exclusive | alternate head, face |
hair |
attachment | HeadTop |
exclusive | hairstyles, hats |
face |
attachment | Jaw |
exclusive | glasses, masks, beards |
torso |
body part | Chest |
exclusive | alternate torso |
chest |
attachment | Chest |
exclusive | shirts, armor, chest gear |
back |
attachment | Chest (back) |
exclusive | backpacks, wings, capes |
left_arm |
body part | LeftUpperArm |
exclusive | alternate arm |
right_arm |
body part | RightUpperArm |
exclusive | alternate arm |
left_hand |
attachment | LeftHand |
exclusive | gloves, weapons (held) |
right_hand |
attachment | RightHand |
exclusive | gloves, weapons (held) |
legs |
body part | Hips |
exclusive | alternate legs |
pants |
attachment | Hips |
exclusive | pants, skirts |
left_foot |
attachment | LeftFoot |
exclusive | shoes |
right_foot |
attachment | RightFoot |
exclusive | shoes |
accessory |
attachment | Root (free) |
multiple | belts, jewelry, floating items |
Slot semantics:
exclusiveslots allow at most one equipped item at a time.- The
accessoryslot is multi-slot — multiple accessories may be equipped simultaneously (bounded by a configurable limit, e.g., 8). - Body-part slots (
head,torso,legs,left_arm,right_arm) replace the corresponding base mesh. When a body part is equipped, the base mesh for that region is hidden. - Attachment slots layer on top of the base mesh and never hide it.
3.3 Slot → Bone Mapping
Each slot declares a primary mount bone and an optional secondary bone list for skinning. The runtime uses this to:
- Parent static accessories to the mount bone.
- Skin skinned attachments to the union of the slot's bones.
4. glTF/GLB Format Requirements
All avatar assets (base bodies, body parts, attachments) are distributed as
glTF 2.0 (.gltf + external buffers/textures) or GLB (.glb, binary
container). GLB is the preferred distribution format for runtime loading.
4.1 Required Conventions
| Concern | Requirement |
|---|---|
| Format | glTF 2.0 (.gltf or .glb). GLB preferred for runtime. |
| Coordinate system | glTF standard: +Y up, +Z forward, right-handed. Godot converts to its own convention on import. |
| Units | Meters. Avatar height ~1.8 m at the Root bone. |
| Skeleton | Single skeleton per asset; bone names per §3.1. |
| Skinning | skins + nodes with JOINTS_0 / WEIGHTS_0 accessors. |
| Pose | T-pose at rest. |
| Materials | PBR metallic-roughness. Use KHR_materials_unlit for emissive/UI-only items. |
| Textures | Max 2048×2048; power-of-two preferred. Use KHR_texture_basisu (BasisU) for runtime. |
| Animation | Optional. If present, must not rename or remove canonical bones. |
4.2 Asset Metadata (extras)
Each asset must declare its slot and identity in the glTF asset.extras
object. This is the machine-readable contract consumed by the build pipeline and
runtime.
{
"asset": {
"version": "2.0",
"extras": {
"agp": {
"type": "attachment", // "base_body" | "body_part" | "attachment"
"slot": "chest", // slot ID from §3.2
"id": "shirt_bandit_01", // globally unique asset id
"display_name": "Bandit Shirt",
"version": "1.0.0",
"mount_bone": "Chest", // primary mount bone
"skin_bones": ["Chest", "Spine", "LeftUpperArm", "RightUpperArm"],
"exclusive": true, // matches slot exclusivity
"tags": ["cloth", "common"],
"author": "platform-team"
}
}
}
}
Validation rules for extras.agp:
typemust be one ofbase_body,body_part,attachment.slotmust be a known slot ID (§3.2).mount_bonemust exist in the canonical skeleton.skin_bonesmust all exist in the canonical skeleton.exclusivemust match the slot's declared exclusivity.idmust be unique across the asset catalog.
4.3 Node Naming Convention
To make assets self-describing and mergeable, nodes follow a convention:
- Base body: the mesh node is named
Body; the skeleton root isRoot. - Body part: the mesh node is named
<SlotID>_Mesh(e.g.,head_Mesh). - Attachment: the mesh node is named
<SlotID>_<AssetID>(e.g.,chest_shirt_bandit_01). - Attachment points (empty nodes) on the base body are named
attach_<slot>(e.g.,attach_head,attach_left_hand).
4.4 Material & Texture Requirements
- All materials use the metallic-roughness PBR model.
- Textures are embedded in the GLB or referenced as external files; external references must be relative paths within the asset package.
- Color customization is supported via a
baseColorFactortint applied at runtime (multiplied over the albedo). Assets should use a neutral albedo (white/gray) where tinting is intended. - Masked/transparent materials must set the correct alpha mode
(
MASKorBLEND) andalphaCutoff.
4.5 Skeleton Merging at Runtime
When an attachment is loaded, the runtime merges it onto the base body:
- Load the base body (skeleton + base meshes).
- Load each equipped attachment.
- For each attachment, re-target its skin to the base body's skeleton by bone name (no re-export needed).
- Parent static accessories to their
mount_bone. - Hide base meshes for any equipped body-part slots.
- Apply material tints and merge into the render tree.
The merge is name-based — bone names must match exactly (§3.1). This is the core reason for the strict naming contract.
5. Loadout Model (Network & Persistence)
The avatar's composition is serialized as a loadout — a compact, ordered list of equipped asset IDs. This is what the server validates, persists, and replicates.
5.1 Loadout JSON
{
"base_body": "base_human_male_01",
"slots": {
"head": "head_cyber_01",
"hair": "hair_mohawk_01",
"face": null,
"torso": null,
"chest": "chest_bandit_01",
"back": "backpack_01",
"left_arm": null,
"right_arm": null,
"left_hand": "glove_leather_01",
"right_hand": "glove_leather_01",
"legs": null,
"pants": "pants_cargo_01",
"left_foot": "shoe_boot_01",
"right_foot": "shoe_boot_01",
"accessory": ["belt_01", "necklace_01"]
}
}
Rules:
base_bodyis required and must reference a validbase_bodyasset.- Each exclusive slot holds at most one asset id (or
null). accessoryis an array (0..N, bounded).- The server validates that every id exists in the catalog and that slot exclusivity is respected.
5.2 Network Replication
The loadout is replicated as part of the player's spawn/state data:
spawn_entity:
entity_id: uint32
type: player
transform: { x, y, rot }
props:
loadout_hash: uint32 # hash of the loadout for change detection
loadout: { ... } # full loadout on spawn / on change
- The full loadout is sent on spawn and whenever it changes (reliable).
- A
loadout_hashlets clients skip re-resolving assets if unchanged. - The server is the authority on the loadout; clients send requests to change equipment, and the server validates and broadcasts the result.
5.3 Server-Side Validation
The server rejects a loadout if:
base_bodyis missing or unknown.- Any slot references an unknown asset id.
- An exclusive slot has more than one item.
- An asset's declared
slotdoes not match the slot it is placed in. - The accessory list exceeds the configured limit.
- The asset is not owned/unlocked by the player (for gated cosmetics).
6. Godot Runtime Architecture
6.1 Node Structure
Avatar (Node3D)
├── Skeleton3D # canonical skeleton (from base body)
├── BodyMeshes # base body meshes (skinned to Skeleton3D)
│ ├── head_Mesh
│ ├── torso_Mesh
│ └── ...
├── Attachments # equipped attachments
│ ├── chest_shirt_bandit_01
│ ├── hair_mohawk_01
│ └── ...
└── AttachmentPoints # empty nodes for dynamic mounting
├── attach_head
├── attach_left_hand
└── ...
6.2 Core Classes
| Class | Responsibility |
|---|---|
AvatarSystem |
Loads base body + attachments, resolves loadouts, owns the avatar node. |
AvatarLoader |
glTF/GLB import, skeleton merge, skin re-targeting. |
AvatarSlotRegistry |
Static registry of slot definitions (id, bones, exclusivity). |
AvatarLoadout |
Serializable loadout model + validation. |
AvatarCatalog |
Asset catalog (id → asset metadata + resource path). |
AvatarVisualizer |
Builds the render tree from a loadout. |
6.3 Loading Pipeline
loadout JSON
│
▼
AvatarCatalog.resolve(loadout) # id → asset metadata + GLB path
│
▼
AvatarLoader.load_base_body() # import GLB, extract skeleton + base meshes
│
▼
for each equipped slot:
AvatarLoader.load_attachment()# import GLB, re-target skin to skeleton
│
▼
AvatarVisualizer.assemble() # parent, hide replaced parts, apply tints
│
▼
Avatar node ready for render
6.4 Performance Considerations
- Shared skeleton: All meshes skin to one
Skeleton3D, so skinning is computed once per frame. - Material batching: Attachments that share a material are merged into a single mesh where possible (static accessories).
- LOD: Optional LOD variants per asset; the runtime selects based on distance.
- Caching: Resolved loadouts and imported GLB resources are cached by
AvatarCatalogto avoid re-importing on respawn or re-join.
7. Asset Pipeline & Validation
7.1 Build-Time Validation
A build-time checker validates every asset before it enters the catalog:
- Format check: valid glTF 2.0 / GLB.
- Metadata check:
extras.agppresent and valid (§4.2). - Skeleton check: all
skin_bonesexist in the canonical skeleton. - Slot check:
slotis known andexclusivematches. - Mesh check: meshes are skinned (or static accessories parented to a bone).
- Texture check: sizes, formats, and alpha modes are valid.
- Uniqueness check:
idis unique.
7.2 Catalog Manifest
The catalog is a JSON manifest mapping asset ids to metadata and resource paths:
{
"assets": {
"base_human_male_01": {
"type": "base_body",
"path": "res://assets/avatars/base_human_male_01.glb",
"version": "1.0.0"
},
"chest_bandit_01": {
"type": "attachment",
"slot": "chest",
"path": "res://assets/avatars/chest_bandit_01.glb",
"version": "1.0.0"
}
}
}
8. Security & Anti-Cheat
- Server-authoritative loadouts: Clients only request equipment changes; the server validates against the catalog and the player's owned items.
- No client asset injection: Clients cannot send arbitrary GLB data; they reference catalog ids only.
- Rate limiting: Equipment-change requests are rate-limited per peer.
- Hash verification: The
loadout_hashlets the server detect tampered or stale loadout state.
9. Open Questions / Future Work
- Morph targets / blendshapes: Support for facial expressions and body morphs (e.g., height, muscle) via glTF morph targets.
- Physics attachments: Cloth simulation for capes/skirts (Godot cloth).
- Dynamic tinting UI: A color-picker that writes
baseColorFactortints. - VRM compatibility: Consider supporting VRM (a glTF extension for humanoid avatars) as an import source.
- Attachment LOD streaming: Streaming high-res attachments on demand.
10. Summary
| Concern | Decision |
|---|---|
| Format | glTF 2.0 / GLB (GLB preferred for runtime) |
| Skeleton | Single canonical humanoid skeleton, name-based merge |
| Slots | Fixed slot registry; exclusive body parts + layered attachments |
| Loadout | Compact JSON; server-authoritative and validated |
| Replication | Full loadout on spawn/change + loadout_hash |
| Runtime | Godot Skeleton3D + name-based skin re-targeting |
| Validation | Build-time asset checker + runtime catalog validation |