添加打包动画

This commit is contained in:
Rowland 2026-03-17 09:24:30 +08:00
parent 23472b9639
commit 681d13ea48
4 changed files with 242 additions and 1 deletions

View File

@ -509,12 +509,64 @@ class WebGLPackager:
"material_override": material_override,
"source_model_tag": source_tag,
}
animation = self._extract_animation_settings(node, model_source)
if animation:
entry["animation"] = animation
if textures:
entry["texture_overrides"] = textures
if subnode_overrides:
entry["subnode_overrides"] = subnode_overrides
return entry
def _extract_animation_settings(self, node, model_source: str) -> Dict[str, Any]:
has_animation_hint = False
try:
if node.hasTag("has_animations"):
has_animation_hint = (node.getTag("has_animations").strip().lower() == "true")
except Exception:
has_animation_hint = False
python_animation_flag = self._safe_get_python_tag(node, "animation")
if python_animation_flag is True:
has_animation_hint = True
clip_name = (
self._safe_get_python_tag(node, "selected_animation")
or self._safe_get_tag_value(node, "saved_selected_animation")
or self._safe_get_tag_value(node, "selected_animation")
)
clip_name = str(clip_name).strip() if clip_name is not None else ""
speed_value = (
self._safe_get_python_tag(node, "anim_speed")
or self._safe_get_tag_value(node, "saved_anim_speed")
or self._safe_get_tag_value(node, "anim_speed")
)
speed = self._safe_float(speed_value, 1.0)
if abs(speed) < 1e-4:
speed = 1.0
mode = (
self._safe_get_python_tag(node, "anim_play_mode")
or self._safe_get_tag_value(node, "saved_anim_play_mode")
or self._safe_get_tag_value(node, "anim_play_mode")
or ""
)
mode = str(mode).strip().lower()
if mode not in {"play", "loop", "pause", "stop"}:
mode = "loop"
if not (has_animation_hint or clip_name):
return {}
return {
"enabled": True,
"clip_name": clip_name,
"speed": speed,
"mode": mode,
"autoplay": mode != "stop",
}
def _collect_ssbo_subnode_transform_overrides(self, model_node) -> Dict[str, Any]:
"""
Collect changed subnode transforms from SSBO controller runtime state.
@ -1458,6 +1510,28 @@ class WebGLPackager:
except Exception:
return float(default)
@staticmethod
def _safe_get_python_tag(node, tag_name: str) -> Any:
try:
if hasattr(node, "hasPythonTag") and node.hasPythonTag(tag_name):
return node.getPythonTag(tag_name)
except Exception:
pass
try:
value = node.getPythonTag(tag_name)
return value
except Exception:
return None
@staticmethod
def _safe_get_tag_value(node, tag_name: str) -> str:
try:
if node.hasTag(tag_name):
return (node.getTag(tag_name) or "").strip()
except Exception:
return ""
return ""
def _unique_filename(self, stem: str, suffix: str) -> str:
safe_stem = self._sanitize_filename(stem) or "asset"
key = f"{safe_stem}{suffix.lower()}"

View File

@ -889,6 +889,21 @@ class SceneManagerIOMixin:
node.setTag("saved_model_path", node.getTag("model_path"))
if node.hasTag("can_create_actor_from_memory"):
node.setTag("saved_can_create_actor_from_memory", node.getTag("can_create_actor_from_memory"))
try:
if hasattr(node, "hasPythonTag") and node.hasPythonTag("selected_animation"):
selected_anim = node.getPythonTag("selected_animation")
if selected_anim:
node.setTag("saved_selected_animation", str(selected_anim))
if hasattr(node, "hasPythonTag") and node.hasPythonTag("anim_speed"):
anim_speed = node.getPythonTag("anim_speed")
if anim_speed is not None:
node.setTag("saved_anim_speed", str(float(anim_speed)))
if hasattr(node, "hasPythonTag") and node.hasPythonTag("anim_play_mode"):
anim_mode = node.getPythonTag("anim_play_mode")
if anim_mode:
node.setTag("saved_anim_play_mode", str(anim_mode).lower())
except Exception:
pass
# 保存 SSBO 子物体变换(仅当前受 SSBOEditor 控制的模型根)。
ssbo_subobject_payload = self._serialize_ssbo_subobject_transforms(node)
@ -1537,6 +1552,21 @@ class SceneManagerIOMixin:
self._fixModelStructure(nodePath)
self._restoreModelAnimationInfo(nodePath)
self._processModelAnimations(nodePath)
try:
if nodePath.hasTag("saved_selected_animation"):
selected_anim = nodePath.getTag("saved_selected_animation").strip()
if selected_anim:
nodePath.setPythonTag("selected_animation", selected_anim)
if nodePath.hasTag("saved_anim_speed"):
speed_text = nodePath.getTag("saved_anim_speed").strip()
if speed_text:
nodePath.setPythonTag("anim_speed", float(speed_text))
if nodePath.hasTag("saved_anim_play_mode"):
mode_text = nodePath.getTag("saved_anim_play_mode").strip().lower()
if mode_text:
nodePath.setPythonTag("anim_play_mode", mode_text)
except Exception:
pass
try:
ssbo_editor = getattr(self.world, "ssbo_editor", None)
repair_fn = getattr(ssbo_editor, "_repair_missing_textures", None) if ssbo_editor else None

View File

@ -971,6 +971,90 @@ function directionToThree(THREE, direction, basis) {
return d.normalize();
}
function normalizeAnimationName(name) {
return canonicalizeNameSegment(String(name ?? ""));
}
function pickAnimationClip(clips, requestedName) {
if (!Array.isArray(clips) || clips.length === 0) return null;
const requested = String(requestedName ?? "").trim();
if (!requested) return clips[0];
let clip = clips.find((c) => String(c?.name ?? "") === requested);
if (clip) return clip;
const reqLower = requested.toLowerCase();
clip = clips.find((c) => String(c?.name ?? "").toLowerCase() === reqLower);
if (clip) return clip;
const reqNorm = normalizeAnimationName(requested);
if (reqNorm) {
clip = clips.find((c) => normalizeAnimationName(c?.name ?? "") === reqNorm);
if (clip) return clip;
clip = clips.find((c) => {
const n = normalizeAnimationName(c?.name ?? "");
return n && (n.includes(reqNorm) || reqNorm.includes(n));
});
if (clip) return clip;
}
return clips[0];
}
function setupModelAnimation(THREE, root, gltf, animationConfig, nodeName) {
const config = animationConfig && typeof animationConfig === "object" ? animationConfig : {};
if (config.enabled === false) {
return { mixer: null, clipName: "", mode: "stop", started: false };
}
const clips = Array.isArray(gltf?.animations) ? gltf.animations : [];
if (clips.length === 0) {
if (config.clip_name || config.mode) {
console.warn("[WebGLPack] Animation requested but no clips found:", nodeName || "(unnamed)");
}
return { mixer: null, clipName: "", mode: "stop", started: false };
}
const mode = String(config.mode || "loop").toLowerCase();
const clip = pickAnimationClip(clips, config.clip_name);
if (!clip) {
return { mixer: null, clipName: "", mode, started: false };
}
const mixer = new THREE.AnimationMixer(root);
const action = mixer.clipAction(clip);
const speedRaw = Number(config.speed ?? 1.0);
const speed = Number.isFinite(speedRaw) && Math.abs(speedRaw) > 1e-4 ? speedRaw : 1.0;
action.enabled = true;
action.clampWhenFinished = (mode === "play");
if (mode === "play") {
action.setLoop(THREE.LoopOnce, 1);
} else {
action.setLoop(THREE.LoopRepeat, Infinity);
}
action.setEffectiveTimeScale(speed);
const shouldPlay = Boolean(config.autoplay ?? (mode !== "stop"));
const startTimeRaw = Number(config.start_time ?? 0);
const duration = Math.max(0.0001, Number(clip.duration || 0.0001));
const startTime = Number.isFinite(startTimeRaw) ? (startTimeRaw % duration) : 0;
action.play();
action.time = startTime;
if (!shouldPlay || mode === "stop" || mode === "pause") {
action.paused = true;
}
return {
mixer,
clipName: String(clip.name || ""),
mode,
started: shouldPlay && mode !== "stop" && mode !== "pause",
};
}
async function bootstrap() {
setStatus("Loading WebGL dependencies...");
@ -1082,6 +1166,8 @@ async function bootstrap() {
const nodeMap = new Map();
const pendingModelLoads = [];
const animationMixers = [];
const animationStates = [];
const gltfLoader = new GLTFLoader();
const textureLoader = new THREE.TextureLoader();
@ -1146,6 +1232,24 @@ async function bootstrap() {
);
applyMaterialOverride(THREE, root, node.material_override || null);
applyTextureOverrides(THREE, root, node.texture_overrides || [], textureLoader);
const anim = setupModelAnimation(
THREE,
root,
gltf,
node.animation || null,
node.name || node.id || "",
);
if (anim.mixer) {
animationMixers.push(anim.mixer);
}
if (anim.clipName) {
animationStates.push({
node: node.name || node.id || "node",
clip: anim.clipName,
mode: anim.mode,
started: anim.started,
});
}
obj.add(root);
}
resolve();
@ -1203,7 +1307,7 @@ async function bootstrap() {
resize();
setStatus(
`Scene ready. Nodes: ${(data.nodes || []).length}.\nUse mouse to orbit, wheel to zoom.`,
`Scene ready. Nodes: ${(data.nodes || []).length}, Animations: ${animationStates.length}.\nUse mouse to orbit, wheel to zoom.`,
"ok",
);
@ -1212,6 +1316,13 @@ async function bootstrap() {
requestAnimationFrame(tick);
const dt = clock.getDelta();
if (dt >= 0) {
for (const mixer of animationMixers) {
try {
mixer.update(dt);
} catch (err) {
// Keep render loop alive even if one mixer fails.
}
}
controls.update();
renderer.render(scene, camera);
}

View File

@ -1310,13 +1310,33 @@ class AnimationTools:
try:
origin_model.setPythonTag("selected_animation", current_anim)
origin_model.setTag("saved_selected_animation", str(current_anim))
if owner_model and owner_model != origin_model:
owner_model.setPythonTag("selected_animation", current_anim)
owner_model.setTag("saved_selected_animation", str(current_anim))
except Exception:
pass
return current_anim
def _save_animation_runtime_state(self, origin_model, owner_model=None, mode=None, speed=None):
targets = [origin_model]
if owner_model and owner_model != origin_model:
targets.append(owner_model)
for target in targets:
try:
if mode is not None:
mode_text = str(mode).strip().lower()
target.setPythonTag("anim_play_mode", mode_text)
target.setTag("saved_anim_play_mode", mode_text)
if speed is not None:
speed_value = float(speed)
target.setPythonTag("anim_speed", speed_value)
target.setTag("saved_anim_speed", str(speed_value))
except Exception:
continue
def _should_swap_visibility_for_actor(self, owner_model, actor):
"""
是否需要隐藏原模型/显示Actor的切换
@ -1415,6 +1435,7 @@ class AnimationTools:
except Exception:
pass
actor.play(current_anim)
self._save_animation_runtime_state(origin_model, owner_model, mode="play")
print(f"『动画播放』:{current_anim}")
def _pauseAnimation(self, origin_model):
@ -1455,6 +1476,7 @@ class AnimationTools:
# 停止动画(保持当前姿势)
actor.stop()
self._save_animation_runtime_state(origin_model, owner_model, mode="pause")
print("『动画』暂停")
def _stopAnimation(self, origin_model):
@ -1485,6 +1507,7 @@ class AnimationTools:
# 移除位置维护任务
taskMgr.remove(f"maintain_anim_pos_{id(actor)}")
self._save_animation_runtime_state(origin_model, owner_model, mode="stop")
print("『动画』停止切换至原始模型")
@ -1544,6 +1567,7 @@ class AnimationTools:
except Exception:
pass
actor.loop(current_anim)
self._save_animation_runtime_state(origin_model, owner_model, mode="loop")
print(f"[动画] 循环: {current_anim}")
def _setAnimationSpeed(self, origin_model, speed):
@ -1557,12 +1581,14 @@ class AnimationTools:
current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model)
if current_anim:
actor.setPlayRate(speed, current_anim)
self._save_animation_runtime_state(origin_model, owner_model, speed=speed)
print(f"[动画] 速度设为: {speed} ({current_anim})")
else:
# 兜底:尝试所有动画
anim_names = actor.getAnimNames()
for anim_name in anim_names:
actor.setPlayRate(speed, anim_name)
self._save_animation_runtime_state(origin_model, owner_model, speed=speed)
print(f"[动画] 速度设为: {speed} (所有动画)")