ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenMontage Three.js 动画系统实战:关键帧、骨骼动画、Morph Targets 与动作混合

OpenMontage Three.js 动画系统实战:关键帧、骨骼动画、Morph Targets 与动作混合 OpenMontage Three.js 动画系统实战关键帧、骨骼动画、Morph Targets 与动作混合【免费下载链接】OpenMontageWorlds first open-source, agentic video production system. 12 production pipelines, 100 tools, 700 agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage导读本文围绕 OpenMontage 仓库中面向 AI Agent 的.agents/skills/threejs-animation/SKILL.md技能文档展开系统讲解 Three.js 动画系统的三大核心组件AnimationClip、AnimationMixer、AnimationAction、六类 KeyframeTrack、GLTF 骨骼动画加载、Morph Targets 形变混合、基于权重的动作混合与叠加混合以及平滑阻尼、弹簧物理、振荡三类程序化动画模式。这套技能在 OpenMontage 的 3D 世界中台threejs_world工具 HyperFrames 渲染链路中有着直接落点Agent 可以利用本文的知识为地形、地标与 GLTF 资产编写可编辑的浏览器原生动画再交给视频合成管线输出成片。读完本文你将掌握从逐帧驱动到动作编排的完整 Three.js 动画能力并能理解这些能力如何嵌入 OpenMontage 的确定性世界生成与渲染流程。Three.js 动画系统总览Three.js 的动画系统由三个协同工作的核心组件构成这也是整份技能文档的主线AnimationClip—— 关键帧数据的容器描述一段动画里各属性在时间轴上的取值变化AnimationMixer—— 动画播放器挂在某个根对象模型或场景节点上负责驱动该对象及其后代节点上的所有动画AnimationAction—— 单个剪辑的播放控制器管理播放/暂停、循环、速度、权重、淡入淡出等行为。三者关系可以概括为Clip 定义数据Mixer 负责执行Action 控制播放。一个 Mixer 可以同时持有多个 Action这正是动作混合Animation Blending得以实现的基础。快速开始最简程序化动画循环技能文档给出的 Quick Start 展示了最基本的动画骨架——用THREE.Clock提供稳定的帧间隔而不是依赖Date.now()或浏览器时间戳import * as THREE from three; // Simple procedural animation const clock new THREE.Clock(); function animate() { const delta clock.getDelta(); const elapsed clock.getElapsedTime(); mesh.rotation.y delta; mesh.position.y Math.sin(elapsed) * 0.5; requestAnimationFrame(animate); renderer.render(scene, camera); } animate();这里的关键点是clock.getDelta()本次帧与上次帧的时间差与clock.getElapsedTime()动画启动后的累计时间的区分基于 delta 的写法用于累加式运动如旋转增量基于 elapsed 的写法用于时间函数驱动的运动如正弦起伏。在 OpenMontage 的 world-runtime.js 中这一模式被进一步演化为时间驱动渲染通过监听hf-seek事件按绝对时间renderAt(time)渲染每一帧配合 GSAP 时间线见 index.html 中的gsap.timeline从而让动画与视频时间轴严格对齐。AnimationClip关键帧数据的容器AnimationClip 存放一段动画的关键帧数据。一个最简剪辑由属性路径、关键帧时间点、对应取值三要素构成// Create animation clip const times [0, 1, 2]; // Keyframe times (seconds) const values [0, 1, 0]; // Values at each keyframe const track new THREE.NumberKeyframeTrack( .position[y], // Property path times, values, ); const clip new THREE.AnimationClip(bounce, 2, [track]);AnimationClip构造函数签名是(name, duration, tracks)其中duration可以显式给出如果不给Three.js 会根据各轨道的最晚关键帧时间自动计算。属性路径支持点号与方括号两种语法如.position[y]、.material.opacity方括号可用于带下标或名称的字段例如 Morph Target 的.morphTargetInfluences[smile]。KeyframeTrack 类型六种常用轨道技能文档系统罗列了六种轨道类型分别对应不同的属性数据类型// Number track (single value) new THREE.NumberKeyframeTrack(.opacity, times, [1, 0]); new THREE.NumberKeyframeTrack(.material.opacity, times, [1, 0]); // Vector track (position, scale) new THREE.VectorKeyframeTrack(.position, times, [ 0, 0, 0, // t0 1, 2, 0, // t1 0, 0, 0, // t2 ]); // Quaternion track (rotation) const q1 new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)); const q2 new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0)); new THREE.QuaternionKeyframeTrack( .quaternion, [0, 1], [q1.x, q1.y, q1.z, q1.w, q2.x, q2.y, q2.z, q2.w], ); // Color track new THREE.ColorKeyframeTrack(.material.color, times, [ 1, 0, 0, // red 0, 1, 0, // green 0, 0, 1, // blue ]); // Boolean track new THREE.BooleanKeyframeTrack(.visible, [0, 0.5, 1], [true, false, true]); // String track (for morph targets) new THREE.StringKeyframeTrack( .morphTargetInfluences[smile], [0, 1], [0, 1], );各轨道的数据长度规则值得注意Vector 轨道每个关键帧 3 个分量Quaternion 轨道每个关键帧 4 个分量x/y/z/wColor 轨道每个关键帧 3 个分量r/g/b数值数组必须与times长度严格对应。Rotatiton 必须使用四元数轨道而非欧拉角轨道以避免万向锁与插值歧义。插值模式轨道默认线性插值也可切换为样条平滑或离散跳变const track new THREE.VectorKeyframeTrack(.position, times, values); // Interpolation track.setInterpolation(THREE.InterpolateLinear); // Default track.setInterpolation(THREE.InterpolateSmooth); // Cubic spline track.setInterpolation(THREE.InterpolateDiscrete); // Step functionInterpolateLinear默认值相邻关键帧直线过渡InterpolateSmooth三次样条插值曲线平滑、无拐点突变适合相机路径与有机形变InterpolateDiscrete阶跃函数只在关键帧处切换取值适合布尔开关或离散状态动画。AnimationMixer动画的执行器Mixer 将一段或多段剪辑应用到对象及其后代节点上是动画系统的心脏const mixer new THREE.AnimationMixer(model); // Create action from clip const action mixer.clipAction(clip); action.play(); // Update in animation loop function animate() { const delta clock.getDelta(); mixer.update(delta); // Required! requestAnimationFrame(animate); renderer.render(scene, camera); }最容易踩的坑是忘记调用mixer.update(delta)。Mixer 内部基于累计时间推进update参数是帧间隔秒而不是绝对时间只有每一帧都调用它Action 的播放、循环、权重混合才会被真正计算并写回对象的属性。技能的 GLTF 示例中同样强调Store mixer for update loop并在动画循环里以if (window.mixer) window.mixer.update(delta)的方式更新。Mixer 事件Mixer 在播放到关键节点时会派发事件可用于编排后续逻辑如播完开场动画后切换状态机mixer.addEventListener(finished, (e) { console.log(Animation finished:, e.action.getClip().name); }); mixer.addEventListener(loop, (e) { console.log(Animation looped:, e.action.getClip().name); });事件对象e.action指向触发事件的 Action通过getClip().name可取得对应剪辑名便于区分多个动画。AnimationAction播放控制中枢Action 是日常开发中接触最多的对象技能文档对其 API 做了全景式归纳可分为五组播放控制const action mixer.clipAction(clip); // Playback control action.play(); action.stop(); action.reset(); action.halt(fadeOutDuration); // Playback state action.isRunning(); action.isScheduled();play()只负责计划播放真正的状态由isScheduled()已进入播放队列与isRunning()当前帧实际生效区分reset()将动作重置到剪辑起点再播放halt(duration)则在一个给定的淡出时长后停止。时间控制// Time control action.time 0.5; // Current time action.timeScale 1; // Playback speed (negative reverse) action.paused false;timeScale支持负值实现倒放也支持 0.5、2.0 等变速效果paused用于临时冻结。权重混合的基石// Weight (for blending) action.weight 1; // 0-1, contribution to final pose action.setEffectiveWeight(1);weight表示该 Action 对最终姿态的贡献比例0 到 1多个 Action 同时播放时最终姿态是各 Action 按权重加权的结果。setEffectiveWeight会同时考虑全局权重开关与淡入淡出系数返回实际生效的权重。循环模式// Loop modes action.loop THREE.LoopRepeat; // Default: loop forever action.loop THREE.LoopOnce; // Play once and stop action.loop THREE.LoopPingPong; // Alternate forward/backward action.repetitions 3; // Number of loops (Infinity default) // Clamping action.clampWhenFinished true; // Hold last frame when doneLoopRepeat默认模式无限循环repetitions默认InfinityLoopOnce只播放一次LoopPingPong正向播放后反向播放适合呼吸、摇晃等往复运动clampWhenFinished配合LoopOnce使用播完后钉在最后一帧而不是跳回起点。混合模式// Blending action.blendMode THREE.NormalAnimationBlendMode; action.blendMode THREE.AdditiveAnimationBlendMode;NormalAnimationBlendMode是常规的加权混合AdditiveAnimationBlendMode是叠加混合在基础姿态上叠加差值详见下文叠加混合。淡入淡出与交叉过渡// Fade in action.reset().fadeIn(0.5).play(); // Fade out action.fadeOut(0.5); // Crossfade between animations const action1 mixer.clipAction(clip1); const action2 mixer.clipAction(clip2); action1.play(); // Later, crossfade to action2 action1.crossFadeTo(action2, 0.5, true); action2.play();fadeIn/fadeOut是带时长的权重渐变crossFadeTo(otherAction, duration, warp)实现两段动作间的平滑过渡第三个参数warp为true时还会对时间比例做 warp 校正避免过渡期间动作速度不一致导致的跳变。这是角色从待机切到走路再切到奔跑的标准做法。加载 GLTF 骨骼动画GLTF/GLB 是骨骼动画最常见的来源。技能文档给出了从加载、取剪辑、按名播放到接入循环的完整流程import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader.js; const loader new GLTFLoader(); loader.load(model.glb, (gltf) { const model gltf.scene; scene.add(model); // Create mixer const mixer new THREE.AnimationMixer(model); // Get all clips const clips gltf.animations; console.log( Available animations:, clips.map((c) c.name), ); // Play first animation if (clips.length 0) { const action mixer.clipAction(clips[0]); action.play(); } // Play specific animation by name const walkClip THREE.AnimationClip.findByName(clips, Walk); if (walkClip) { mixer.clipAction(walkClip).play(); } // Store mixer for update loop window.mixer mixer; }); // Animation loop function animate() { const delta clock.getDelta(); if (window.mixer) window.mixer.update(delta); requestAnimationFrame(animate); renderer.render(scene, camera); }要点在于Mixer 必须挂在gltf.scene这个根对象上而不是挂在单个网格上否则骨骼驱动链路Mixer → 骨骼 → SkinnedMesh 顶点不会生效。AnimationClip.findByName(clips, Walk)是查找命名剪辑的标准方式。OpenMontage 中的 GLTF 加载实践在 OpenMontage 的 3D 世界中台里GLTF 资产的加载不是手写loader.load而是由 threejs_asset_catalog.py 先安装权属清晰的本地资产目录内置 Kenney Nature Kit、Fantasy Town Kit、Survival Kit 等 CC0 目录再由 world-runtime.js 在production质量档下用GLTFLoader.loadAsync异步加载调色板模型async function loadProductionPalette() { if (qualityTier ! production) return; const loader new GLTFLoader(); const prototypes new Map(); const palette WORLD_SPEC.asset_palette || []; await Promise.all(palette.map(async (entry) { const key ${entry.catalog_id}:${entry.model_id}; const path catalogModels.get(key); if (!path || prototypes.has(key)) return; const gltf await loader.loadAsync(path); gltf.scene.traverse((node) { if (!node.isMesh) return; node.castShadow renderMode cinematic; node.receiveShadow renderMode cinematic; if (node.material) node.material.envMapIntensity 0.8; }); prototypes.set(key, gltf.scene); })); // ... 按世界 spec 的 asset_palette 布局克隆实例 }这段源码体现了动画技能在真实工程中的两个重要补充其一加载完成后用traverse统一设置阴影与材质参数避免模型自带材质与环境不匹配其二用prototype.clone(true)深拷贝同一份加载结果生成大量实例避免为每个散布点重复走网络加载。这与技能文档Share clips的性能建议一脉相承——同一份资源Clip 或 Scene 原型可以在多个对象上复用。骨骼动画Skeletal Animation访问骨架与骨骼// Access skeleton from skinned mesh const skinnedMesh model.getObjectByProperty(type, SkinnedMesh); const skeleton skinnedMesh.skeleton; // Access bones skeleton.bones.forEach((bone) { console.log(bone.name, bone.position, bone.rotation); }); // Find specific bone by name const headBone skeleton.bones.find((b) b.name Head); if (headBone) headBone.rotation.y Math.PI / 4; // Turn head // Skeleton helper const helper new THREE.SkeletonHelper(model); scene.add(helper);SkeletonHelper用于调试——它会画出骨骼的可视化连线方便确认骨骼层级与动画作用范围。程序化骨骼动画不依赖剪辑直接逐帧修改骨骼姿态适合活着感的动态呼吸、头部跟随等function animate() { const time clock.getElapsedTime(); // Animate bone const headBone skeleton.bones.find((b) b.name Head); if (headBone) { headBone.rotation.y Math.sin(time) * 0.3; } // Update mixer if also playing clips mixer.update(clock.getDelta()); }注意程序化修改骨骼与 Mixer 播放剪辑可以并存但 Mixer 的update仍须调用否则剪辑驱动的骨骼会停在原地、与程序化骨骼产生冲突。骨骼挂点附件系统武器、道具等物体可以挂到骨骼上随骨骼一起运动// Attach object to bone const weapon new THREE.Mesh(weaponGeometry, weaponMaterial); const handBone skeleton.bones.find((b) b.name RightHand); if (handBone) handBone.add(weapon); // Offset attachment weapon.position.set(0, 0, 0.5); weapon.rotation.set(0, Math.PI / 2, 0);把网格add到Bone节点后它的变换就进入骨骼局部坐标系骨骼旋转时武器自然跟随偏移量通过挂点自身的position/rotation调整。Morph Targets形状混合动画Morph Targets形变目标/混合变形在两个或多个网格形状之间做顶点级混合常用于面部表情// Morph targets are stored in geometry const geometry mesh.geometry; console.log(Morph attributes:, Object.keys(geometry.morphAttributes)); // Access morph target influences mesh.morphTargetInfluences; // Array of weights mesh.morphTargetDictionary; // Name - index mapping // Set morph target by index mesh.morphTargetInfluences[0] 0.5; // Set by name const smileIndex mesh.morphTargetDictionary[smile]; mesh.morphTargetInfluences[smileIndex] 1;morphTargetInfluences是每个形变目标的权重数组01morphTargetDictionary提供名称 → 索引的映射按名操作比按裸索引更抗重构。两种驱动方式Morph 权重既可以用代码实时驱动也可以用关键帧轨道驱动// Procedural function animate() { const t clock.getElapsedTime(); mesh.morphTargetInfluences[0] (Math.sin(t) 1) / 2; } // With keyframe animation const track new THREE.NumberKeyframeTrack( .morphTargetInfluences[smile], [0, 0.5, 1], [0, 1, 0], ); const clip new THREE.AnimationClip(smile, 1, [track]); mixer.clipAction(clip).play();程序化方式的优势是实时反馈例如根据音频响度驱动张嘴幅度关键帧方式则适合与整体动画时间轴严格同步。技能文档中的 String 轨道写法.morphTargetInfluences[smile]配0/1是兼容某些 GLTF 导出器将 morph 名称写入 string track 的另一种路径。动画混合Animation Blending基于权重的状态混合典型场景根据角色移动速度在 idle/walk/run 三个动作间连续过渡// Setup actions const idleAction mixer.clipAction(idleClip); const walkAction mixer.clipAction(walkClip); const runAction mixer.clipAction(runClip); // Play all with different weights idleAction.play(); walkAction.play(); runAction.play(); // Set initial weights idleAction.setEffectiveWeight(1); walkAction.setEffectiveWeight(0); runAction.setEffectiveWeight(0); // Blend based on speed function updateAnimations(speed) { if (speed 0.1) { idleAction.setEffectiveWeight(1); walkAction.setEffectiveWeight(0); runAction.setEffectiveWeight(0); } else if (speed 5) { const t speed / 5; idleAction.setEffectiveWeight(1 - t); walkAction.setEffectiveWeight(t); runAction.setEffectiveWeight(0); } else { const t Math.min((speed - 5) / 5, 1); idleAction.setEffectiveWeight(0); walkAction.setEffectiveWeight(1 - t); runAction.setEffectiveWeight(t); } }这种写法的核心思想是所有候选动作始终保持play()状态只动态调整权重权重之和恒为 1从而得到数学上连续的姿态插值。相比先 fadeOut 一个再 fadeIn 另一个权重混合更平滑、无重叠权重缺口。叠加混合Additive Blending叠加层用于在基础动作之上叠加细碎动态而不干扰基础姿态// Base pose const baseAction mixer.clipAction(baseClip); baseAction.play(); // Additive layer (e.g., breathing) const additiveAction mixer.clipAction(additiveClip); additiveAction.blendMode THREE.AdditiveAnimationBlendMode; additiveAction.play(); // Convert clip to additive THREE.AnimationUtils.makeClipAdditive(additiveClip);典型用法是基础剪辑播跑步叠加剪辑播呼吸起伏两者互不干扰。AnimationUtils.makeClipAdditive(clip)会把一段普通剪辑原地转换为叠加剪辑可传入参考剪辑/参考帧指定差值基准。动画工具函数技能文档还汇总了AnimationUtils的常用工具适合剪辑复用与后处理import * as THREE from three; // Find clip by name const clip THREE.AnimationClip.findByName(clips, Walk); // Create subclip const subclip THREE.AnimationUtils.subclip(clip, subclip, 0, 30, 30); // Convert to additive THREE.AnimationUtils.makeClipAdditive(clip); THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip); // Clone clip const clone clip.clone(); // Get clip duration clip.duration; // Optimize clip (remove redundant keyframes) clip.optimize(); // Reset clip to first frame clip.resetDuration();其中subclip(clip, name, startFrame, endFrame, fps)从原剪辑截取一段按帧号换算时间optimize()删除冗余关键帧同一取值区间内的多余帧在保证视觉不变的前提下压缩数据量resetDuration()让duration回到按轨道重新计算的正确值常用于对剪辑做编辑之后。程序化动画模式除了剪辑驱动的录制式动画技能文档提供了三类高频复用的程序化运动模式它们不依赖动画系统直接修改对象变换特别适合相机跟随、UI 弹性和装饰物运动。平滑阻尼Smooth DampingUnity 风格SmoothDamp的三维实现特点是加速度随时间自然衰减、无过冲且帧率无关// Smooth follow/lerp const target new THREE.Vector3(); const current new THREE.Vector3(); const velocity new THREE.Vector3(); function smoothDamp(current, target, velocity, smoothTime, deltaTime) { const omega 2 / smoothTime; const x omega * deltaTime; const exp 1 / (1 x 0.48 * x * x 0.235 * x * x * x); const change current.clone().sub(target); const temp velocity .clone() .add(change.clone().multiplyScalar(omega)) .multiplyScalar(deltaTime); velocity.sub(temp.clone().multiplyScalar(omega)).multiplyScalar(exp); return target.clone().add(change.add(temp).multiplyScalar(exp)); } function animate() { current.copy(smoothDamp(current, target, velocity, 0.3, delta)); mesh.position.copy(current); }velocity对象必须在多次调用间持续持有否则失去阻尼记忆smoothTime越小跟随越快。弹簧物理Spring Physics带刚度与阻尼的二阶弹簧用于弹性回弹、受击震动、UI 弹跳等class Spring { constructor(stiffness 100, damping 10) { this.stiffness stiffness; this.damping damping; this.position 0; this.velocity 0; this.target 0; } update(dt) { const force -this.stiffness * (this.position - this.target); const dampingForce -this.damping * this.velocity; this.velocity (force dampingForce) * dt; this.position this.velocity * dt; return this.position; } } const spring new Spring(100, 10); spring.target 1; function animate() { mesh.position.y spring.update(delta); }stiffness决定回复力大小越高越硬damping决定能量损耗越高越早静止当damping² 4·stiffness时系统会出现欠阻尼振荡这是制造弹性的关键区间。振荡与轨迹运动用时间函数直接合成运动轨迹是装饰物、粒子与相机微动的最轻量方案function animate() { const t clock.getElapsedTime(); // Sine wave mesh.position.y Math.sin(t * 2) * 0.5; // Bouncing mesh.position.y Math.abs(Math.sin(t * 3)) * 2; // Circular motion mesh.position.x Math.cos(t) * 2; mesh.position.z Math.sin(t) * 2; // Figure 8 mesh.position.x Math.sin(t) * 2; mesh.position.z Math.sin(t * 2) * 1; }技能文档给出的四个模式——正弦起伏、取绝对值制造弹跳、正余弦合成圆环、倍频合成8 字——覆盖了从波动到轨迹巡航的大多数装饰性需求。在 OpenMontage 的 world-runtime.js 中同样可以看到这类时间函数水面透明度0.73 Math.sin(time * 0.42) * 0.035、太阳强度0.96 Math.sin(time * 0.09) * 0.04都是振荡模式在场景氛围动画上的直接应用。性能优化建议技能文档给出了五条动画性能原则对任何规模的三维场景都适用共享剪辑同一份AnimationClip可以被多个 Mixer 复用不要在每次使用时重新构造优化剪辑对冗余关键帧调用clip.optimize()压缩离屏暂停不可见对象停止mixer.update按距离使用 LOD远景角色使用简化骨骼/低骨骼数模型控制 Mixer 数量每个mixer.update()都有遍历开销尽量合并对象、减少活跃 Mixer。配套的工程化写法包括按视锥裁剪暂停动作以及用 Map 做剪辑缓存// Pause animation when not visible mesh.onBeforeRender () { action.paused false; }; mesh.onAfterRender () { // Check if will be visible next frame if (!isInFrustum(mesh)) { action.paused true; } }; // Cache clips const clipCache new Map(); function getClip(name) { if (!clipCache.has(name)) { clipCache.set(name, loadClip(name)); } return clipCache.get(name); }在 OpenMontage 的 3D 世界中台中性能治理还有更系统化的一层ThreeJSWorld工具threejs_world.py在构建阶段就把场景统计写入诊断报告terrain_triangles、environment_instances等见其_report方法并在production档通过保真度闸门_fidelity_gate约束资产调色板与地形材质数量。密集环境物统一走InstancedMesh合批渲染makeInstanced而散布点由确定性种子生成保证不同渲染轮次结果一致。在 OpenMontage 中的完整落点从技能到可渲染工作区将本文的动画知识放入 OpenMontage 的全局上下文可以看到一条完整的调用链技能选路Agent 依据 skills/INDEX.md 的3D Graphics分类定位threejs-animation动画能力、threejs-loadersGLTF 加载与threejs-world-generationOpenMontage 语义世界工作流世界规范Agent 向threejs_world工具提交结构化world_spec区域、地标、相机路径、资产调色板输入约束记录在 schemas/tools/threejs_world.schema.jsonoperation取build/validaterender_mode取cinematic/semantic/wireframequality_tier取blockout/production资产就绪通过 threejs_asset_catalog.py 安装 CC0 许可的 GLTF 目录install 时校验 SHA-256、生成catalog-manifest.json与模型清单工作区物化工具把模板 index.html、world-runtime.js 与规范化后的world.json一起写入可编辑工作区运行时可响应hf-seek时间事件、暴露window.__worldRenderAt(time)供时间轴驱动逐帧渲染确定性验证test_threejs_world.py 断言同一 spec 两次 build 的world-spec.jsSHA-256 完全一致并验证相机路径首尾时间、区域唯一性、生产档闸门等契约成片输出工作区最终交由video_compose/hyperframes_compose以atelier模式渲染为视频动画无论是剪辑驱动的 GLTF 动作还是renderAt(time)时间函数驱动的镜头运动都在这一环节成为成片内容。这条链路表明threejs-animation技能不只是孤立的 API 速查它直接服务于确定性 3D 世界生成 → 可编辑工作区 → 时间轴驱动渲染的生产体系。当你需要让 GLTF 模型播放动画、用骨骼程序化驱动角色细节、用 Morph Targets 做表情混合或为镜头与装饰物编写程序化运动时本文覆盖的 API 与模式就是 Agent 落地这些需求的执行依据。相关技能延伸threejs-loaders—— 加载带动画的 GLTF 模型threejs-fundamentals—— Clock 与动画循环基础threejs-shaders—— 在着色器中实现顶点动画。【免费下载链接】OpenMontageWorlds first open-source, agentic video production system. 12 production pipelines, 100 tools, 700 agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表