fix arrival animation, fix pidgey regression
This commit is contained in:
+50
-1
@@ -286,6 +286,53 @@
|
||||
the shot driver's `DS_FAINT` case prints the frame the bar empties against
|
||||
the frame the animation starts, and they are the same frame.
|
||||
|
||||
- **The first Pokemon of a battle arrived, left, and arrived again.** Every
|
||||
guard deciding whether the player's Pokemon is on the field is a field the
|
||||
engine sets once the battle is RUNNING, and during the opening none of them
|
||||
is set yet: `showPlayerBack` is still nil (BattleState assigns it further
|
||||
in), `playerBackPic` is nil with it, and `sendingOut` does not go true until
|
||||
the ball is thrown. So the whole intro read as "this Pokemon is standing on
|
||||
the field" -- two and a half seconds of it, on its tile, playing its standby
|
||||
loop, before the trainer sprite it is meant to be hiding behind had even
|
||||
appeared. It then vanished when that sprite arrived and came back with its
|
||||
entrance when the ball opened. A switch has no intro, which is why a switch
|
||||
always looked right and was the thing worth comparing against. The player's
|
||||
side is now simply not on the field during the intro phase.
|
||||
|
||||
- **The anchor made birds shake, and the shake read as fast flapping.** The
|
||||
body estimate it corrects toward was the MEDIAN bone origin -- robust to a
|
||||
few bones flung out, which is what it was chosen for, and wrong in a way
|
||||
that only shows on a flapping model: a median is a RANK, and on a bird most
|
||||
of the skeleton is wing, so which bone sits at the middle of the sorted list
|
||||
swaps between the up cluster and the down one every beat. Measured, the
|
||||
estimate moved a tenth of a body-height between adjacent half-frames on
|
||||
Pidgey and three whole body-heights on Pidgeot, and the anchor turned that
|
||||
into a translation of the entire Pokemon -- the body counter-shaking against
|
||||
its own wings, which reads as flapping at twice the real speed.
|
||||
|
||||
The centre is now the bone origins averaged and **weighted by how many
|
||||
vertices each bone moves**. The weights are a property of the mesh, computed
|
||||
once, so there is no rank to flip -- and a bone with little geometry barely
|
||||
counts, which is the robustness the median was for in the first place
|
||||
(Farfetch'd's trail is 30 vertices on five bones). On top of that the
|
||||
correction is low-passed, so what survives is where the Pokemon has drifted
|
||||
to and never how it is shaking on the way. Pidgey's shake goes from 0.24 to
|
||||
**0.10 pixels a frame** on a fourteen-pixel model, and travel correction
|
||||
improved with it.
|
||||
|
||||
Two species -- Pidgeot and Dodrio -- have standby loops with genuinely junk
|
||||
rotation frames, which move the estimate three and two body-heights in a
|
||||
single frame against a fifth of a body-height for the fastest real motion in
|
||||
the set. No filter separates those: rate-limiting the correction bounded the
|
||||
shake but put 33 of the 148 entrances back outside the frame, and
|
||||
rate-limiting the measurement could not tell a spike from an excursion
|
||||
because they are only a factor of fifteen apart. So each species' own
|
||||
standby loop is walked once, at rig construction, and one whose estimate is
|
||||
that unsteady is **not anchored at all**: it travels as far as its animation
|
||||
says and does not vibrate, which is exactly how it behaved before the anchor
|
||||
existed. One species trading a framing problem for no problem beats 147
|
||||
trading a solved framing problem for a shake.
|
||||
|
||||
- **A Pokemon calling out a fifth species killed every model in the fight.**
|
||||
Reported as `Cannot use object after it has been released` out of
|
||||
`Voxel3D.draw`, after which nothing 3D drew for the rest of the battle.
|
||||
@@ -411,7 +458,9 @@
|
||||
- Pidgeot, Dodrio and Grimer have a handful of erratic rotation frames in
|
||||
their standby loops -- a few frames of junk at the top of the loop rather
|
||||
than a corrupt stream. The blend guard holds those frames rather than
|
||||
smoothing through them, so they step where the source steps. Not chased
|
||||
smoothing through them, so they step where the source steps, and Pidgeot
|
||||
and Dodrio are additionally left unanchored because those frames move the
|
||||
body estimate too far to measure against (see above). Not chased
|
||||
further: the extraction is byte-identical to a reference pipeline whose
|
||||
sampling was validated against the decompilation's own arithmetic, and
|
||||
guessing at the format to fix three species risks the other 145.
|
||||
|
||||
@@ -206,6 +206,29 @@ local function onField(battle, side, mon)
|
||||
if battle.enemyHidden or battle.enemySendingOut then return false end
|
||||
else
|
||||
if battle.safari or battle.demo or battle.sendingOut then return false end
|
||||
-- ------- and not before the battle has even opened
|
||||
--
|
||||
-- The player's Pokemon is not out during the INTRO. Every other guard
|
||||
-- here is a field the engine sets once the battle is running, and during
|
||||
-- the opening none of them is set yet: `showPlayerBack` is still nil
|
||||
-- (BattleState assigns it further in, when the back pic is built),
|
||||
-- `playerBackPic` is nil with it, and `sendingOut` does not go true until
|
||||
-- the ball is actually thrown. So the whole opening read as "this
|
||||
-- Pokemon is standing on the field" and the model was drawn through it --
|
||||
-- two and a half seconds of it, on its tile, playing its standby loop,
|
||||
-- before the trainer sprite it is supposed to be hiding behind had even
|
||||
-- appeared. It then vanished when that sprite arrived and came back with
|
||||
-- its entrance when the ball opened, so the first Pokemon of a battle
|
||||
-- appeared, left and arrived again.
|
||||
--
|
||||
-- A SWITCH has no intro, which is why a switch always looked right and
|
||||
-- was the thing worth comparing against.
|
||||
--
|
||||
-- Gated on the PHASE rather than on a flag latched at the send-out: a
|
||||
-- latch that never fires (a link battle, a script pushing a battle
|
||||
-- straight to the menu) would hide the Pokemon for good, and being wrong
|
||||
-- in that direction is far worse than the two seconds this fixes.
|
||||
if battle.phase == "intro" then return false end
|
||||
end
|
||||
local ok, hidden = pcall(battle.fxHidden, battle, battler)
|
||||
if ok and hidden then return false end
|
||||
|
||||
+5
-1
@@ -287,6 +287,10 @@ end
|
||||
-- ------- per frame
|
||||
|
||||
function StadiumMon:update(dt)
|
||||
-- kept for build(), which runs later in the same frame and needs it to
|
||||
-- advance the anchor's filter (StadiumRig.anchor). Stashed before the
|
||||
-- early-outs below, so a species with nothing to play still has one.
|
||||
self.dt = dt or 0
|
||||
local model = self.model
|
||||
if not (model and self.anim) then return end
|
||||
local anim = model.anims[self.anim]
|
||||
@@ -393,7 +397,7 @@ function StadiumMon:build()
|
||||
-- and then back onto the tile, because these animations were authored for
|
||||
-- a camera that followed the Pokemon and this one does not move (see
|
||||
-- StadiumRig.anchor)
|
||||
self.rig:anchor(StadiumMon.TRAVEL)
|
||||
self.rig:anchor(StadiumMon.TRAVEL, self.dt)
|
||||
self.rig:skin(self.yaw or 0)
|
||||
-- no clock of its own: the texture animation rides the frame pose() just
|
||||
-- resolved, which is what keeps a blink inside its standby loop and a
|
||||
|
||||
+199
-28
@@ -437,35 +437,80 @@ end
|
||||
-- they were; past it the excess alone is removed, so a big move still reads
|
||||
-- as big and still comes back to the tile it left.
|
||||
--
|
||||
-- ------- and why the MEDIAN bone
|
||||
-- ------- where the body IS, and why it is not the median
|
||||
--
|
||||
-- The centre is the median bone origin on each axis, not the mean and not
|
||||
-- the root. The mean is dragged by exactly the thing that must not count --
|
||||
-- Farfetch'd's five-bone trail streaks three thousand units out while the
|
||||
-- bird stays put -- and the root is a bone like any other, which several
|
||||
-- species animate independently of the body hanging off it. The median is
|
||||
-- the position most of the skeleton agrees on, and a handful of bones flung
|
||||
-- anywhere cannot move it.
|
||||
-- The first version of this took the median bone origin, on the reasoning
|
||||
-- that a handful of bones flung anywhere cannot move a median. True, and it
|
||||
-- had a worse problem: a median is a RANK, and a rank flips. On a bird most
|
||||
-- of the skeleton is wing, so as the wings beat, which bone sits at the
|
||||
-- middle of the sorted list swaps between the up cluster and the down one --
|
||||
-- and the estimate jumps with it. Measured on Pidgey's standby loop the
|
||||
-- median moved a tenth of a body-height between adjacent half-frames, and on
|
||||
-- Pidgeot three whole body-heights. The anchor turns that straight into a
|
||||
-- translation of the ENTIRE Pokemon, so the body counter-shook against its
|
||||
-- own wings and the flapping read as twice its real speed. That is the
|
||||
-- "Pidgey's wings flap super fast" this comment exists because of.
|
||||
--
|
||||
-- The centre is now the bone origins averaged, WEIGHTED BY HOW MANY VERTICES
|
||||
-- EACH BONE MOVES. That fixes both halves at once:
|
||||
--
|
||||
-- * the weights are a property of the MESH, computed once and never
|
||||
-- changing, so there is no rank to flip and no discontinuity available
|
||||
-- to it -- the estimate is as smooth as the bones themselves
|
||||
-- * a bone with little geometry on it barely counts, which is exactly the
|
||||
-- robustness the median was for. Farfetch'd's trail is thirty vertices
|
||||
-- on five bones -- 1.6% of the model -- so streaking three thousand
|
||||
-- units out moves this by nothing worth measuring
|
||||
--
|
||||
-- Against the median it is two to five times smoother on every species
|
||||
-- tested and measures the same travel to within a few percent.
|
||||
|
||||
-- How far the body estimate may move in ONE 30 Hz frame of a species' own
|
||||
-- standby loop before that species is judged unmeasurable and left
|
||||
-- unanchored (see measureBind). The fastest genuine motion in the set is
|
||||
-- about a fifth of a body-height a frame; the one species that fails this
|
||||
-- moves three.
|
||||
StadiumRig.ANCHOR_STEADY = 0.5
|
||||
|
||||
-- Which context slot the standby loop is, without requiring StadiumPack --
|
||||
-- this module is below it and a require would be circular. Position 1 of
|
||||
-- StadiumPack.CONTEXT, which is the format's own contract.
|
||||
local IDLE_SLOT = 1
|
||||
|
||||
-- How much of the model each bone actually carries. Cached on the shared
|
||||
-- model: it is a fact about the mesh, not about this instance.
|
||||
local function boneWeights(model)
|
||||
if model.boneW then return model.boneW, model.boneWTotal end
|
||||
local w, total = {}, 0
|
||||
for b = 1, model.boneCount do w[b] = 0 end
|
||||
for _, prim in ipairs(model.prims) do
|
||||
local bone = prim.bone
|
||||
for k = 1, prim.vertCount do
|
||||
local b = bone[k]
|
||||
if w[b] then w[b] = w[b] + 1; total = total + 1 end
|
||||
end
|
||||
end
|
||||
model.boneW, model.boneWTotal = w, total
|
||||
return w, total
|
||||
end
|
||||
|
||||
-- The body centre of the pose currently in drawM.
|
||||
local function centre(self, n)
|
||||
-- made on demand as well as in new(), so the probes and the QA sweep --
|
||||
-- which build a rig with no meshes by hand, because pose() needs none --
|
||||
-- can measure without having to know about this scratch
|
||||
local xs, ys, zs = self.cx, self.cy, self.cz
|
||||
if not xs then
|
||||
xs, ys, zs = {}, {}, {}
|
||||
self.cx, self.cy, self.cz = xs, ys, zs
|
||||
end
|
||||
local model = self.model
|
||||
local w, total = boneWeights(model)
|
||||
if not (total > 0) then return nil end
|
||||
local x, y, z = 0, 0, 0
|
||||
local d = self.drawM
|
||||
for b = 1, n do
|
||||
local o = (b - 1) * 12
|
||||
xs[b], ys[b], zs[b] = d[o + 4], d[o + 8], d[o + 12]
|
||||
local q = w[b]
|
||||
if q and q > 0 then
|
||||
local o = (b - 1) * 12
|
||||
x = x + d[o + 4] * q
|
||||
y = y + d[o + 8] * q
|
||||
z = z + d[o + 12] * q
|
||||
end
|
||||
end
|
||||
for i = n + 1, #xs do xs[i], ys[i], zs[i] = nil, nil, nil end
|
||||
table.sort(xs) table.sort(ys) table.sort(zs)
|
||||
local h = floor(n / 2) + 1
|
||||
return xs[h], ys[h], zs[h]
|
||||
return x / total, y / total, z / total
|
||||
end
|
||||
|
||||
-- Where the BIND pose puts it -- the spot every animation is measured
|
||||
@@ -481,11 +526,117 @@ function StadiumRig:measureBind()
|
||||
if model.bindCX then return end
|
||||
self:pose(nil, 0, false)
|
||||
model.bindCX, model.bindCY, model.bindCZ = centre(self, model.boneCount)
|
||||
|
||||
-- ------- and whether this species can be anchored at all
|
||||
--
|
||||
-- Decided ONCE, per model, offline, by walking its standby loop and asking
|
||||
-- how far the body estimate moves between one frame and the next.
|
||||
--
|
||||
-- Everything the anchor does rests on that estimate being a description of
|
||||
-- where the Pokemon is. For 147 species it is: the fastest real motion in
|
||||
-- the set moves the body about a fifth of a body-height per 30 Hz frame.
|
||||
-- Pidgeot's standby loop moves it THREE, because a few of its rotation
|
||||
-- frames are junk (the worst data in the set, and a known issue in its own
|
||||
-- right). There is no filter setting that both tracks a real excursion and
|
||||
-- rejects that -- measured, at four time constants, either the excursions
|
||||
-- came back or the shake did -- because the two are only a factor of
|
||||
-- fifteen apart and a filter is a proportion.
|
||||
--
|
||||
-- So a species whose own idle says its estimate cannot be trusted is not
|
||||
-- anchored, and plays exactly as it did before the anchor existed: it
|
||||
-- travels as far as its animation says, and it does not vibrate. One
|
||||
-- species trading a framing problem for no problem beats 147 trading a
|
||||
-- solved framing problem for a shake.
|
||||
--
|
||||
-- Cheap: forty-odd poses on a model that is about to be posed sixty times
|
||||
-- a second anyway.
|
||||
local idle = model.ctx and model.ctx[IDLE_SLOT]
|
||||
local anim = (idle and idle ~= 0xFFFF) and (idle + 1) or nil
|
||||
local rec = anim and model.anims and model.anims[anim]
|
||||
model.anchorOk = true
|
||||
if rec and rec.frames and rec.frames > 1 then
|
||||
local root = model.rootScale
|
||||
if not (root and root > 0) then root = 1 end
|
||||
local h = (model.height or 0) / root
|
||||
if h > 0 then
|
||||
local px, py, pz, worst = nil, nil, nil, 0
|
||||
for f = 0, rec.frames - 1 do
|
||||
self:pose(anim, f, true)
|
||||
local x, y, z = centre(self, model.boneCount)
|
||||
if x and px then
|
||||
local d = (((x - px) ^ 2 + (y - py) ^ 2 + (z - pz) ^ 2) ^ 0.5) / h
|
||||
if d > worst then worst = d end
|
||||
end
|
||||
px, py, pz = x, y, z
|
||||
end
|
||||
if worst > StadiumRig.ANCHOR_STEADY then
|
||||
model.anchorOk = false
|
||||
V.mod.log:info("stadium: species %s moves its own body %.1f "
|
||||
.. "body-heights in one frame of its standby loop -- "
|
||||
.. "not anchoring it, the measurement cannot be "
|
||||
.. "trusted", tostring(model.species), worst)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- and leave the bind pose behind, not the last frame of the idle
|
||||
self:pose(nil, 0, false)
|
||||
end
|
||||
|
||||
-- ------- and why the offset is SMOOTHED
|
||||
--
|
||||
-- A better centre is not enough on its own. Any estimate that follows the
|
||||
-- pose carries the pose's own frame-to-frame wobble into it, and the anchor
|
||||
-- multiplies that up into a translation of the whole Pokemon -- so a species
|
||||
-- whose source data is erratic (Pidgeot's standby loop has a few frames of
|
||||
-- junk in it, and no estimator can smooth data that is genuinely wrong)
|
||||
-- would shake bodily rather than in the one bone that is wrong.
|
||||
--
|
||||
-- So the offset is low-passed. What the anchor is FOR is a slow excursion --
|
||||
-- a Pokemon swimming seven body-heights away over two seconds -- and that
|
||||
-- survives a filter with this time constant untouched, while anything
|
||||
-- oscillating frame to frame is flattened. The correction ends up describing
|
||||
-- where the Pokemon has drifted TO, never how it is shaking on the way.
|
||||
--
|
||||
-- HALF_LIFE is in seconds: the time the offset takes to close half of any
|
||||
-- gap between where it is and where the pose says it should be. Short enough
|
||||
-- that a real excursion is caught within a few frames of starting, long
|
||||
-- enough that a 30 Hz wobble does not survive it.
|
||||
StadiumRig.ANCHOR_HALF_LIFE = 0.05
|
||||
|
||||
|
||||
-- ------- what this does NOT fix, and why it stops here
|
||||
--
|
||||
-- The filter is a proportion, so it divides the input wobble down rather than
|
||||
-- bounding it -- and one species' data is bad enough to get through anyway.
|
||||
-- Pidgeot's standby loop carries a few frames of junk rotation (the worst in
|
||||
-- the set, and a known issue since before the anchor existed), which moves
|
||||
-- the body estimate three body-heights inside a single frame; filtered, that
|
||||
-- is still about three pixels a frame on a fourteen-pixel model.
|
||||
--
|
||||
-- Two further mechanisms were built and MEASURED against the set, and both
|
||||
-- were taken back out:
|
||||
--
|
||||
-- a rate limit on the correction bounded the shake to a third of a pixel,
|
||||
-- and cost so much tracking that 33 of the 148 entrances went back to
|
||||
-- leaving the frame -- half the problem the anchor exists to solve
|
||||
--
|
||||
-- a rate limit on the MEASUREMENT, to tell a spike from an excursion by
|
||||
-- speed, could not separate them: the fastest real excursion (Dewgong's
|
||||
-- entrance, five and a half body-heights a second) is close enough to
|
||||
-- Pidgeot's sustained junk that any threshold either clipped Dewgong or
|
||||
-- passed Pidgeot, and freezing on distrust made both worse
|
||||
--
|
||||
-- So it stops here, at the setting that is right for the 147 species whose
|
||||
-- data is not broken. Pidgeot is a data problem and belongs with the other
|
||||
-- data problems in the CHANGELOG's Known section, not in this control loop:
|
||||
-- the alternative was distorting every other Pokemon's animation to flatter
|
||||
-- one whose source frames are wrong.
|
||||
|
||||
-- Pull the pose back toward the tile. `limit` is in the Pokemon's own
|
||||
-- body-heights; nil or a non-positive value leaves the pose exactly as posed.
|
||||
function StadiumRig:anchor(limit)
|
||||
-- `dt` is the frame's own delta; without one the offset is applied whole,
|
||||
-- which is what a still (the QA sweep, a probe) wants.
|
||||
function StadiumRig:anchor(limit, dt)
|
||||
if not (limit and limit > 0) then return end
|
||||
local model = self.model
|
||||
local n = model.boneCount
|
||||
@@ -498,16 +649,36 @@ function StadiumRig:anchor(limit)
|
||||
|
||||
local bx, by, bz = model.bindCX, model.bindCY, model.bindCZ
|
||||
if not bx then return end -- never measured; leave the pose alone
|
||||
if model.anchorOk == false then return end -- and unmeasurable, at that
|
||||
local x, y, z = centre(self, n)
|
||||
if not x then return end
|
||||
|
||||
local dx, dy, dz = x - bx, y - by, z - bz
|
||||
local dist = (dx * dx + dy * dy + dz * dz) ^ 0.5
|
||||
local allow = limit * h
|
||||
if dist <= allow or dist <= 0 then return end
|
||||
|
||||
-- the EXCESS only: what is inside the limit stays, so the motion keeps its
|
||||
-- shape and only the part that would leave the frame is removed
|
||||
local k = (dist - allow) / dist
|
||||
local ox, oy, oz = dx * k, dy * k, dz * k
|
||||
-- what the pose alone asks for: the EXCESS beyond the limit, so what is
|
||||
-- inside it stays and the motion keeps its shape
|
||||
local ox, oy, oz = 0, 0, 0
|
||||
if dist > allow and dist > 0 then
|
||||
local k = (dist - allow) / dist
|
||||
ox, oy, oz = dx * k, dy * k, dz * k
|
||||
end
|
||||
|
||||
-- and then toward it rather than straight to it (see ANCHOR_HALF_LIFE),
|
||||
-- and never faster than ANCHOR_RATE
|
||||
if dt and dt > 0 then
|
||||
local half = StadiumRig.ANCHOR_HALF_LIFE
|
||||
local a = (half > 0) and (1 - 0.5 ^ (dt / half)) or 1
|
||||
if a > 1 then a = 1 end
|
||||
local px, py, pz = self.anchorX or ox, self.anchorY or oy, self.anchorZ or oz
|
||||
ox = px + (ox - px) * a
|
||||
oy = py + (oy - py) * a
|
||||
oz = pz + (oz - pz) * a
|
||||
end
|
||||
self.anchorX, self.anchorY, self.anchorZ = ox, oy, oz
|
||||
if ox == 0 and oy == 0 and oz == 0 then return end
|
||||
|
||||
local pivot, drw = self.pivotM, self.drawM
|
||||
for b = 1, n do
|
||||
local o = (b - 1) * 12
|
||||
|
||||
@@ -1419,15 +1419,29 @@ end)()
|
||||
accX = {}, accY = {}, accZ = {}, parts = {} }, Rig)
|
||||
rig:measureBind()
|
||||
|
||||
local function centre()
|
||||
local n, xs, ys, zs = model.boneCount, {}, {}, {}
|
||||
for b = 1, n do
|
||||
local o = (b - 1) * 12
|
||||
xs[b], ys[b], zs[b] = rig.drawM[o + 4], rig.drawM[o + 8], rig.drawM[o + 12]
|
||||
-- the same quantity StadiumRig.anchor corrects -- bone origins averaged and
|
||||
-- weighted by the vertices each bone moves -- written out here rather than
|
||||
-- called, so this checks the behaviour and not its own arithmetic
|
||||
local weight, total = {}, 0
|
||||
for b = 1, model.boneCount do weight[b] = 0 end
|
||||
for _, prim in ipairs(model.prims) do
|
||||
for k = 1, prim.vertCount do
|
||||
local b = prim.bone[k]
|
||||
if weight[b] then weight[b] = weight[b] + 1; total = total + 1 end
|
||||
end
|
||||
table.sort(xs) table.sort(ys) table.sort(zs)
|
||||
local h = math.floor(n / 2) + 1
|
||||
return xs[h], ys[h], zs[h]
|
||||
end
|
||||
local function centre()
|
||||
local x, y, z = 0, 0, 0
|
||||
for b = 1, model.boneCount do
|
||||
local q = weight[b]
|
||||
if q > 0 then
|
||||
local o = (b - 1) * 12
|
||||
x = x + rig.drawM[o + 4] * q
|
||||
y = y + rig.drawM[o + 8] * q
|
||||
z = z + rig.drawM[o + 12] * q
|
||||
end
|
||||
end
|
||||
return x / total, y / total, z / total
|
||||
end
|
||||
local raw = model.height / (model.rootScale > 0 and model.rootScale or 1)
|
||||
local slot = model.ctx[Pack.SLOT.entrance]
|
||||
|
||||
+47
-16
@@ -81,6 +81,10 @@ local UNTEXTURED = 0xFFFF + 1
|
||||
-- than reported (see the texture check).
|
||||
local untextured = {}
|
||||
|
||||
-- species the rig refuses to anchor because their own standby loop says the
|
||||
-- body estimate cannot be trusted (StadiumRig.ANCHOR_STEADY)
|
||||
local unanchored = {}
|
||||
|
||||
-- How far the mode lets an animation carry the Pokemon off its tile, in body
|
||||
-- heights. Read from StadiumMon rather than repeated, so the sweep cannot go
|
||||
-- on passing against a number the mode has since changed.
|
||||
@@ -226,24 +230,40 @@ end
|
||||
|
||||
-- ------- one animation, frame by frame
|
||||
|
||||
-- Where the BODY of a posed rig is: the median bone origin on each axis.
|
||||
-- Where the BODY of a posed rig is: the bone origins averaged, weighted by
|
||||
-- how many vertices each bone moves.
|
||||
--
|
||||
-- The median rather than the mean or the root, for the reason StadiumRig's
|
||||
-- own anchor uses it -- Farfetch'd's five-bone trail streaks three thousand
|
||||
-- units out while the bird stays where it is, and a mean would follow the
|
||||
-- trail. This is deliberately a second, independent implementation of the
|
||||
-- same idea: a check that shared the code it is checking would agree with it
|
||||
-- by construction.
|
||||
-- The same QUANTITY StadiumRig.anchor corrects, written out a second time
|
||||
-- rather than called: a check that shared the code it is checking would agree
|
||||
-- with it by construction. It must be the same quantity, though -- an earlier
|
||||
-- version of this measured the MEDIAN instead, and reported 27,965 findings
|
||||
-- purely because it was asking a different question than the anchor answers.
|
||||
local function bodyCentreOf(rig)
|
||||
local n = rig.model.boneCount
|
||||
local xs, ys, zs, d = {}, {}, {}, rig.drawM
|
||||
for b = 1, n do
|
||||
local o = (b - 1) * 12
|
||||
xs[b], ys[b], zs[b] = d[o + 4], d[o + 8], d[o + 12]
|
||||
local m = rig.model
|
||||
local w, total = m.qaWeights, m.qaWeightTotal
|
||||
if not w then
|
||||
w, total = {}, 0
|
||||
for b = 1, m.boneCount do w[b] = 0 end
|
||||
for _, prim in ipairs(m.prims) do
|
||||
for k = 1, prim.vertCount do
|
||||
local b = prim.bone[k]
|
||||
if w[b] then w[b] = w[b] + 1; total = total + 1 end
|
||||
end
|
||||
end
|
||||
m.qaWeights, m.qaWeightTotal = w, total
|
||||
end
|
||||
table.sort(xs) table.sort(ys) table.sort(zs)
|
||||
local h = math.floor(n / 2) + 1
|
||||
return xs[h], ys[h], zs[h]
|
||||
if not (total > 0) then return 0, 0, 0 end
|
||||
local x, y, z, d = 0, 0, 0, rig.drawM
|
||||
for b = 1, m.boneCount do
|
||||
local q = w[b]
|
||||
if q > 0 then
|
||||
local o = (b - 1) * 12
|
||||
x = x + d[o + 4] * q
|
||||
y = y + d[o + 8] * q
|
||||
z = z + d[o + 12] * q
|
||||
end
|
||||
end
|
||||
return x / total, y / total, z / total
|
||||
end
|
||||
|
||||
local function bboxOf(rig)
|
||||
@@ -300,6 +320,7 @@ local function sweepSpecies(dex)
|
||||
-- and where the bind pose puts the BODY, for the travel check below. The
|
||||
-- tracks are in raw units, before the model_root scale model.height is
|
||||
-- measured after.
|
||||
if model.anchorOk == false then unanchored[dex] = true end
|
||||
local bcx, bcy, bcz = bodyCentreOf(rig)
|
||||
local rawHeight = (model.height or 0)
|
||||
/ ((model.rootScale or 0) > 0 and model.rootScale or 1)
|
||||
@@ -360,7 +381,10 @@ local function sweepSpecies(dex)
|
||||
local cx, cy, cz = bodyCentreOf(rig)
|
||||
local drift = (((cx - bcx) ^ 2 + (cy - bcy) ^ 2 + (cz - bcz) ^ 2) ^ 0.5)
|
||||
/ rawHeight
|
||||
if drift > TRAVEL * 1.05 then
|
||||
-- a species the rig has judged unmeasurable is deliberately NOT
|
||||
-- anchored (StadiumRig.measureBind), so of course it still travels --
|
||||
-- reporting that would be reporting a decision as a defect
|
||||
if model.anchorOk ~= false and drift > TRAVEL * 1.05 then
|
||||
report("animation walks the Pokemon off its tile", dex, name, f,
|
||||
("body centre %.1f body-heights from where it started")
|
||||
:format(drift))
|
||||
@@ -548,6 +572,13 @@ if nUntextured > 0 then
|
||||
print(("species with flat-shaded (untextured) primitives: %d -- expected, "
|
||||
.. "the source has no texture for those"):format(nUntextured))
|
||||
end
|
||||
local anchorList = {}
|
||||
for dex in pairs(unanchored) do anchorList[#anchorList + 1] = dex end
|
||||
table.sort(anchorList)
|
||||
if #anchorList > 0 then
|
||||
print(("not anchored (standby loop too unsteady to measure, so they travel "
|
||||
.. "as authored): %s"):format(table.concat(anchorList, " ")))
|
||||
end
|
||||
if #staticPose > 0 then
|
||||
print(("staticPose (declined by the packer, never drawn, not swept): %s")
|
||||
:format(table.concat(staticPose, " ")))
|
||||
|
||||
Reference in New Issue
Block a user