摄像机控制与动画轨道¶
接口、配置和调用参考。原理说明见 camera-animation.md。
MinecraftCameras.create(...) 和 MinecraftFrameWindows.createCamera(...) 返回的摄像机句柄
是 Free Camera,支持位置、旋转、光学缩放和关键帧播放。三类相机及实体跟随见
Player / Free / Fixed Camera。所有控制操作在渲染线程执行。
直接控制¶
camera.moveTo(x, y, z); // 世界坐标,绝对位置
camera.moveBy(dx, dy, dz); // 世界坐标,增量移动
camera.rotateTo(yaw, pitch, roll); // 角度,沿用 Minecraft 摄像机约定
camera.rotateBy(dYaw, dPitch, dRoll); // 增量旋转
camera.setVerticalFov(60); // 垂直 FOV,必须在 (0, 180) 度内
camera.zoom(2); // 投影放大两倍
WorldCameraView pose = camera.pose(); // 当前 tick 的姿态
直接控制以当前动画 tick 的姿态为起点,保留未修改的分量和输出尺寸,然后替换为固定姿态,停止原动画。
需要跟随实体时使用 MinecraftCameras.createFixed(...);它的 FOV/投影控制会保留目标绑定。
Free 仍可使用 updatePose(Supplier<WorldCameraView>) 描述自定义姿态源;替换供应器会清除动画。
WorldCameraView 另提供相同的不可变变换方法,方便自行组合供应器。
摄像机缩放改变投影,不缩放世界或输出分辨率。倍率 m 满足
tan(newFov / 2) = tan(baseFov / 2) / m,大于 1 放大,小于 1 缩小。
数值极限处 FOV 保持在可表示的 (0, 180) 度范围内。
动画轨道¶
var start = camera.pose();
var clip = CameraAnimationClip.builder(Id.parse("example:camera_pan"), 3f)
.move(0, start.x(), start.y(), start.z(), Easing.easeInOutCubic())
.move(3, start.x() + 10, start.y() + 2, start.z(), null)
.rotate(0, start.yaw(), start.pitch(), start.roll(), Easing.easeInOutSine())
.rotate(3, start.yaw() + 90, 15, 0, null)
.zoom(0, 1, Easing.easeInOutCubic())
.zoom(3, 2, null)
.build();
camera.animation().play(clip, false);
每条轨道独立驱动 x / y / z / yaw / pitch / roll / zoom 中的一个分量。
move、rotate 是同时添加三条轨道关键帧的便捷方法;只改变一个分量可使用
builder.keyframe(Channel.YAW, time, value, easing)。
位置和角度是绝对值;zoom 相对于供应器当帧提供的 FOV,每帧重新求值,不累积放大。
缺少的轨道保留供应器的值,窗口摄像机仍随 framebuffer/DPI 更新输出尺寸。
时间单位为秒。轨道自动排序并复制,重复时间、重复分量、超出片段时长、空轨道和非法数值在构造时拒绝。
首尾关键帧之外保持端点值。缓动沿用模型的 Easing,作用于关键帧的下一段。
旋转对未折返的角度数值插值,因此 0 → 720 可以完整转两圈;例如跨越 360 度边界时使用 350 → 370。
back/elastic 缓动造成 zoom 小于等于零时限制为极小正值,保证投影有效。
camera.animation().pause(); // 暂停动画,摄像机继续输出画面
camera.animation().seek(1.5f); // 立即跳转,清除旧的插值区间
camera.animation().setSpeed(2f); // 两倍速;0 冻结时钟
camera.animation().resume();
camera.animation().isPlaying();
camera.animation().currentTime(); // 循环时也是单调时钟
camera.animation().stop(); // 移除轨道,恢复原姿态供应器
非循环播放结束后保留终点姿态;重播使用 play,或先 seek(0) 再 resume()。
独立播放时 camera.pause() 同时暂停画面输出和自身时钟推进,camera.resume() 恢复。
跟随共享时间轴时,camera.pause() 只暂停该摄像机输出,其他目标继续播放;暂停整个场景使用 timeline.pause()。
关闭或失败的摄像机拒绝进一步控制,包括之前保存的 animation 控制器引用。
Minecraft 自动每游戏 tick 推进一次时钟(20 Hz),世界暂停时不推进;每帧按游戏的 partial tick 插值。
render-core 中的 OwnedCamera / CameraAnimationPlayer 可独立使用,由宿主调用 tick(dt) 和
samplePose(partialTick) / sample(partialTick)。渲染采样本身不推进时钟。
与现有模型动画集成¶
模型的 AnimationPlayer<T> 和摄像机适配器现在都使用 AnimationPlayback<T, R>。
现有 AnimationSampler<T> 是输出模型 Pose 的 AnimationSource<T, Pose>,
摄像机的 CameraClipSampler 输出未应用到世界的 CameraPose,统一内核负责计时和采样。
原模型 play(sampler, data, loop) 和 JSON 保留。命名片段与静态姿态统一使用
ModelPosing,glTF 专用驱动已迁移到该公共入口。
现有 AnimationClip 新增可选的 cameras,一个片段可以包含模型骨骼、形变、材质和多个命名镜头。
例如以下 JSON 会同步移动模型的 root 和场景镜头:
{
"id": "example:scene",
"duration_seconds": 2,
"bones": [{"bone": "root", "keyframes": [
{"time": 0, "transform": {"translate": [0, 0, 0]}},
{"time": 2, "transform": {"translate": [2, 0, 0]}}
]}],
"cameras": [{"camera": "main_shot", "tracks": [
{"channel": "x", "keyframes": [
{"time": 0, "value": 100}, {"time": 2, "value": 102}
]},
{"channel": "zoom", "keyframes": [
{"time": 0, "value": 1}, {"time": 2, "value": 1.5}
]}
]}]
}
通过 AnimationClip.CODEC 读取为 scene 后,把目标绑定到同一个时间轴:
var timeline = new AnimationTimeline();
var modelPlayer = new AnimationPlayer<AnimationClip>(modelInstance);
modelInstance.setPoseDriver(modelPlayer);
modelPlayer.follow(ClipSampler.INSTANCE, scene, timeline);
camera.animation().follow(scene, "main_shot", timeline);
timeline.play(scene.durationSeconds(), true);
var driver = MinecraftAnimationTimelines.drive(timeline);
timeline.pause();
timeline.seek(1f);
timeline.setSpeed(2f);
timeline.resume();
// 场景结束时释放驱动登记和目标绑定。
driver.close();
modelPlayer.stop();
camera.animation().stop();
timeline.stop();
follow 只绑定和采样,不重启时间轴,不自行推进。模型原有的 animate 调用和摄像机的自动 tick
都不会重复推进共享时钟。MinecraftAnimationTimelines.drive 每游戏 tick 统一推进一次,世界暂停时停止,
重复登记同一个时间轴会报错。纯 Java 宿主可以不用该登记,自行每 tick 调用一次 timeline.tick(dt)。
控制线程通过不可变的 volatile 快照把时钟交给渲染线程。
摄像机 animation().pause/seek/setSpeed/resume 和模型播放器的同名控制操作作用于其绑定的时间轴,
因此共享时也会同时控制其他目标。stop()、直接控制和关闭镜头只解除该目标,外部时间轴继续工作。
模型 reload/rebind 只更换输出目标,保留共享时钟进度。
逻辑镜头名(如 main_shot)与输出 view ID 独立,可以将同一个轨道绑定到多个镜头。
也可以把独立 CameraAnimationClip 与 glTF/VMD 等现有模型采样器绑定到同一时间轴。
不同长度的片段在共享周期内按各自长度保持终点,循环统一在时间轴的 duration 处发生。
已有模型播放器独立播放时也可作为时钟所有者:
modelPlayer.play(ClipSampler.INSTANCE, scene, true);
camera.animation().follow(scene, "main_shot", modelPlayer.timeline());
此时模型宿主的 tick 驱动时钟,摄像机只跟随;该时钟不能再登记到 MinecraftAnimationTimelines
或直接调用 timeline.tick。模型的 stop/play 会停止旧的自有时钟;需要独立管理场景生命周期时使用外部共享时间轴。
ClipSampler / FSM 的模型姿态出口仍只应用模型通道;摄像机通道通过显式的目标绑定应用,
当前共享时间轴不接管 FSM 的分层权重或状态切换。
JSON¶
通过 CameraAnimationClip.CODEC 读取和写出,与模型动画采用相同的 easing 名称。
{
"id": "example:camera_pan",
"duration_seconds": 3,
"tracks": [
{"channel": "yaw", "keyframes": [
{"time": 0, "value": 0, "easing": "ease_in_out_cubic"},
{"time": 3, "value": 90}
]},
{"channel": "zoom", "keyframes": [
{"time": 0, "value": 1},
{"time": 3, "value": 2}
]}
]
}
单元测试使用 ./gradlew :modules:modelling:modelUnitTest --offline。
使用复制的测试世界运行真实摄像机轨道烟测:
./gradlew :modules:modelling:runClientHeadless \
-PkasugaClientDirectory=build/camera-animation-smoke \
-PkasugaQuickPlayWorld=world-view-test -PkasugaTestCameraAnimation=true --offline
测试目录需预先包含 saves/world-view-test。成功时输出 CAMERA_ANIMATION_SMOKE_PASS,
并在该客户端目录的 debug/camera-animation.json 写出结果。