添加打包动画
This commit is contained in:
parent
23472b9639
commit
681d13ea48
@ -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()}"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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} (所有动画)")
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user