# 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:** - **`exclusive`** slots allow at most one equipped item at a time. - The **`accessory`** slot 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: 1. Parent static accessories to the mount bone. 2. 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. ```json { "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`:** - `type` must be one of `base_body`, `body_part`, `attachment`. - `slot` must be a known slot ID (§3.2). - `mount_bone` must exist in the canonical skeleton. - `skin_bones` must all exist in the canonical skeleton. - `exclusive` must match the slot's declared exclusivity. - `id` must 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 is `Root`. - **Body part:** the mesh node is named `_Mesh` (e.g., `head_Mesh`). - **Attachment:** the mesh node is named `_` (e.g., `chest_shirt_bandit_01`). - **Attachment points** (empty nodes) on the base body are named `attach_` (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 `baseColorFactor` tint 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 (`MASK` or `BLEND`) and `alphaCutoff`. ### 4.5 Skeleton Merging at Runtime When an attachment is loaded, the runtime **merges** it onto the base body: 1. Load the base body (skeleton + base meshes). 2. Load each equipped attachment. 3. For each attachment, **re-target its skin** to the base body's skeleton by bone name (no re-export needed). 4. Parent static accessories to their `mount_bone`. 5. Hide base meshes for any equipped body-part slots. 6. 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 ```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_body` is required and must reference a valid `base_body` asset. - Each exclusive slot holds **at most one** asset id (or `null`). - `accessory` is 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_hash`** lets 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_body` is missing or unknown. - Any slot references an unknown asset id. - An exclusive slot has more than one item. - An asset's declared `slot` does 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 `AvatarCatalog` to 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: 1. **Format check:** valid glTF 2.0 / GLB. 2. **Metadata check:** `extras.agp` present and valid (§4.2). 3. **Skeleton check:** all `skin_bones` exist in the canonical skeleton. 4. **Slot check:** `slot` is known and `exclusive` matches. 5. **Mesh check:** meshes are skinned (or static accessories parented to a bone). 6. **Texture check:** sizes, formats, and alpha modes are valid. 7. **Uniqueness check:** `id` is unique. ### 7.2 Catalog Manifest The catalog is a JSON manifest mapping asset ids to metadata and resource paths: ```json { "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_hash` lets 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 `baseColorFactor` tints. - **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 |