Zelda64Recomp/patches/camera_transform_tagging.c
Wiseguy 3c34fa63c1
Adds analog camera and other controller options (#264)
* Added analog cam and camera inversion options to menu, initial implementation of analog cam
* Automatically suppress inputs on the right stick while analog cam is active
* Return to automatic camera mode when pressing target
* Add aiming inversion options
* Add analog camera inversion options
2024-05-26 09:34:26 -04:00

407 lines
15 KiB
C

#include "patches.h"
#include "camera_patches.h"
#include "transform_ids.h"
#include "z64cutscene.h"
#include "overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope.h"
#include "overlays/gamestates/ovl_file_choose/z_file_select.h"
// #define PRINT_CAMERA_INFO
static bool camera_interpolation_forced = false;
static bool camera_skip_interpolation_forced = false;
static bool camera_ignore_tracking = false;
static bool in_kaleido = false;
static bool prev_in_kaleido = false;
static bool camera_skipped = false;
void set_camera_skipped(bool skipped) {
camera_skipped = skipped;
}
void clear_camera_skipped() {
camera_skipped = false;
}
bool camera_was_skipped() {
return camera_skipped;
}
void camera_pre_play_update(PlayState* play) {
}
void camera_post_play_update(PlayState* play) {
// Track whether the game is in kaleido.
prev_in_kaleido = in_kaleido;
if ((play->pauseCtx.state != 0) || (play->pauseCtx.debugEditor != DEBUG_EDITOR_NONE)) {
in_kaleido = true;
}
else {
in_kaleido = false;
if (play->activeCamId >= 0 && play->activeCamId < NUM_CAMS) {
Camera *active_cam = play->cameraPtrs[play->activeCamId];
#ifdef PRINT_CAMERA_INFO
recomp_printf("active_cam->setting %d active_cam->mode %d play->sceneId %d\n", active_cam->setting, active_cam->mode, play->sceneId);
#endif
// Dedicated section for workarounds where the heuristic fails to detect the large amount of movements the camera does in these particular areas.
bool force_interpolation = false;
if (active_cam->setting == CAM_SET_BOSS_MAJORA) {
// Majora's Mask final fight. All cameras should transition smoothly during this fight while not in a cutscene.
force_interpolation = true;
}
else if (active_cam->setting == CAM_SET_NORMAL0 || active_cam->setting == CAM_SET_DUNGEON0) {
force_interpolation =
// Pirates' Fortress Moat. Pushing the switch that unlocks the fortress will cause very large camera movement.
play->sceneId == SCENE_TORIDE ||
// Z-targetting an actor.
active_cam->mode == CAM_MODE_FOLLOWTARGET ||
// Z-targetting nothing.
active_cam->mode == CAM_MODE_TARGET ||
// Z-targetting in battle.
active_cam->mode == CAM_MODE_BATTLE;
}
// TODO: This setting claims "Smoothly and gradually return camera to Player after a cutscene "CONNECT0"". It might be worth it to enable this globally regardless of the scene.
else if (active_cam->setting == CAM_SET_CONNECT0) {
// Stone tower and inverted stone tower. The block puzzles will cause very large camera movement after they're activated.
force_interpolation = play->sceneId == SCENE_F40 || play->sceneId == SCENE_F41;
}
else if (active_cam->setting == CAM_SET_FREE0) {
// Cutscene after Majora fight. The camera zooms out while facing the moon with a very large movement.
force_interpolation = play->sceneId == SCENE_00KEIKOKU && play->csCtx.scriptIndex == 0 && play->csCtx.curFrame <= 98 && gSaveContext.sceneLayer == 8;
}
if (force_interpolation) {
force_camera_interpolation();
}
}
}
}
s32 View_ApplyPerspective(View* view);
s32 View_ApplyOrtho(View* view);
void force_camera_interpolation() {
camera_interpolation_forced = true;
}
void force_camera_skip_interpolation() {
camera_skip_interpolation_forced = true;
}
void force_camera_ignore_tracking() {
camera_ignore_tracking = true;
}
void KaleidoScope_SetView(PauseContext* pauseCtx, f32 eyeX, f32 eyeY, f32 eyeZ) {
Vec3f eye;
Vec3f at;
Vec3f up;
eye.x = eyeX;
eye.y = eyeY;
eye.z = eyeZ;
at.x = at.y = at.z = 0.0f;
up.x = up.z = 0.0f;
up.y = 1.0f;
// @recomp Force interpolation for this view and skip tracking positions.
force_camera_interpolation();
force_camera_ignore_tracking();
View_LookAt(&pauseCtx->view, &eye, &at, &up);
View_Apply(&pauseCtx->view,
VIEW_ALL | VIEW_FORCE_VIEWING | VIEW_FORCE_VIEWPORT | VIEW_FORCE_PROJECTION_PERSPECTIVE);
}
void FileSelect_SetView(FileSelectState* this, f32 eyeX, f32 eyeY, f32 eyeZ) {
Vec3f eye;
Vec3f lookAt;
Vec3f up;
eye.x = eyeX;
eye.y = eyeY;
eye.z = eyeZ;
lookAt.x = lookAt.y = lookAt.z = 0.0f;
up.x = up.z = 0.0f;
up.y = 1.0f;
// @recomp Force interpolation for this view and skip tracking positions.
force_camera_interpolation();
force_camera_ignore_tracking();
View_LookAt(&this->view, &eye, &lookAt, &up);
View_Apply(&this->view, VIEW_ALL | VIEW_FORCE_VIEWING | VIEW_FORCE_VIEWPORT | VIEW_FORCE_PROJECTION_PERSPECTIVE);
}
bool should_interpolate_perspective(Vec3f* eye, Vec3f* at) {
static Vec3f prev_eye = {0,0,0};
static Vec3f prev_at = {0,0,0};
static Vec3f eye_velocity = {0,0,0};
static Vec3f at_velocity = {0,0,0};
Vec3f predicted_eye;
Vec3f predicted_at;
// Predict the new eye and at positions based on the previous velocity and positions.
Math_Vec3f_Sum(&prev_eye, &eye_velocity, &predicted_eye);
Math_Vec3f_Sum(&prev_at, &at_velocity, &predicted_at);
// Calculate the current velocities from the previous and current positions.
Math_Vec3f_Diff(eye, &prev_eye, &eye_velocity);
Math_Vec3f_Diff(at, &prev_at, &at_velocity);
// Compare the predicted positions to the real positions.
float eye_dist = Math_Vec3f_DistXYZ(&predicted_eye, eye);
float at_dist = Math_Vec3f_DistXYZ(&predicted_at, at);
// Compare the velocities of the eye and at positions.
float velocity_diff = Math_Vec3f_DistXYZ(&eye_velocity, &at_velocity);
// Update the tracking for the previous positions with the new ones.
prev_eye = *eye;
prev_at = *at;
// These numbers are all picked via testing.
// If the velocity of both positions was the same, then they're moving together and should interpolate.
if (velocity_diff <= 3.0f && eye_dist <= 100.0f && at_dist <= 100.0f) {
return true;
}
// If the focus or position are basically the same across frames and the eye didn't move too far then it should probably be interpolated.
if (at_dist <= 10.0f && eye_dist <= 300.0f) {
return true;
}
if (eye_dist <= 10.0f && at_dist <= 300.0f) {
return true;
}
if (velocity_diff > 50.0f || at_dist > 50.0f || eye_dist > 300.0f) {
eye_velocity.x = 0.0f;
eye_velocity.y = 0.0f;
eye_velocity.z = 0.0f;
at_velocity.x = 0.0f;
at_velocity.y = 0.0f;
at_velocity.z = 0.0f;
return false;
}
return true;
}
/**
* Apply view to POLY_OPA_DISP, POLY_XLU_DISP (and OVERLAY_DISP if ortho)
*/
void View_Apply(View* view, s32 mask) {
mask = (view->flags & mask) | (mask >> 4);
// @recomp Determine if the camera should be interpolated this frame.
bool interpolate_camera = false;
if (mask & VIEW_PROJECTION_ORTHO) {
View_ApplyOrtho(view);
} else {
View_ApplyPerspective(view);
// @recomp Determine if interpolation should occur based on the new eye and at positions.
if (!camera_ignore_tracking) {
interpolate_camera = should_interpolate_perspective(&view->eye, &view->at);
}
}
camera_ignore_tracking = false;
// Force skipping interpolation if the previous view was kaleido and this one isn't.
if (prev_in_kaleido && !in_kaleido) {
camera_skip_interpolation_forced = true;
}
// @recomp Apply camera interpolation overrides.
if (camera_skip_interpolation_forced) {
interpolate_camera = false;
}
else if (camera_interpolation_forced) {
interpolate_camera = true;
}
// @recomp Tag the camera matrices
GraphicsContext* gfxCtx = view->gfxCtx;
OPEN_DISPS(gfxCtx);
if (interpolate_camera) {
// Simple interpolation works much better for cameras because they orbit around a focus.
gEXMatrixGroupSimple(POLY_OPA_DISP++, CAMERA_TRANSFORM_ID, G_EX_NOPUSH, G_MTX_PROJECTION,
G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_ORDER_LINEAR, G_EX_EDIT_NONE);
gEXMatrixGroupSimple(POLY_XLU_DISP++, CAMERA_TRANSFORM_ID, G_EX_NOPUSH, G_MTX_PROJECTION,
G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_ORDER_LINEAR, G_EX_EDIT_NONE);
}
else {
// Skip interpolation for this frame.
gEXMatrixGroupSimple(POLY_OPA_DISP++, CAMERA_TRANSFORM_ID, G_EX_NOPUSH, G_MTX_PROJECTION,
G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_INTERPOLATE, G_EX_ORDER_LINEAR, G_EX_EDIT_NONE);
gEXMatrixGroupSimple(POLY_XLU_DISP++, CAMERA_TRANSFORM_ID, G_EX_NOPUSH, G_MTX_PROJECTION,
G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_INTERPOLATE, G_EX_ORDER_LINEAR, G_EX_EDIT_NONE);
}
// Record whether the camera was skipped for later use.
set_camera_skipped(!interpolate_camera);
camera_interpolation_forced = false;
camera_skip_interpolation_forced = false;
CLOSE_DISPS(gfxCtx);
}
typedef s32 (*CameraUpdateFunc)(Camera*);
typedef struct {
/* 0x0 */ s16 val;
/* 0x2 */ s16 param;
} CameraModeValue; // size = 0x4
typedef struct {
/* 0x0 */ s16 funcId;
/* 0x2 */ s16 numValues;
/* 0x4 */ CameraModeValue* values;
} CameraMode; // size = 0x8
typedef struct {
/* 0x0 */ u32 validModes;
/* 0x4 */ u32 flags;
/* 0x8 */ CameraMode* cameraModes;
} CameraSetting; // size = 0xC
extern CameraSetting sCameraSettings[];
extern s32 sCameraInterfaceFlags;
s32 Camera_BgCheck(Camera* camera, Vec3f* from, Vec3f* to);
Vec3s* Camera_GetBgCamOrActorCsCamFuncData(Camera* camera, u32 camDataId);
f32 Camera_GetFocalActorHeight(Camera* camera);
f32 Camera_ScaledStepToCeilF(f32 target, f32 cur, f32 stepScale, f32 minDiff);
s16 Camera_ScaledStepToCeilS(s16 target, s16 cur, f32 stepScale, s16 minDiff);
void Camera_ScaledStepToCeilVec3f(Vec3f* target, Vec3f* cur, f32 xzStepScale, f32 yStepScale, f32 minDiff);
void Camera_SetFocalActorAtOffset(Camera* camera, Vec3f* focalActorPos);
void Camera_SetUpdateRatesSlow(Camera* camera);
Vec3f Camera_Vec3sToVec3f(Vec3s* src);
/**
* Used for many fixed-based camera settings i.e. camera is fixed in rotation, and often position (but not always)
*/
// @recomp Modified to not force interpolation while panning.
s32 Camera_Fixed1(Camera* camera) {
s32 pad[2];
s32 yawDiff;
VecGeo eyeOffset;
VecGeo eyeAtOffset;
VecGeo sp7C;
u32 negOne;
Vec3f adjustedPos;
BgCamFuncData* bgCamFuncData;
Vec3f* eye = &camera->eye;
Vec3f* at = &camera->at;
PosRot* focalActorPosRot = &camera->focalActorPosRot;
f32 focalActorHeight = Camera_GetFocalActorHeight(camera);
CameraModeValue* values;
PosRot* targetHome;
PosRot* targetWorld;
VecGeo sp44;
Fixed1ReadOnlyData* roData = &camera->paramData.fixd1.roData;
Fixed1ReadWriteData* rwData = &camera->paramData.fixd1.rwData;
sp7C = OLib_Vec3fDiffToVecGeo(at, eye);
if (!RELOAD_PARAMS(camera)) {
} else {
values = sCameraSettings[camera->setting].cameraModes[camera->mode].values;
bgCamFuncData = (BgCamFuncData*)Camera_GetBgCamOrActorCsCamFuncData(camera, camera->bgCamIndex);
rwData->eyePosRotTarget.pos = Camera_Vec3sToVec3f(&bgCamFuncData->pos);
rwData->eyePosRotTarget.rot = bgCamFuncData->rot;
rwData->fov = bgCamFuncData->fov;
rwData->focalActor = camera->focalActor;
roData->unk_00 = GET_NEXT_SCALED_RO_DATA(values) * focalActorHeight;
roData->unk_04 = GET_NEXT_SCALED_RO_DATA(values);
roData->fov = GET_NEXT_RO_DATA(values);
roData->interfaceFlags = GET_NEXT_RO_DATA(values);
if (roData->interfaceFlags & FIXED1_FLAG_4) {
if (camera->target == NULL) {
return false;
}
targetHome = &camera->target->home;
targetWorld = &camera->target->world;
sp44 = OLib_Vec3fDiffToVecGeo(&targetHome->pos, &rwData->eyePosRotTarget.pos);
sp44.yaw = targetWorld->rot.y + (s16)(sp44.yaw - targetHome->rot.y);
rwData->eyePosRotTarget.pos = OLib_AddVecGeoToVec3f(&targetWorld->pos, &sp44);
yawDiff = (s16)(rwData->eyePosRotTarget.rot.y - targetHome->rot.y);
rwData->eyePosRotTarget.rot.y = targetWorld->rot.y + yawDiff;
}
}
negOne = -1;
if (rwData->focalActor != camera->focalActor) {
camera->animState = 20;
}
if (rwData->fov == (s32)negOne) {
rwData->fov = roData->fov * 100;
} else if (rwData->fov <= 360) {
rwData->fov *= 100;
}
sCameraInterfaceFlags = roData->interfaceFlags;
if (camera->animState == 0) {
camera->animState++;
Camera_SetUpdateRatesSlow(camera);
if (rwData->fov != (s32)negOne) {
roData->fov = CAM_RODATA_UNSCALE(rwData->fov);
}
if (bgCamFuncData->unk_0E != (s32)negOne) {
roData->unk_04 = CAM_RODATA_UNSCALE(bgCamFuncData->unk_0E);
}
}
// @recomp Camera interpolation should always apply on this mode unless something else modified it externally.
// We check for the approach percentage as well to detect when it wants to force an interpolation to the next position.
static Vec3f lastEye = {};
static Vec3f lastAt = {};
if (OLib_Vec3fDist(eye, &lastEye) < 1e-6f && OLib_Vec3fDist(at, &lastAt) < 1e-6f && (roData->unk_04 < 0.999f)) {
force_camera_interpolation();
}
eyeAtOffset = OLib_Vec3fDiffToVecGeo(eye, at);
Camera_ScaledStepToCeilVec3f(&rwData->eyePosRotTarget.pos, eye, roData->unk_04, roData->unk_04, 0.2f);
adjustedPos = focalActorPosRot->pos;
adjustedPos.y += focalActorHeight;
camera->dist = OLib_Vec3fDist(&adjustedPos, eye);
eyeOffset.r = camera->dist;
eyeOffset.pitch =
Camera_ScaledStepToCeilS(rwData->eyePosRotTarget.rot.x * -1, eyeAtOffset.pitch, roData->unk_04, 5);
eyeOffset.yaw = Camera_ScaledStepToCeilS(rwData->eyePosRotTarget.rot.y, eyeAtOffset.yaw, roData->unk_04, 5);
*at = OLib_AddVecGeoToVec3f(eye, &eyeOffset);
camera->eyeNext = *eye;
Camera_BgCheck(camera, eye, at);
camera->fov = Camera_ScaledStepToCeilF(roData->fov, camera->fov, roData->unk_04, 0.1f);
camera->roll = 0;
camera->atLerpStepScale = 0.0f;
Camera_SetFocalActorAtOffset(camera, &focalActorPosRot->pos);
camera->roll = Camera_ScaledStepToCeilS(rwData->eyePosRotTarget.rot.z, camera->roll, roData->unk_04, 5);
// @recomp
lastEye = *eye;
lastAt = *at;
return 1;
}