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:
1 parent
2471b1fd4f
commit
d1437a3923
3 files changed
+100
-6
No files matched your search
+90
-4
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -19,10 +20,15 @@ using Method = void(__thiscall*)(void* self);
|
|||||||
|
|
||||||
Method originalStep = nullptr;
|
Method originalStep = nullptr;
|
||||||
Method originalUpdateCalSkeleton = 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::int64_t> stepTime = 0;
|
||||||
std::atomic<std::uint64_t> steps = 0;
|
std::atomic<std::uint64_t> steps = 0;
|
||||||
std::atomic<bool> stepHooked = false;
|
std::atomic<bool> stepHooked = false;
|
||||||
std::atomic<bool> poseHooked = 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).
|
// zero.dll's constants: a tick, in milliseconds (33), and an animation frame, in seconds (1/30).
|
||||||
const unsigned* millisecondsPerFrame = nullptr;
|
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.
|
// Replaces the vtable entry at slot, if it holds expected; returns whether it did.
|
||||||
bool replace(void** slot, void* expected, void* replacement)
|
bool replace(void** slot, void* expected, void* replacement)
|
||||||
{
|
{
|
||||||
@@ -231,6 +284,38 @@ void install_pose(HMODULE sceneWindow)
|
|||||||
poseHooked = true;
|
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
|
} // namespace
|
||||||
|
|
||||||
void install()
|
void install()
|
||||||
@@ -243,11 +328,12 @@ void install()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
install_step(sceneWindow);
|
install_step(sceneWindow);
|
||||||
if (stepHooked) {
|
if (stepHooked) { // drawing between ticks needs the ticks
|
||||||
install_pose(sceneWindow); // posing between ticks needs the ticks
|
install_pose(sceneWindow);
|
||||||
|
install_camera(sceneWindow);
|
||||||
}
|
}
|
||||||
log::info("scene hooks: ticks {}, posing between ticks {}", stepHooked ? "recorded" : "not recorded",
|
log::info("scene hooks: ticks {}, posing between ticks {}, camera between ticks {}",
|
||||||
poseHooked ? "on" : "off");
|
stepHooked ? "recorded" : "not recorded", poseHooked ? "on" : "off", cameraHooked ? "on" : "off");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
// - ModelInstance::render's call to updateCalSkeleton, which poses the model before it's drawn: between ticks, the
|
// - 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
|
// 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.
|
// 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>
|
#include <cstdint>
|
||||||
|
|
||||||
|
|||||||
@@ -90,8 +90,14 @@ While the renderer process covers the view, the client's renderer doesn't begin
|
|||||||
wait for the display). The renderer process takes each view's chunks on a thread of its own as they come, so the
|
wait for the display). The renderer process takes each view's chunks on a thread of its own as they come, so the
|
||||||
client never waits for drawing; it draws the newest frame. On a PC with a graphics card a room frame takes the client
|
client never waits for drawing; it draws the newest frame. On a PC with a graphics card a room frame takes the client
|
||||||
about 2.5 ms (the client's own work: posing avatars, which it does every frame, and walking the scene), and the
|
about 2.5 ms (the client's own work: posing avatars, which it does every frame, and walking the scene), and the
|
||||||
renderer process about 0.5 ms. The extra frames show the same scene until animations, the camera and particles are
|
renderer process about 0.5 ms.
|
||||||
drawn at the time between steps.
|
|
||||||
|
Between steps, the frames show the scene at the time between them (`adapter/scene_hooks.cpp`): avatars posed with
|
||||||
|
their animations a fraction of a frame on (ModelInstance::render's call to updateCalSkeleton, redirected; Cal3D poses
|
||||||
|
between keyframes; animations held, looping or starting are posed as they are), and the camera where its easing (a
|
||||||
|
share 0.4 of the way to its target per step) has brought it, `1 - 0.6^fraction` of the way (CameraController's
|
||||||
|
getCameraTransform, in its exported vtable). Nothing is predicted: both are what the next step would give, at the
|
||||||
|
fraction. Still at 30 per second: particles, crossfades between animations, gaze.
|
||||||
|
|
||||||
Next: sharing pipelines between views (each view's scene compiles its own).
|
Next: sharing pipelines between views (each view's scene compiles its own).
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user