增加物理结算
This commit is contained in:
1 parent
650d9c0398
commit
c29de7a26b
4 files changed
+96
-39
No files matched your search
@@ -136,8 +136,12 @@ function readSkeletonData(spine, skeletonPath, atlas, isBinary) {
|
||||
return loader.readSkeletonData(fs.readFileSync(skeletonPath, 'utf8'));
|
||||
}
|
||||
|
||||
function updateWorld(spine, skeleton) {
|
||||
if (spine.Physics && spine.Physics.update !== undefined) skeleton.updateWorldTransform(spine.Physics.update);
|
||||
function updateWorld(spine, skeleton, delta = 0, resetPhysics = false) {
|
||||
if (spine.Physics && spine.Physics.update !== undefined) {
|
||||
if (delta > 0 && typeof skeleton.update === 'function') skeleton.update(delta);
|
||||
const mode = resetPhysics && spine.Physics.reset !== undefined ? spine.Physics.reset : spine.Physics.update;
|
||||
skeleton.updateWorldTransform(mode);
|
||||
}
|
||||
else skeleton.updateWorldTransform();
|
||||
}
|
||||
|
||||
@@ -191,7 +195,7 @@ async function renderThumbnail(options) {
|
||||
utils.applySkin(skeleton, utils.preferredSkin(data));
|
||||
if (typeof skeleton.setToSetupPose === 'function') skeleton.setToSetupPose();
|
||||
else if (typeof skeleton.setupPose === 'function') skeleton.setupPose();
|
||||
updateWorld(spine, skeleton);
|
||||
updateWorld(spine, skeleton, 0, true);
|
||||
|
||||
const bounds = boundsOf(spine, skeleton);
|
||||
|
||||
@@ -275,7 +279,7 @@ function firstAnimation(data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function applyAnimationPose(spine, targetSkeleton, targetState, animation, time) {
|
||||
function applyAnimationPose(spine, targetSkeleton, targetState, animation, time, previousTime) {
|
||||
if (typeof targetState.clearTracks === 'function') targetState.clearTracks();
|
||||
else if (typeof targetState.clearTrack === 'function') targetState.clearTrack(0);
|
||||
if (typeof targetSkeleton.setToSetupPose === 'function') targetSkeleton.setToSetupPose();
|
||||
@@ -283,7 +287,7 @@ function applyAnimationPose(spine, targetSkeleton, targetState, animation, time)
|
||||
if (entry) entry.loop = false;
|
||||
targetState.update(Math.max(0, time));
|
||||
targetState.apply(targetSkeleton);
|
||||
updateWorld(spine, targetSkeleton);
|
||||
updateWorld(spine, targetSkeleton, previousTime === undefined ? 0 : Math.max(0, time - previousTime), previousTime === undefined);
|
||||
}
|
||||
|
||||
function mergeBounds(total, next) {
|
||||
@@ -319,8 +323,10 @@ async function measureVisibleBounds(loaded, targetSkeleton, fallbackBounds, pose
|
||||
try {
|
||||
probe = window.SpineCanvasClipping.createRenderer(loaded.spine, context, loaded.runtimeDir);
|
||||
probe.premultipliedAlpha = loaded.premultipliedAlpha;
|
||||
let previousIndex = -1;
|
||||
for (let index = 0; index < poseCount; index += step) {
|
||||
applyPose(index);
|
||||
applyPose(index, previousIndex);
|
||||
previousIndex = index;
|
||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||
context.clearRect(0, 0, size, size);
|
||||
context.save();
|
||||
@@ -397,13 +403,13 @@ async function renderAnimatedSourceFrames(loaded, animation, options) {
|
||||
const state = new spine.AnimationState(stateData);
|
||||
|
||||
let bounds = null;
|
||||
for (const time of sampleTimes) {
|
||||
applyAnimationPose(spine, skeleton, state, animation, time);
|
||||
for (let index = 0; index < sampleTimes.length; index++) {
|
||||
applyAnimationPose(spine, skeleton, state, animation, sampleTimes[index], index ? sampleTimes[index - 1] : undefined);
|
||||
bounds = mergeBounds(bounds, boundsOf(spine, skeleton));
|
||||
}
|
||||
if (!bounds || bounds.width <= 0 || bounds.height <= 0) throw new Error('骨骼数据没有可绘制的范围');
|
||||
bounds = await measureVisibleBounds(loaded, skeleton, bounds, sampleTimes.length, (index) => {
|
||||
applyAnimationPose(spine, skeleton, state, animation, sampleTimes[index]);
|
||||
bounds = await measureVisibleBounds(loaded, skeleton, bounds, sampleTimes.length, (index, previousIndex) => {
|
||||
applyAnimationPose(spine, skeleton, state, animation, sampleTimes[index], previousIndex >= 0 ? sampleTimes[previousIndex] : undefined);
|
||||
}, options.probe) || bounds;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
@@ -421,7 +427,7 @@ async function renderAnimatedSourceFrames(loaded, animation, options) {
|
||||
renderer = window.SpineCanvasClipping.createRenderer(spine, ctx, loaded.runtimeDir);
|
||||
renderer.premultipliedAlpha = loaded.premultipliedAlpha;
|
||||
for (let index = 0; index < sampleTimes.length; index++) {
|
||||
applyAnimationPose(spine, skeleton, state, animation, sampleTimes[index]);
|
||||
applyAnimationPose(spine, skeleton, state, animation, sampleTimes[index], index ? sampleTimes[index - 1] : undefined);
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.fillStyle = '#1e1e1e';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
@@ -320,6 +320,7 @@
|
||||
let duration = 0; // 当前动画时长
|
||||
let isStaticAnimation = false;
|
||||
const FPS = 30;
|
||||
const MAX_FRAME_DELTA = 0.064;
|
||||
const STATIC_EPSILON = 1e-6;
|
||||
let statusHoldUntil = 0;
|
||||
|
||||
@@ -572,9 +573,13 @@
|
||||
}
|
||||
|
||||
// ---------- 更新动画状态 ----------
|
||||
function updateWorld(sk) {
|
||||
function updateWorld(sk, delta = 0, resetPhysics = false) {
|
||||
if (sk.updateWorldTransform) {
|
||||
if (spine.Physics && spine.Physics.update !== undefined) sk.updateWorldTransform(spine.Physics.update);
|
||||
if (spine.Physics && spine.Physics.update !== undefined) {
|
||||
if (delta > 0 && typeof sk.update === 'function') sk.update(delta);
|
||||
const mode = resetPhysics && spine.Physics.reset !== undefined ? spine.Physics.reset : spine.Physics.update;
|
||||
sk.updateWorldTransform(mode);
|
||||
}
|
||||
else sk.updateWorldTransform();
|
||||
}
|
||||
}
|
||||
@@ -880,7 +885,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function applyAnimationPose(targetSkeleton, targetState, animation, time) {
|
||||
function applyAnimationPose(targetSkeleton, targetState, animation, time, previousTime) {
|
||||
if (typeof targetState.clearTracks === 'function') targetState.clearTracks();
|
||||
else if (typeof targetState.clearTrack === 'function') targetState.clearTrack(0);
|
||||
if (typeof targetSkeleton.setToSetupPose === 'function') targetSkeleton.setToSetupPose();
|
||||
@@ -888,7 +893,7 @@
|
||||
if (entry) entry.loop = false;
|
||||
targetState.update(Math.max(0, time));
|
||||
targetState.apply(targetSkeleton);
|
||||
updateWorld(targetSkeleton);
|
||||
updateWorld(targetSkeleton, previousTime === undefined ? 0 : Math.max(0, time - previousTime), previousTime === undefined);
|
||||
}
|
||||
|
||||
function mergeBounds(total, next) {
|
||||
@@ -916,7 +921,7 @@
|
||||
probeRenderer = clippingRenderer.createRenderer(spine, context, activeRuntimeDir);
|
||||
probeRenderer.premultipliedAlpha = premultipliedAlpha;
|
||||
for (let index = 0; index < poseCount; index++) {
|
||||
applyPose(index);
|
||||
applyPose(index, index - 1);
|
||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||
context.clearRect(0, 0, size, size);
|
||||
context.save();
|
||||
@@ -972,14 +977,14 @@
|
||||
previewStateData.defaultMix = 0;
|
||||
const previewState = new spine.AnimationState(previewStateData);
|
||||
let bounds = null;
|
||||
for (const time of sampleTimes) {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, time);
|
||||
for (let index = 0; index < sampleTimes.length; index++) {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index], index ? sampleTimes[index - 1] : undefined);
|
||||
bounds = mergeBounds(bounds, getBounds(previewSkeleton));
|
||||
}
|
||||
bounds = bounds || stableFitBounds;
|
||||
if (!bounds || bounds.width <= 0 || bounds.height <= 0) throw new Error(t('viewer.emptySkeletonMsg'));
|
||||
bounds = await measureVisibleBounds(previewSkeleton, bounds, sampleTimes.length, (index) => {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index]);
|
||||
bounds = await measureVisibleBounds(previewSkeleton, bounds, sampleTimes.length, (index, previousIndex) => {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index], previousIndex >= 0 ? sampleTimes[previousIndex] : undefined);
|
||||
}) || bounds;
|
||||
|
||||
const off = document.createElement('canvas');
|
||||
@@ -994,7 +999,7 @@
|
||||
offRenderer = clippingRenderer.createRenderer(spine, octx, activeRuntimeDir);
|
||||
offRenderer.premultipliedAlpha = premultipliedAlpha;
|
||||
for (let index = 0; index < sampleTimes.length; index++) {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index]);
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index], index ? sampleTimes[index - 1] : undefined);
|
||||
octx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
octx.clearRect(0, 0, size, size);
|
||||
octx.fillStyle = '#1e1e1e';
|
||||
@@ -1371,10 +1376,12 @@
|
||||
if (skeleton && state) {
|
||||
if (playing) {
|
||||
// Spine 运行时自身处理循环;持续 update 即可。
|
||||
state.update(dt * speed);
|
||||
const frameDelta = Math.min(Math.max(dt, 0), MAX_FRAME_DELTA) * speed;
|
||||
state.update(frameDelta);
|
||||
state.apply(skeleton);
|
||||
updateWorld(skeleton, frameDelta);
|
||||
}
|
||||
updateWorld(skeleton);
|
||||
else updateWorld(skeleton);
|
||||
draw();
|
||||
const entry = currentTrack();
|
||||
if (entry) {
|
||||
@@ -1611,7 +1618,7 @@
|
||||
if (typeof skeleton.setBonesToSetupPose === 'function') skeleton.setBonesToSetupPose();
|
||||
if (typeof skeleton.setSlotsToSetupPose === 'function') skeleton.setSlotsToSetupPose();
|
||||
}
|
||||
updateWorld(skeleton);
|
||||
updateWorld(skeleton, 0, true);
|
||||
updateTimelineDuration(null, true);
|
||||
setStatus(t('viewer.setupPose'));
|
||||
draw();
|
||||
@@ -1693,7 +1700,7 @@
|
||||
}
|
||||
state.update(Math.max(0, time));
|
||||
state.apply(skeleton);
|
||||
updateWorld(skeleton);
|
||||
updateWorld(skeleton, 0, true);
|
||||
draw();
|
||||
updateTimeReadout(time);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const viewer = fs.readFileSync('viewer/spine.html', 'utf8');
|
||||
const releaseViewer = fs.readFileSync('release/UNIVERSAL_SpineViewer/viewer/spine.html', 'utf8');
|
||||
const server = fs.readFileSync('SpineThumbnailGenerateServer/js/renderer.js', 'utf8');
|
||||
|
||||
function functionSource(source, name) {
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
assert.ok(start >= 0, `missing ${name}`);
|
||||
const open = source.indexOf('{', start);
|
||||
let depth = 0;
|
||||
for (let index = open; index < source.length; index++) {
|
||||
if (source[index] === '{') depth++;
|
||||
else if (source[index] === '}' && --depth === 0) return source.slice(start, index + 1);
|
||||
}
|
||||
throw new Error(`unterminated ${name}`);
|
||||
}
|
||||
|
||||
const calls = [];
|
||||
const skeleton = {
|
||||
update(delta) { calls.push(['time', delta]); },
|
||||
updateWorldTransform(mode) { calls.push(['world', mode]); },
|
||||
};
|
||||
vm.runInNewContext(`const spine = { Physics: { update: 'update', reset: 'reset' } }; ${functionSource(viewer, 'updateWorld')}; updateWorld(skeleton, 0.05); updateWorld(skeleton, 0, true);`, { skeleton, calls });
|
||||
assert.deepEqual(calls, [['time', 0.05], ['world', 'update'], ['world', 'reset']]);
|
||||
|
||||
assert.equal(viewer, releaseViewer, 'release viewer must mirror the source viewer');
|
||||
assert.match(viewer, /MAX_FRAME_DELTA = 0\.064[\s\S]*updateWorld\(skeleton, frameDelta\)/);
|
||||
assert.match(viewer, /time - previousTime[\s\S]*previousTime === undefined/);
|
||||
assert.match(server, /skeleton\.update\(delta\)[\s\S]*spine\.Physics\.reset/);
|
||||
assert.match(server, /time - previousTime[\s\S]*previousTime === undefined/);
|
||||
|
||||
console.log('PASS: Spine 4.2+ physics advances time and resets between sampling passes');
|
||||
+21
-14
@@ -320,6 +320,7 @@
|
||||
let duration = 0; // 当前动画时长
|
||||
let isStaticAnimation = false;
|
||||
const FPS = 30;
|
||||
const MAX_FRAME_DELTA = 0.064;
|
||||
const STATIC_EPSILON = 1e-6;
|
||||
let statusHoldUntil = 0;
|
||||
|
||||
@@ -572,9 +573,13 @@
|
||||
}
|
||||
|
||||
// ---------- 更新动画状态 ----------
|
||||
function updateWorld(sk) {
|
||||
function updateWorld(sk, delta = 0, resetPhysics = false) {
|
||||
if (sk.updateWorldTransform) {
|
||||
if (spine.Physics && spine.Physics.update !== undefined) sk.updateWorldTransform(spine.Physics.update);
|
||||
if (spine.Physics && spine.Physics.update !== undefined) {
|
||||
if (delta > 0 && typeof sk.update === 'function') sk.update(delta);
|
||||
const mode = resetPhysics && spine.Physics.reset !== undefined ? spine.Physics.reset : spine.Physics.update;
|
||||
sk.updateWorldTransform(mode);
|
||||
}
|
||||
else sk.updateWorldTransform();
|
||||
}
|
||||
}
|
||||
@@ -880,7 +885,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function applyAnimationPose(targetSkeleton, targetState, animation, time) {
|
||||
function applyAnimationPose(targetSkeleton, targetState, animation, time, previousTime) {
|
||||
if (typeof targetState.clearTracks === 'function') targetState.clearTracks();
|
||||
else if (typeof targetState.clearTrack === 'function') targetState.clearTrack(0);
|
||||
if (typeof targetSkeleton.setToSetupPose === 'function') targetSkeleton.setToSetupPose();
|
||||
@@ -888,7 +893,7 @@
|
||||
if (entry) entry.loop = false;
|
||||
targetState.update(Math.max(0, time));
|
||||
targetState.apply(targetSkeleton);
|
||||
updateWorld(targetSkeleton);
|
||||
updateWorld(targetSkeleton, previousTime === undefined ? 0 : Math.max(0, time - previousTime), previousTime === undefined);
|
||||
}
|
||||
|
||||
function mergeBounds(total, next) {
|
||||
@@ -916,7 +921,7 @@
|
||||
probeRenderer = clippingRenderer.createRenderer(spine, context, activeRuntimeDir);
|
||||
probeRenderer.premultipliedAlpha = premultipliedAlpha;
|
||||
for (let index = 0; index < poseCount; index++) {
|
||||
applyPose(index);
|
||||
applyPose(index, index - 1);
|
||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||
context.clearRect(0, 0, size, size);
|
||||
context.save();
|
||||
@@ -972,14 +977,14 @@
|
||||
previewStateData.defaultMix = 0;
|
||||
const previewState = new spine.AnimationState(previewStateData);
|
||||
let bounds = null;
|
||||
for (const time of sampleTimes) {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, time);
|
||||
for (let index = 0; index < sampleTimes.length; index++) {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index], index ? sampleTimes[index - 1] : undefined);
|
||||
bounds = mergeBounds(bounds, getBounds(previewSkeleton));
|
||||
}
|
||||
bounds = bounds || stableFitBounds;
|
||||
if (!bounds || bounds.width <= 0 || bounds.height <= 0) throw new Error(t('viewer.emptySkeletonMsg'));
|
||||
bounds = await measureVisibleBounds(previewSkeleton, bounds, sampleTimes.length, (index) => {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index]);
|
||||
bounds = await measureVisibleBounds(previewSkeleton, bounds, sampleTimes.length, (index, previousIndex) => {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index], previousIndex >= 0 ? sampleTimes[previousIndex] : undefined);
|
||||
}) || bounds;
|
||||
|
||||
const off = document.createElement('canvas');
|
||||
@@ -994,7 +999,7 @@
|
||||
offRenderer = clippingRenderer.createRenderer(spine, octx, activeRuntimeDir);
|
||||
offRenderer.premultipliedAlpha = premultipliedAlpha;
|
||||
for (let index = 0; index < sampleTimes.length; index++) {
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index]);
|
||||
applyAnimationPose(previewSkeleton, previewState, animation, sampleTimes[index], index ? sampleTimes[index - 1] : undefined);
|
||||
octx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
octx.clearRect(0, 0, size, size);
|
||||
octx.fillStyle = '#1e1e1e';
|
||||
@@ -1371,10 +1376,12 @@
|
||||
if (skeleton && state) {
|
||||
if (playing) {
|
||||
// Spine 运行时自身处理循环;持续 update 即可。
|
||||
state.update(dt * speed);
|
||||
const frameDelta = Math.min(Math.max(dt, 0), MAX_FRAME_DELTA) * speed;
|
||||
state.update(frameDelta);
|
||||
state.apply(skeleton);
|
||||
updateWorld(skeleton, frameDelta);
|
||||
}
|
||||
updateWorld(skeleton);
|
||||
else updateWorld(skeleton);
|
||||
draw();
|
||||
const entry = currentTrack();
|
||||
if (entry) {
|
||||
@@ -1611,7 +1618,7 @@
|
||||
if (typeof skeleton.setBonesToSetupPose === 'function') skeleton.setBonesToSetupPose();
|
||||
if (typeof skeleton.setSlotsToSetupPose === 'function') skeleton.setSlotsToSetupPose();
|
||||
}
|
||||
updateWorld(skeleton);
|
||||
updateWorld(skeleton, 0, true);
|
||||
updateTimelineDuration(null, true);
|
||||
setStatus(t('viewer.setupPose'));
|
||||
draw();
|
||||
@@ -1693,7 +1700,7 @@
|
||||
}
|
||||
state.update(Math.max(0, time));
|
||||
state.apply(skeleton);
|
||||
updateWorld(skeleton);
|
||||
updateWorld(skeleton, 0, true);
|
||||
draw();
|
||||
updateTimeReadout(time);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
Reference in new issue
Block a user