Move the camera between ticks
CI / build (push) Successful in 1m38s

CameraController::step eases the camera's position, orbit angles and zoom a
share k (0.4) of the way to their targets at each tick; between ticks, the
adapter's getCameraTransform (its entry in CameraController's exported vtable,
replaced after checking it) sets them 1 - (1 - k)^fraction of the way for IMVU's
code to make the transform from, then puts them back. k is read from the
constant CameraController::step uses, checked to be referenced there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-28 21:44:56 -07:00
1 parent 2471b1fd4f
commit d1437a3923
3 files changed
+100 -6

No files matched your search

+90 -4
View File
@@ -4,6 +4,7 @@
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstring>
#include <mutex>
#include <unordered_map>
@@ -19,10 +20,15 @@ using Method = void(__thiscall*)(void* self);
Method originalStep = nullptr;
Method originalUpdateCalSkeleton = nullptr;
// IMVU's CameraController::getCameraTransform(): returns a Transform by value (constructed in result).
using GetTransform = void*(__thiscall*)(const void* self, void* result);
GetTransform originalGetCameraTransform = nullptr;
float cameraEasing = 0; // the share of the way to its target the camera moves at each tick
std::atomic<std::int64_t> stepTime = 0;
std::atomic<std::uint64_t> steps = 0;
std::atomic<bool> stepHooked = false;
std::atomic<bool> poseHooked = false;
std::atomic<bool> cameraHooked = false;
// zero.dll's constants: a tick, in milliseconds (33), and an animation frame, in seconds (1/30).
const unsigned* millisecondsPerFrame = nullptr;
@@ -155,6 +161,53 @@ void __fastcall updateCalSkeleton(void* model, void* /*edx*/)
}
}
// The camera (SceneWindow.dll's CameraController::step, at each tick): each of these values moves a share k of the way
// to its target (k is 0.4): the camera's position (+0x118, 3 floats, to +0xf0), and its orbit angles and zoom, in
// degrees (+0x140, 3 floats, to +0x134; the difference taken between -180 and 180).
// A fraction f of a tick on, the same easing has moved it 1 - (1 - k)^f of the way: exactly, nothing guessed.
constexpr std::size_t cameraPosition = 0x118;
constexpr std::size_t cameraPositionTarget = 0xf0;
constexpr std::size_t cameraAngles = 0x140;
constexpr std::size_t cameraAnglesTarget = 0x134;
// Nonzero: the camera shows a fixed view ("take"), not eased.
constexpr std::size_t cameraTake = 0xec;
float degrees180(float degrees)
{
float wrapped = std::fmod(degrees + 180.0f, 360.0f);
if (wrapped < 0) {
wrapped += 360.0f;
}
return wrapped - 180.0f;
}
// Our CameraController::getCameraTransform: between ticks, the camera where its easing has brought it since the tick.
// The eased values are set ahead for IMVU's code to make the transform from, then put back. __fastcall takes ECX like
// __thiscall, and like it the callee pops the stack argument.
void* __fastcall getCameraTransform(void* camera, void* /*edx*/, void* result)
{
const float fraction = tick_fraction();
if (fraction < 0.01f || field<std::int32_t>(camera, cameraTake) != 0) {
return originalGetCameraTransform(camera, result);
}
const float moved = 1.0f - std::pow(1.0f - cameraEasing, fraction);
float saved[6];
for (int i = 0; i < 3; ++i) {
float& position = field<float>(camera, cameraPosition + i * 4);
saved[i] = position;
position += (field<float>(camera, cameraPositionTarget + i * 4) - position) * moved;
float& angle = field<float>(camera, cameraAngles + i * 4);
saved[3 + i] = angle;
angle = degrees180(angle + degrees180(field<float>(camera, cameraAnglesTarget + i * 4) - angle) * moved);
}
void* transform = originalGetCameraTransform(camera, result);
for (int i = 0; i < 3; ++i) {
field<float>(camera, cameraPosition + i * 4) = saved[i];
field<float>(camera, cameraAngles + i * 4) = saved[3 + i];
}
return transform;
}
// Replaces the vtable entry at slot, if it holds expected; returns whether it did.
bool replace(void** slot, void* expected, void* replacement)
{
@@ -231,6 +284,38 @@ void install_pose(HMODULE sceneWindow)
poseHooked = true;
}
void install_camera(HMODULE sceneWindow)
{
auto* vtable = reinterpret_cast<void**>(GetProcAddress(sceneWindow, "??_7CameraController@@6B@"));
auto* get = GetProcAddress(sceneWindow, "?getCameraTransform@CameraController@@UBE?AVTransform@@XZ");
auto* step = reinterpret_cast<const std::byte*>(GetProcAddress(sceneWindow, "?step@CameraController@@UAEXXZ"));
if (vtable == nullptr || get == nullptr || step == nullptr) {
log::info("scene hooks: CameraController's vtable, getCameraTransform or step not found");
return;
}
// The easing share: a float constant CameraController::step reads (at SceneWindow.dll's 0x100cb934 when loaded at
// its preferred address), checked to be referenced by step's code and to be the expected 0.4.
const auto* base = reinterpret_cast<const std::byte*>(sceneWindow);
const auto* easing = reinterpret_cast<const float*>(base + 0xcb934);
const auto address = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(easing));
bool referenced = false;
for (std::size_t i = 0; i + 4 <= 0x700 && !referenced; ++i) {
referenced = std::memcmp(step + i, &address, 4) == 0;
}
if (!referenced || *easing != 0.4f) {
log::info("scene hooks: CameraController::step's easing not where expected");
return;
}
cameraEasing = *easing;
// CameraController's vtable: getCameraTransform is its 6th entry.
originalGetCameraTransform = reinterpret_cast<GetTransform>(get);
if (!replace(&vtable[5], reinterpret_cast<void*>(get), reinterpret_cast<void*>(&getCameraTransform))) {
log::info("scene hooks: CameraController's vtable doesn't hold its getCameraTransform");
return;
}
cameraHooked = true;
}
} // namespace
void install()
@@ -243,11 +328,12 @@ void install()
return;
}
install_step(sceneWindow);
if (stepHooked) {
install_pose(sceneWindow); // posing between ticks needs the ticks
if (stepHooked) { // drawing between ticks needs the ticks
install_pose(sceneWindow);
install_camera(sceneWindow);
}
log::info("scene hooks: ticks {}, posing between ticks {}", stepHooked ? "recorded" : "not recorded",
poseHooked ? "on" : "off");
log::info("scene hooks: ticks {}, posing between ticks {}, camera between ticks {}",
stepHooked ? "recorded" : "not recorded", poseHooked ? "on" : "off", cameraHooked ? "on" : "off");
});
}
+2
View File
@@ -9,6 +9,8 @@
// - ModelInstance::render's call to updateCalSkeleton, which poses the model before it's drawn: between ticks, the
// model is posed at the time between them (its animations' times moved on by the fraction of a frame since the
// tick; Cal3D poses between keyframes), then its animations are put back as they were.
// - CameraController::getCameraTransform (its exported vtable): between ticks, the camera where its easing toward its
// target has brought it since the tick.
#include <cstdint>