cutscene system
This commit is contained in:
302
src/cutscene.cpp
Normal file
302
src/cutscene.cpp
Normal file
@@ -0,0 +1,302 @@
|
||||
#include "cutscene.hh"
|
||||
|
||||
#include <psyqo/fixed-point.hh>
|
||||
#include <psyqo/soft-math.hh>
|
||||
#include <psyqo/trigonometry.hh>
|
||||
|
||||
namespace psxsplash {
|
||||
|
||||
// Bare-metal string compare (avoids linking libc)
|
||||
static bool cs_streq(const char* a, const char* b) {
|
||||
while (*a && *b) {
|
||||
if (*a++ != *b++) return false;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
void CutscenePlayer::init(Cutscene* cutscenes, int count, Camera* camera, AudioManager* audio) {
|
||||
m_cutscenes = cutscenes;
|
||||
m_count = count;
|
||||
m_active = nullptr;
|
||||
m_frame = 0;
|
||||
m_nextAudio = 0;
|
||||
m_camera = camera;
|
||||
m_audio = audio;
|
||||
}
|
||||
|
||||
bool CutscenePlayer::play(const char* name) {
|
||||
if (!name || !m_cutscenes) return false;
|
||||
|
||||
for (int i = 0; i < m_count; i++) {
|
||||
if (m_cutscenes[i].name && cs_streq(m_cutscenes[i].name, name)) {
|
||||
m_active = &m_cutscenes[i];
|
||||
m_frame = 0;
|
||||
m_nextAudio = 0;
|
||||
|
||||
// Capture initial state for pre-first-keyframe blending
|
||||
for (uint8_t ti = 0; ti < m_active->trackCount; ti++) {
|
||||
CutsceneTrack& track = m_active->tracks[ti];
|
||||
track.initialValues[0] = track.initialValues[1] = track.initialValues[2] = 0;
|
||||
switch (track.trackType) {
|
||||
case TrackType::CameraPosition:
|
||||
if (m_camera) {
|
||||
auto& pos = m_camera->GetPosition();
|
||||
track.initialValues[0] = (int16_t)pos.x.value;
|
||||
track.initialValues[1] = (int16_t)pos.y.value;
|
||||
track.initialValues[2] = (int16_t)pos.z.value;
|
||||
}
|
||||
break;
|
||||
case TrackType::CameraRotation:
|
||||
if (m_camera) {
|
||||
track.initialValues[0] = m_camera->GetAngleX();
|
||||
track.initialValues[1] = m_camera->GetAngleY();
|
||||
track.initialValues[2] = m_camera->GetAngleZ();
|
||||
}
|
||||
break;
|
||||
case TrackType::ObjectPosition:
|
||||
if (track.target) {
|
||||
track.initialValues[0] = (int16_t)track.target->position.x.value;
|
||||
track.initialValues[1] = (int16_t)track.target->position.y.value;
|
||||
track.initialValues[2] = (int16_t)track.target->position.z.value;
|
||||
}
|
||||
break;
|
||||
case TrackType::ObjectRotationY:
|
||||
// Can't easily recover angle from matrix — default to 0
|
||||
break;
|
||||
case TrackType::ObjectActive:
|
||||
if (track.target) {
|
||||
track.initialValues[0] = track.target->isActive() ? 1 : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CutscenePlayer::stop() {
|
||||
m_active = nullptr;
|
||||
}
|
||||
|
||||
void CutscenePlayer::tick() {
|
||||
if (!m_active) return;
|
||||
|
||||
// Apply all tracks at the current frame
|
||||
for (uint8_t i = 0; i < m_active->trackCount; i++) {
|
||||
applyTrack(m_active->tracks[i]);
|
||||
}
|
||||
|
||||
// Fire audio events whose frame has been reached
|
||||
while (m_nextAudio < m_active->audioEventCount) {
|
||||
CutsceneAudioEvent& evt = m_active->audioEvents[m_nextAudio];
|
||||
if (evt.frame <= m_frame) {
|
||||
if (m_audio) {
|
||||
m_audio->play(evt.clipIndex, evt.volume, evt.pan);
|
||||
}
|
||||
m_nextAudio++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Advance frame
|
||||
m_frame++;
|
||||
if (m_frame >= m_active->totalFrames) {
|
||||
m_active = nullptr; // Cutscene finished
|
||||
}
|
||||
}
|
||||
|
||||
// Apply easing curve to fixed-point t ∈ [0, 4096].
|
||||
static int32_t applyCurve(int32_t t, InterpMode mode) {
|
||||
switch (mode) {
|
||||
default:
|
||||
case InterpMode::Linear:
|
||||
return t;
|
||||
case InterpMode::Step:
|
||||
return 0; // snaps to 'a' value until next keyframe
|
||||
case InterpMode::EaseIn:
|
||||
// t² / 4096
|
||||
return (int32_t)((int64_t)t * t >> 12);
|
||||
case InterpMode::EaseOut:
|
||||
// 1 − (1−t)² = t*(2 − t) / 4096
|
||||
return (int32_t)(((int64_t)t * (8192 - t)) >> 12);
|
||||
case InterpMode::EaseInOut: {
|
||||
// Smoothstep: 3t² − 2t³ (all in 12-bit fixed point)
|
||||
int64_t t2 = (int64_t)t * t; // 24-bit
|
||||
int64_t t3 = t2 * t; // 36-bit
|
||||
return (int32_t)((3 * t2 - 2 * (t3 >> 12)) >> 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common helper: find surrounding keyframes and compute fixed-point t (0..4096).
|
||||
// Returns false if result was clamped (no interpolation needed, out[] already set).
|
||||
// When true, t already has the easing curve applied based on the *destination* keyframe's InterpMode.
|
||||
static bool findKfPair(CutsceneKeyframe* kf, uint8_t count, uint16_t frame,
|
||||
uint8_t& a, uint8_t& b, int32_t& t, int16_t out[3]) {
|
||||
if (count == 0) {
|
||||
out[0] = out[1] = out[2] = 0;
|
||||
return false;
|
||||
}
|
||||
if (frame <= kf[0].getFrame() || count == 1) {
|
||||
out[0] = kf[0].values[0];
|
||||
out[1] = kf[0].values[1];
|
||||
out[2] = kf[0].values[2];
|
||||
return false;
|
||||
}
|
||||
if (frame >= kf[count - 1].getFrame()) {
|
||||
out[0] = kf[count - 1].values[0];
|
||||
out[1] = kf[count - 1].values[1];
|
||||
out[2] = kf[count - 1].values[2];
|
||||
return false;
|
||||
}
|
||||
b = 1;
|
||||
while (b < count && kf[b].getFrame() <= frame) b++;
|
||||
a = b - 1;
|
||||
uint16_t span = kf[b].getFrame() - kf[a].getFrame();
|
||||
if (span == 0) {
|
||||
out[0] = kf[a].values[0];
|
||||
out[1] = kf[a].values[1];
|
||||
out[2] = kf[a].values[2];
|
||||
return false;
|
||||
}
|
||||
uint32_t num = (uint32_t)(frame - kf[a].getFrame()) << 12;
|
||||
int32_t rawT = (int32_t)(num / span);
|
||||
t = applyCurve(rawT, kf[b].getInterp());
|
||||
return true;
|
||||
}
|
||||
|
||||
void CutscenePlayer::lerpKeyframes(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]) {
|
||||
uint8_t a, b;
|
||||
int32_t t;
|
||||
if (!findKfPair(kf, count, m_frame, a, b, t, out)) {
|
||||
// If clamped to first keyframe and frame < first kf frame, blend from initial
|
||||
if (count > 0 && kf[0].getFrame() > 0 && m_frame < kf[0].getFrame()) {
|
||||
uint16_t span = kf[0].getFrame();
|
||||
uint32_t num = (uint32_t)m_frame << 12;
|
||||
int32_t rawT = (int32_t)(num / span);
|
||||
int32_t ct = applyCurve(rawT, kf[0].getInterp());
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int32_t delta = (int32_t)kf[0].values[i] - (int32_t)initial[i];
|
||||
out[i] = (int16_t)((int32_t)initial[i] + ((delta * ct) >> 12));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int32_t delta = (int32_t)kf[b].values[i] - (int32_t)kf[a].values[i];
|
||||
out[i] = (int16_t)((int32_t)kf[a].values[i] + ((delta * t) >> 12));
|
||||
}
|
||||
}
|
||||
|
||||
// Shortest-path angle interpolation.
|
||||
// Angles are in psyqo units where 2048 = full circle (2π).
|
||||
static constexpr int32_t ANGLE_FULL_CIRCLE = 2048;
|
||||
static constexpr int32_t ANGLE_HALF_CIRCLE = 1024;
|
||||
|
||||
void CutscenePlayer::lerpAngles(CutsceneKeyframe* kf, uint8_t count, const int16_t initial[3], int16_t out[3]) {
|
||||
uint8_t a, b;
|
||||
int32_t t;
|
||||
if (!findKfPair(kf, count, m_frame, a, b, t, out)) {
|
||||
// If clamped to first keyframe and frame < first kf frame, blend from initial
|
||||
if (count > 0 && kf[0].getFrame() > 0 && m_frame < kf[0].getFrame()) {
|
||||
uint16_t span = kf[0].getFrame();
|
||||
uint32_t num = (uint32_t)m_frame << 12;
|
||||
int32_t rawT = (int32_t)(num / span);
|
||||
int32_t ct = applyCurve(rawT, kf[0].getInterp());
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int32_t from = (int32_t)initial[i];
|
||||
int32_t to = (int32_t)kf[0].values[i];
|
||||
int32_t delta = to - from;
|
||||
delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE;
|
||||
out[i] = (int16_t)(from + ((delta * ct) >> 12));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int32_t from = (int32_t)kf[a].values[i];
|
||||
int32_t to = (int32_t)kf[b].values[i];
|
||||
// Shortest-path: wrap delta into [-1024, +1024)
|
||||
int32_t delta = to - from;
|
||||
// Modulo into [-2048, +2048) then clamp to half-circle
|
||||
delta = ((delta + ANGLE_HALF_CIRCLE) % ANGLE_FULL_CIRCLE + ANGLE_FULL_CIRCLE) % ANGLE_FULL_CIRCLE - ANGLE_HALF_CIRCLE;
|
||||
out[i] = (int16_t)(from + ((delta * t) >> 12));
|
||||
}
|
||||
}
|
||||
|
||||
void CutscenePlayer::applyTrack(CutsceneTrack& track) {
|
||||
if (track.keyframeCount == 0 || !track.keyframes) return;
|
||||
|
||||
int16_t out[3];
|
||||
|
||||
switch (track.trackType) {
|
||||
case TrackType::CameraPosition: {
|
||||
if (!m_camera) return;
|
||||
lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out);
|
||||
psyqo::FixedPoint<12> x, y, z;
|
||||
x.value = (int32_t)out[0];
|
||||
y.value = (int32_t)out[1];
|
||||
z.value = (int32_t)out[2];
|
||||
m_camera->SetPosition(x, y, z);
|
||||
break;
|
||||
}
|
||||
|
||||
case TrackType::CameraRotation: {
|
||||
if (!m_camera) return;
|
||||
lerpAngles(track.keyframes, track.keyframeCount, track.initialValues, out);
|
||||
psyqo::Angle rx, ry, rz;
|
||||
rx.value = (int32_t)out[0];
|
||||
ry.value = (int32_t)out[1];
|
||||
rz.value = (int32_t)out[2];
|
||||
m_camera->SetRotation(rx, ry, rz);
|
||||
break;
|
||||
}
|
||||
|
||||
case TrackType::ObjectPosition: {
|
||||
if (!track.target) return;
|
||||
lerpKeyframes(track.keyframes, track.keyframeCount, track.initialValues, out);
|
||||
track.target->position.x.value = (int32_t)out[0];
|
||||
track.target->position.y.value = (int32_t)out[1];
|
||||
track.target->position.z.value = (int32_t)out[2];
|
||||
break;
|
||||
}
|
||||
|
||||
case TrackType::ObjectRotationY: {
|
||||
if (!track.target) return;
|
||||
lerpAngles(track.keyframes, track.keyframeCount, track.initialValues, out);
|
||||
psyqo::Angle yAngle;
|
||||
yAngle.value = (int32_t)out[1];
|
||||
track.target->rotation = psyqo::SoftMath::generateRotationMatrix33(
|
||||
yAngle, psyqo::SoftMath::Axis::Y, m_trig);
|
||||
break;
|
||||
}
|
||||
|
||||
case TrackType::ObjectActive: {
|
||||
if (!track.target) return;
|
||||
// Step interpolation: find the last keyframe at or before m_frame
|
||||
CutsceneKeyframe* kf = track.keyframes;
|
||||
uint8_t count = track.keyframeCount;
|
||||
// Use initial state if we're before the first keyframe
|
||||
int16_t activeVal = (count > 0 && m_frame < kf[0].getFrame())
|
||||
? track.initialValues[0]
|
||||
: kf[0].values[0];
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
if (kf[i].getFrame() <= m_frame) {
|
||||
activeVal = kf[i].values[0];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
track.target->setActive(activeVal != 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace psxsplash
|
||||
Reference in New Issue
Block a user