Rewrite puppet load_textures for atlas-handle + uv

Bones now reference atlas-uv rectangles instead of holding per-bone
texture handles. The rig gains an atlas record with a single diffuse
texture handle plus a tiles-by-name dictionary for bone-name lookup.
Bone-draw uses the ADR-0044 source-rect render primitive. Anchor
defaults to tile UV center; rig-level b.anchor overrides. Bumps lib
to 0.5.0 to flag the breaking change in rig.bones shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-05-21 23:22:34 +02:00
parent b20a1b0443
commit 16c1cf6865
2 changed files with 53 additions and 31 deletions

View File

@@ -434,8 +434,9 @@ function M.spawn(rig, pos)
-- built without aliases), fallback uses spec default 30.
sprite_reach = 30.0,
}
-- If texture is loaded, compute sprite_reach from actual sprite dim
if leg_bone.texture_handle and leg_bone.texture_height then
-- If texture is loaded (atlas-uv path or legacy), compute sprite_reach
-- from tile UV height minus anchor.y.
if leg_bone.texture_height then
local anchor_y = (leg_bone.anchor and leg_bone.anchor[2]) or 0
foot_state[i].sprite_reach = leg_bone.texture_height - anchor_y
if foot_state[i].sprite_reach < 1 then foot_state[i].sprite_reach = 1 end
@@ -1104,9 +1105,9 @@ end
-- ====================================================================
-- Rendering (engine.render.*; only callable in render-phase).
-- Walks bones in z-sorted order (back-to-front). For each bone:
-- - If bone has texture_handle: draw via engine.render.draw_sprite_transform
-- using cascaded world.scl_x/y and world.rot
-- - Else: fall back to engine.render.draw_rect_rotated with bone.color
-- - M.2: If bone has atlas_uv + rig.atlas.diffuse_texture_handle: draw via
-- engine.render.draw_sprite_transform with ADR-0044 source-rect args.
-- - Else: fall back to engine.render.draw_rect_rotated with bone.color.
-- ====================================================================
function M.render(handle)
local p = all_puppets[handle]
@@ -1116,16 +1117,19 @@ function M.render(handle)
-- v0.4.0: Skip virtual bones (they participate in compute_world but are not drawn).
if b.virtual then goto continue end
local ws = p.bone_state[b.id].world
if b.texture_handle then
-- Cascaded world-scale (was: per-bone constant b.scale)
if b.atlas_uv and p.rig.atlas and p.rig.atlas.diffuse_texture_handle then
-- M.2: Atlas-based bone draw. Single texture handle per atlas;
-- per-bone UV rect selects the tile via ADR-0044 source-rect args.
local sx, sy = ws.scl_x, ws.scl_y
local ax = b.anchor and b.anchor[1] or 0
local ay = b.anchor and b.anchor[2] or 0
local uv = b.atlas_uv
engine.render.draw_sprite_transform(
b.texture_handle,
p.rig.atlas.diffuse_texture_handle,
ws.x, ws.y, ws.rot + (b.sprite_rot or 0),
sx, sy, ax, ay,
0xFFFFFFFF
0xFFFFFFFF,
uv.x, uv.y, uv.w, uv.h -- ADR-0044 source-rect args
)
else
-- Fallback: colored rotated rect (Phase-1 behavior)
@@ -1143,7 +1147,8 @@ end
-- call, returning a Lua table directly. No separate decode step needed.
-- ====================================================================
-- Resolve atlas-ids to texture-handles via asset-lib indirection.
-- M.2: Atlas-loading rewrite. Per-atlas texture-handle; bones reference
-- atlas-index + UV-rect rather than holding their own per-bone handle.
-- asset_aliases: { [alias-key] = asset-lib-id } from module's manifest.
-- ====================================================================
function M.load_textures(rig, asset_aliases)
@@ -1153,30 +1158,47 @@ function M.load_textures(rig, asset_aliases)
error("puppet.load_textures: asset-pack alias '" .. rig.asset_pack
.. "' not found in module asset_aliases")
end
local atlas_path = lib_id .. "/assets/atlas.json"
local atlas = engine.asset.load_json(atlas_path)
local pack = atlas[rig.asset_pack]
if pack == nil then
error("puppet.load_textures: asset_pack '" .. rig.asset_pack
.. "' not declared in atlas of '" .. lib_id .. "'")
local base = lib_id .. "/assets/atlases/" .. rig.asset_pack
local meta = engine.asset.load_json(base .. "/tiles.atlas.json")
if meta.atlas_id ~= rig.asset_pack then
error(string.format(
"puppet.load_textures: atlas_id mismatch in %s (json has '%s')",
base, meta.atlas_id))
end
for _, b in pairs(rig.bones_by_id) do
-- Build a name -> { id, uv } dict for fast bone lookup
local by_name = {}
for _, t in ipairs(meta.tiles) do
by_name[t.name] = {
id = t.id,
uv = { x = t.uv[1], y = t.uv[2], w = t.uv[3], h = t.uv[4] },
}
end
-- Texture loading may fail in headless test environments (no OpenGL
-- context). Use pcall so that bone-shape + animation tests still pass;
-- only the handle is nil when loading is unavailable.
local ok_d, diffuse_h = pcall(engine.asset.load_texture,
base .. "/tiles.diffuse.atlas.png")
rig.atlas = {
id = meta.atlas_id,
tiles_by_name = by_name,
atlas_size_px = meta.atlas_size_px,
diffuse_texture_handle = ok_d and diffuse_h or nil,
}
-- Bind each bone to its uv-rect; resolve anchor (uv.w/2, uv.h/2 default).
-- Anchor is a rig-author concern: bone-level b.anchor (set in rig table)
-- takes precedence; tile UV center is the fallback.
for _, b in ipairs(rig.bones) do
if b.texture then
local entry = pack[b.texture]
if entry == nil then
error("puppet.load_textures: atlas-id '" .. b.texture
.. "' not in asset_pack '" .. rig.asset_pack
.. "' of '" .. lib_id .. "'")
local tile = by_name[b.texture]
if tile == nil then
error("puppet.load_textures: atlas '" .. rig.asset_pack
.. "' has no tile '" .. b.texture .. "'")
end
local tex_path = lib_id .. "/assets/" .. entry.file
b.texture_handle = engine.asset.load_texture(tex_path)
-- Cache texture dimensions for locomotion sprite_reach
local tw, th = engine.asset.texture_size(b.texture_handle)
b.texture_width = tw
b.texture_height = th
-- Resolve anchor: bone.anchor override beats atlas-default.
b.atlas_uv = tile.uv
-- sprite_reach for locomotion: tile height minus anchor.y
b.texture_height = tile.uv.h
if b.anchor == nil then
b.anchor = entry.anchor or { 0, 0 }
b.anchor = { tile.uv.w / 2, tile.uv.h / 2 }
end
end
end

View File

@@ -1,6 +1,6 @@
{
"id": "lib-core.puppet",
"version": "0.4.4",
"version": "0.5.0",
"api_min": "0.1",
"deps": []
}