Files

5.3 KiB

Layered avatar renderer

avatar_layers implements IAvatarRenderer from avatar_base, registered as layers through LayeredAvatarRendererFactory. Content mods depend on avatar_layers>=1; no dependency on characters_base, dialog_base or Live2D is required.

This is a working discrete renderer, not a character-art generator. Authors supply aligned PNG layers and a Kind: avatar definition. For preset semantics and save state see avatar_base.

Files and bindings

my_content/
  manifest.json                       depends: ["avatar_layers>=1"]
  avatars/marao.json                   Kind: avatar, Renderer: layers
  avatar_layers/marao/body.png
  avatar_layers/marao/eyes_opened.png
  avatar_layers/marao/eyes_closed.png

Image paths include .png and are relative to avatar_layers/. They are resolved through mod providers, so ZIP mods and dependent content overrides work without direct filesystem loading. Use a content-specific prefix such as marao/ to avoid collisions with unrelated content.

A complete resource:

{
  "Kind": "avatar",
  "Items": [{
    "Id": "my_content/marao",
    "Renderer": "layers",
    "BaseValues": {
      "canvas": {"Width": 512, "Height": 1024},
      "layers/body": {"Image": "marao/body.png", "Order": 0}
    },
    "Groups": {
      "eyes_state": {
        "Default": "opened",
        "Presets": {
          "opened": {"Values": {
            "layers/eyes": {"Image": "marao/eyes_opened.png", "Order": 10}
          }},
          "closed": {"Values": {
            "layers/eyes": {"Image": "marao/eyes_closed.png", "Order": 10}
          }}
        }
      }
    }
  }]
}

Draw eyes onto transparent images aligned to the same logical canvas as the body. To use cropped images instead, set X, Y, Width, Height on the layer. Coordinates are logical canvas pixels, not normalized viewport coordinates. The host may scale the visual root to fit a portrait slot. Root layout belongs to scenes/avatar_layers/view.tscn; layer children are data-driven.

Accepted bindings:

Binding Meaning
canvas Object with positive finite Width and Height; defaults are 512 and 1024
layers/<id> as string Exact PNG path, full-canvas rectangle, Order 0
layers/<id> as object AvatarLayerDefinition settings below
layers/<id> as null No layer

Layer properties:

Property Default Meaning
Image required when visible PNG path including extension
Order 0 Ascending drawing order; ties use ordinal layer binding ID
X, Y 0 Top-left position in logical pixels
Width, Height 0 Rectangle dimensions; zero uses the corresponding canvas dimension
Visible true False omits the layer, without requiring/loading an image
Opacity 1 Alpha in [0,1]
FlipH, FlipV false Image mirroring inside its rectangle

Unknown bindings and unknown object properties are rejected to expose typos. Coordinates must be finite, dimensions nonnegative. A preset replaces a binding object whole: when changing an image, repeat any non-default bounds/order needed by that layer. Logical binding order and compositing layer order are separate: group Priority resolves competing targets; layer Order controls drawing.

Hosting and changing a preset

IAvatar avatar = AvatarCatalog.Create("my_content/marao");
IAvatarRenderer renderer = AvatarRenderers.Create(avatar);
host.AddChild(renderer.View);
avatar.ApplyPresets(new Dictionary<string, string> { ["eyes_state"] = "closed" });
renderer.Apply(avatar.Resolve());
// Before freeing host or unloading the renderer mod:
renderer.Dispose();

Run visual operations on the Godot runtime main thread. Positive transition times are accepted but images switch immediately: no interpolated anatomy or crossfade is implied. Rendering or image validation errors leave the previous valid visual target intact. Logical preset state is separate: if an owner changed it before a rendering error, it must handle that error and choose another target.

Each renderer owns its node tree and image cache. Unchanged images are reused across Apply calls; unused textures are disposed after old nodes release them. No shared engine texture cache is mutated. Dispose is idempotent, detaches the root and frees nodes before textures. Missing bindings remove previous layers; hidden layers do not load assets. Applying after disposal is an error.

Not implemented: per-layer motion/tweens, clipping masks, skeletal deformation, automatic blinking, scenario commands, editor UI, and automatic dialog integration.

Tests

The opt-in contracts suite contains synthetic red/green/blue PNG fixtures, a real avatar entity, and integration tests for image switching, draw order, positioning, invalid targets, resource ownership and rendered pixel colors. These fixtures do not mount in normal games.

godot --headless --path . -- --frontend --with-mods=avatar_layers --run-tests --mod=avatar_layers
godot --path . --rendering-method gl_compatibility -- --frontend --with-mods=avatar_layers --itest=avatar_layers_contracts --run-tests --mod=avatar_layers__contracts

The second command needs an actual rendering window; the headless dummy renderer cannot verify pixels. In this workspace execute these commands through the shared Windows watcher job bridge.