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. -- built without aliases), fallback uses spec default 30.
sprite_reach = 30.0, sprite_reach = 30.0,
} }
-- If texture is loaded, compute sprite_reach from actual sprite dim -- If texture is loaded (atlas-uv path or legacy), compute sprite_reach
if leg_bone.texture_handle and leg_bone.texture_height then -- 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 local anchor_y = (leg_bone.anchor and leg_bone.anchor[2]) or 0
foot_state[i].sprite_reach = leg_bone.texture_height - anchor_y 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 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). -- Rendering (engine.render.*; only callable in render-phase).
-- Walks bones in z-sorted order (back-to-front). For each bone: -- Walks bones in z-sorted order (back-to-front). For each bone:
-- - If bone has texture_handle: draw via engine.render.draw_sprite_transform -- - M.2: If bone has atlas_uv + rig.atlas.diffuse_texture_handle: draw via
-- using cascaded world.scl_x/y and world.rot -- engine.render.draw_sprite_transform with ADR-0044 source-rect args.
-- - Else: fall back to engine.render.draw_rect_rotated with bone.color -- - Else: fall back to engine.render.draw_rect_rotated with bone.color.
-- ==================================================================== -- ====================================================================
function M.render(handle) function M.render(handle)
local p = all_puppets[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). -- v0.4.0: Skip virtual bones (they participate in compute_world but are not drawn).
if b.virtual then goto continue end if b.virtual then goto continue end
local ws = p.bone_state[b.id].world local ws = p.bone_state[b.id].world
if b.texture_handle then if b.atlas_uv and p.rig.atlas and p.rig.atlas.diffuse_texture_handle then
-- Cascaded world-scale (was: per-bone constant b.scale) -- 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 sx, sy = ws.scl_x, ws.scl_y
local ax = b.anchor and b.anchor[1] or 0 local ax = b.anchor and b.anchor[1] or 0
local ay = b.anchor and b.anchor[2] or 0 local ay = b.anchor and b.anchor[2] or 0
local uv = b.atlas_uv
engine.render.draw_sprite_transform( 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), ws.x, ws.y, ws.rot + (b.sprite_rot or 0),
sx, sy, ax, ay, sx, sy, ax, ay,
0xFFFFFFFF 0xFFFFFFFF,
uv.x, uv.y, uv.w, uv.h -- ADR-0044 source-rect args
) )
else else
-- Fallback: colored rotated rect (Phase-1 behavior) -- Fallback: colored rotated rect (Phase-1 behavior)
@@ -1143,7 +1147,8 @@ end
-- call, returning a Lua table directly. No separate decode step needed. -- 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. -- asset_aliases: { [alias-key] = asset-lib-id } from module's manifest.
-- ==================================================================== -- ====================================================================
function M.load_textures(rig, asset_aliases) 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 error("puppet.load_textures: asset-pack alias '" .. rig.asset_pack
.. "' not found in module asset_aliases") .. "' not found in module asset_aliases")
end end
local atlas_path = lib_id .. "/assets/atlas.json" local base = lib_id .. "/assets/atlases/" .. rig.asset_pack
local atlas = engine.asset.load_json(atlas_path) local meta = engine.asset.load_json(base .. "/tiles.atlas.json")
local pack = atlas[rig.asset_pack] if meta.atlas_id ~= rig.asset_pack then
if pack == nil then error(string.format(
error("puppet.load_textures: asset_pack '" .. rig.asset_pack "puppet.load_textures: atlas_id mismatch in %s (json has '%s')",
.. "' not declared in atlas of '" .. lib_id .. "'") base, meta.atlas_id))
end 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 if b.texture then
local entry = pack[b.texture] local tile = by_name[b.texture]
if entry == nil then if tile == nil then
error("puppet.load_textures: atlas-id '" .. b.texture error("puppet.load_textures: atlas '" .. rig.asset_pack
.. "' not in asset_pack '" .. rig.asset_pack .. "' has no tile '" .. b.texture .. "'")
.. "' of '" .. lib_id .. "'")
end end
local tex_path = lib_id .. "/assets/" .. entry.file b.atlas_uv = tile.uv
b.texture_handle = engine.asset.load_texture(tex_path) -- sprite_reach for locomotion: tile height minus anchor.y
-- Cache texture dimensions for locomotion sprite_reach b.texture_height = tile.uv.h
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.
if b.anchor == nil then 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 end
end end

View File

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