ModelInstance::step ramps the weights of animations fading in and out in tenths over 10 ticks; between ticks, the posing hook sets them partway to the next tick's weight, and puts them back with the times. Only if ModelInstance::step counts the fades where expected (checked when installing); otherwise crossfades stay at the client's rate. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9d0faecf4d
commit
db344b7b95
3 files changed
+66
-17
No files matched your search
+62
-15
@@ -30,6 +30,7 @@ std::atomic<std::uint64_t> steps = 0;
|
||||
std::atomic<int> stepTicks = 1;
|
||||
std::atomic<bool> stepHooked = false;
|
||||
std::atomic<bool> poseHooked = false;
|
||||
bool crossfades = false; // whether ModelInstance::step's crossfades are as expected (checked when installing)
|
||||
std::atomic<bool> cameraHooked = false;
|
||||
std::atomic<bool> particlesHooked = false;
|
||||
|
||||
@@ -88,13 +89,28 @@ void __fastcall step(void* self, void* /*edx*/)
|
||||
// - ModelInstance: +0x2c, a flag: its skeleton must be posed again; +0x120, its animation effects, a std::list (a pointer
|
||||
// to the list's head node; a node: next, previous, then the effect).
|
||||
// - Effect: +0x34, playing (a flag); +0x3c, its time, in seconds; +0x90, its Cal3D animation.
|
||||
// - The Cal3D animation: +0x8, its time, which the mixer poses the skeleton at (CalMixer::updateSkeleton).
|
||||
// - The Cal3D animation: +0x8, its time, which the mixer poses the skeleton at (CalMixer::updateSkeleton); +0xc, its
|
||||
// weight in the pose.
|
||||
//
|
||||
// Crossfades (ModelInstance::step, at each tick): an effect fading in (+0x44 set, +0x18 nonzero) has its weight
|
||||
// (+0x38) ramped over 10 ticks, counted at +0x48: (n + 1) * 0.1 * weight, then n goes up. One fading out is in the
|
||||
// model's crossfade list (+0x138, a std::list; a node holds the effect at +0xc and its count at +0x14): m goes up, then
|
||||
// (1 - m * 0.1) * weight; after 10, it's removed. Between ticks, both are partway to the next tick's weight.
|
||||
constexpr std::size_t modelPoseDirty = 0x2c;
|
||||
constexpr std::size_t modelEffects = 0x120;
|
||||
constexpr std::size_t modelFadingOut = 0x138;
|
||||
constexpr std::size_t effectPlaying = 0x34;
|
||||
constexpr std::size_t effectTime = 0x3c;
|
||||
constexpr std::size_t effectAnimation = 0x90;
|
||||
constexpr std::size_t effectFadesIn = 0x44;
|
||||
constexpr std::size_t effectFadeSource = 0x18;
|
||||
constexpr std::size_t effectFadeCount = 0x48;
|
||||
constexpr std::size_t effectWeight = 0x38;
|
||||
constexpr std::size_t fadingOutEffect = 0xc;
|
||||
constexpr std::size_t fadingOutCount = 0x14;
|
||||
constexpr std::size_t animationTime = 0x8;
|
||||
constexpr std::size_t animationWeight = 0xc;
|
||||
constexpr unsigned fadeTicks = 10;
|
||||
|
||||
template <class T>
|
||||
T& field(void* object, std::size_t offset)
|
||||
@@ -126,8 +142,9 @@ float tick_fraction()
|
||||
|
||||
// Our ModelInstance::updateCalSkeleton, called where ModelInstance::render poses the model before drawing it: between
|
||||
// ticks, poses it at the time between them. Each playing animation that moved on at the last step is set ahead by the
|
||||
// fraction of a tick since, at the rate it moved (up to 4 frames a tick), the skeleton posed (by IMVU's), and the
|
||||
// times put back, with the skeleton marked to be posed again, so everything else sees the tick's pose.
|
||||
// fraction of a tick since, at the rate it moved (up to 4 frames a tick), and crossfading animations' weights partway
|
||||
// to the next tick's; the skeleton is posed (by IMVU's), and the times and weights put back, with the skeleton marked to
|
||||
// be posed again, so everything else sees the tick's pose.
|
||||
void __fastcall updateCalSkeleton(void* model, void* /*edx*/)
|
||||
{
|
||||
const float fraction = tick_fraction();
|
||||
@@ -139,20 +156,30 @@ void __fastcall updateCalSkeleton(void* model, void* /*edx*/)
|
||||
const float frame = *secondsPerFrame;
|
||||
|
||||
struct Saved {
|
||||
float* time;
|
||||
float value;
|
||||
float* value;
|
||||
float was;
|
||||
};
|
||||
Saved saved[64];
|
||||
Saved saved[128];
|
||||
std::size_t count = 0;
|
||||
const auto set = [&](float& value, float to) {
|
||||
saved[count++] = {&value, value};
|
||||
value = to;
|
||||
};
|
||||
void* head = field<void*>(model, modelEffects);
|
||||
for (void* node = head != nullptr ? field<void*>(head, 0) : nullptr; node != nullptr && node != head && count < 64;
|
||||
for (void* node = head != nullptr ? field<void*>(head, 0) : nullptr; node != nullptr && node != head && count < 126;
|
||||
node = field<void*>(node, 0)) {
|
||||
void* effect = field<void*>(node, 8);
|
||||
if (effect == nullptr || field<std::uint8_t>(effect, effectPlaying) == 0) {
|
||||
void* animation = effect != nullptr ? field<void*>(effect, effectAnimation) : nullptr;
|
||||
if (animation == nullptr) {
|
||||
continue;
|
||||
}
|
||||
void* animation = field<void*>(effect, effectAnimation);
|
||||
if (animation == nullptr) {
|
||||
const unsigned faded = field<std::uint32_t>(effect, effectFadeCount);
|
||||
if (crossfades && field<std::uint8_t>(effect, effectFadesIn) != 0 && field<std::int32_t>(effect, effectFadeSource) != 0
|
||||
&& faded >= 1 && faded < fadeTicks) {
|
||||
set(field<float>(animation, animationWeight),
|
||||
(static_cast<float>(faded) + fraction) * 0.1f * field<float>(effect, effectWeight));
|
||||
}
|
||||
if (field<std::uint8_t>(effect, effectPlaying) == 0) {
|
||||
continue;
|
||||
}
|
||||
const float time = field<float>(effect, effectTime);
|
||||
@@ -166,9 +193,18 @@ void __fastcall updateCalSkeleton(void* model, void* /*edx*/)
|
||||
if (history.previous < 0 || perTick <= frame * 0.01f || perTick > frame * 4) {
|
||||
continue; // held, looped back, or just started: as it is
|
||||
}
|
||||
float& posedTime = field<float>(animation, animationTime);
|
||||
saved[count++] = {&posedTime, posedTime};
|
||||
posedTime = time + fraction * perTick;
|
||||
set(field<float>(animation, animationTime), time + fraction * perTick);
|
||||
}
|
||||
void* fadingHead = crossfades ? field<void*>(model, modelFadingOut) : nullptr;
|
||||
for (void* node = fadingHead != nullptr ? field<void*>(fadingHead, 0) : nullptr;
|
||||
node != nullptr && node != fadingHead && count < 127; node = field<void*>(node, 0)) {
|
||||
void* effect = field<void*>(node, fadingOutEffect);
|
||||
void* animation = effect != nullptr ? field<void*>(effect, effectAnimation) : nullptr;
|
||||
const unsigned faded = field<std::uint32_t>(node, fadingOutCount);
|
||||
if (animation != nullptr && faded >= 1 && faded < fadeTicks) {
|
||||
set(field<float>(animation, animationWeight),
|
||||
(1.0f - (static_cast<float>(faded) + fraction) * 0.1f) * field<float>(effect, effectWeight));
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
originalUpdateCalSkeleton(model);
|
||||
@@ -176,8 +212,8 @@ void __fastcall updateCalSkeleton(void* model, void* /*edx*/)
|
||||
}
|
||||
field<std::uint8_t>(model, modelPoseDirty) = 1;
|
||||
originalUpdateCalSkeleton(model);
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
*saved[i].time = saved[i].value;
|
||||
for (std::size_t i = count; i-- > 0;) {
|
||||
*saved[i].value = saved[i].was;
|
||||
}
|
||||
field<std::uint8_t>(model, modelPoseDirty) = 1;
|
||||
|
||||
@@ -364,6 +400,17 @@ void install_pose(HMODULE sceneWindow)
|
||||
return;
|
||||
}
|
||||
poseHooked = true;
|
||||
|
||||
// Crossfades: ModelInstance::step's instructions that count a fade in (cmp dword ptr [eax + 0x48], 10) and a fade
|
||||
// out (cmp dword ptr [ebx + 0x14], 10), where expected.
|
||||
auto* step = reinterpret_cast<const std::byte*>(GetProcAddress(sceneWindow, "?step@ModelInstance@@QAEXABVFrameInterval@@@Z"));
|
||||
constexpr std::uint8_t fadeIn[] = {0x83, 0x78, 0x48, 0x0a};
|
||||
constexpr std::uint8_t fadeOut[] = {0x83, 0x7b, 0x14, 0x0a};
|
||||
crossfades = step != nullptr && std::memcmp(step + 0xc6, fadeIn, sizeof(fadeIn)) == 0
|
||||
&& std::memcmp(step + 0xf1, fadeOut, sizeof(fadeOut)) == 0;
|
||||
if (!crossfades) {
|
||||
log::info("scene hooks: ModelInstance::step's crossfades not as expected; crossfades at the client's rate");
|
||||
}
|
||||
}
|
||||
|
||||
void install_particles(HMODULE sceneWindow)
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
// calls stepFrame, which steps each scene viewer). Recorded, for the frame pacer and for drawing between ticks.
|
||||
// - 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.
|
||||
// tick; Cal3D poses between keyframes) and crossfading animations' weights partway to the next tick's, then its
|
||||
// animations are put back as they were.
|
||||
// - ModelInstance::render's call to ParticleSystem::render: between ticks, particles drawn moved on by their velocity
|
||||
// times the time since the tick (as the particle library moves them at each tick), then put back.
|
||||
// - CameraController::getCameraTransform (its exported vtable): between ticks, the camera where its easing toward its
|
||||
|
||||
@@ -99,7 +99,8 @@ share 0.4 of the way to its target per step) has brought it, `1 - 0.6^fraction`
|
||||
getCameraTransform, in its exported vtable). Nothing is predicted: both are what the next step would give, at the
|
||||
fraction; and particles moved on by their velocity times the time since the step (ModelInstance::render's call to
|
||||
ParticleSystem::render, redirected), then back by the same (the client sorts them by distance as it draws them, which
|
||||
reorders them). Still at 30 per second: crossfades between animations, gaze.
|
||||
reorders them); and crossfades between animations (weights ramped in tenths over 10 steps by ModelInstance::step),
|
||||
partway to the next step's weights. Still at 30 per second: gaze (heads turning to whoever's talking).
|
||||
|
||||
Next: sharing pipelines between views (each view's scene compiles its own).
|
||||
|
||||
|
||||
Reference in new issue
Block a user