diff --git a/src/skate3_app_common.cpp b/src/skate3_app_common.cpp index 7834d78..e625d14 100644 --- a/src/skate3_app_common.cpp +++ b/src/skate3_app_common.cpp @@ -81,6 +81,11 @@ extern const rex::PPCImageInfo eawebkit_PPCImageConfig; // lift via config because they overlap with auto-discovered parent functions. extern "C" REX_FUNC(__restgprlr_19); +// Defined in skate3_native_scene.cpp; the freecam hotkey below toggles it, +// and while it captures input the guest input system is gated off. +REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam); +REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam_capture_input); + REXCVAR_DEFINE_STRING(skate3_dlc_root, "", "Skate 3", "Directory containing Skate 3 DLC package files"); REXCVAR_DEFINE_BOOL(skate3_auto_install_dlc, true, "Skate 3", @@ -757,6 +762,12 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) { "Native render debug menu", [this] { ToggleNativeDebug(); }); + rex::ui::RegisterBind("bind_skate3_freecam", "End", + "Drone camera (free fly)", [] { + REXCVAR_SET( + skate3_native_render_scene_freecam, + !REXCVAR_GET(skate3_native_render_scene_freecam)); + }); } void Skate3BaseApp::OnPostSetup() { @@ -768,7 +779,12 @@ void Skate3BaseApp::OnPostSetup() { input_system->SetActiveCallback([this]() { const bool settings_visible = simple_settings_dialog_ && simple_settings_dialog_->visible(); const bool xam_ui_active = rex::kernel::xam::xeXamIsUIActive(); - return !settings_visible && !xam_ui_active; + // The drone cam owns the keyboard while flying: keep guest input off + // so the fly keys don't also steer the skater. + const bool freecam_captures = + REXCVAR_GET(skate3_native_render_scene_freecam) && + REXCVAR_GET(skate3_native_render_scene_freecam_capture_input); + return !settings_visible && !xam_ui_active && !freecam_captures; }); input_system->SetMenuChordCallback([this]() { app_context().CallInUIThreadDeferred([this]() { ToggleSimpleSettings(); }); diff --git a/src/skate3_draw_distance.cpp b/src/skate3_draw_distance.cpp index f3924e0..8e731db 100644 --- a/src/skate3_draw_distance.cpp +++ b/src/skate3_draw_distance.cpp @@ -31,6 +31,7 @@ #include "generated/skate3_init.h" #include "native/skate3_native_guest_read.h" #include "skate3_demo_path.h" +#include "skate3_native_scene.h" #include #include @@ -313,11 +314,19 @@ extern "C" REX_FUNC(sub_82792900) { } } const double scale = REXCVAR_GET(skate3_draw_distance_scale); - if (std::abs(scale - 1.0) <= kScaleEpsilon) { - return; + if (std::abs(scale - 1.0) > kScaleEpsilon) { + const uint32_t addr = entity + 516; + StoreGuestF32(base, addr, float(double(LoadGuestF32(base, addr)) * scale)); + } + // Drone camera engaged: recenter the size-cull reference on the flown + // position (lanes 0/2 are the camera X/Z the cull job measures distance + // from) so world pieces near the drone don't get distance-culled against + // the far-away gameplay camera. + float drone[3]; + if (skate3::native_scene::FreecamGuestPose(drone)) { + StoreGuestF32(base, entity + 512, drone[0]); + StoreGuestF32(base, entity + 520, drone[2]); } - const uint32_t addr = entity + 516; - StoreGuestF32(base, addr, float(double(LoadGuestF32(base, addr)) * scale)); } // Asset-stream request logging (diagnostics only): a pop-in that coincides diff --git a/src/skate3_native_debug_dialog.cpp b/src/skate3_native_debug_dialog.cpp index 59f1ce0..b9d547b 100644 --- a/src/skate3_native_debug_dialog.cpp +++ b/src/skate3_native_debug_dialog.cpp @@ -84,6 +84,10 @@ REXCVAR_DECLARE(double, skate3_native_render_scene_synthetic_pan_rate); REXCVAR_DECLARE(double, skate3_native_render_scene_synthetic_pan_amp); // SDK: emulated-draw suppression while the native output is active. REXCVAR_DECLARE(bool, native_render_suppress_emulated_draws); +REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam); +REXCVAR_DECLARE(double, skate3_native_render_scene_freecam_speed); +REXCVAR_DECLARE(double, skate3_native_render_scene_freecam_look_speed); +REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam_capture_input); namespace skate3 { namespace { @@ -111,6 +115,39 @@ double CvarSlider(const char* label, double value, float lo, float hi, return value; } +// Drone-camera controls (same cvars everywhere the section is embedded; +// edits apply live). +void DrawFreecamControls() { + ImGui::SeparatorText("Drone camera"); + bool freecam = REXCVAR_GET(skate3_native_render_scene_freecam); + if (ImGui::Checkbox("Free-fly drone cam (End)", &freecam)) { + REXCVAR_SET(skate3_native_render_scene_freecam, freecam); + } + ImGui::SameLine(); + bool capture = REXCVAR_GET(skate3_native_render_scene_freecam_capture_input); + if (ImGui::Checkbox("capture game input", &capture)) { + REXCVAR_SET(skate3_native_render_scene_freecam_capture_input, capture); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "While flying, keep keyboard/controller input away from the game so\n" + "the fly keys don't also steer the skater."); + } + REXCVAR_SET(skate3_native_render_scene_freecam_speed, + CvarSlider("fly speed (m/s)", + REXCVAR_GET(skate3_native_render_scene_freecam_speed), + 0.5f, 60.0f, "%.1f", + "Base fly speed; Shift = 4x, Ctrl = 0.2x")); + REXCVAR_SET( + skate3_native_render_scene_freecam_look_speed, + CvarSlider("look speed (deg/s)", + REXCVAR_GET(skate3_native_render_scene_freecam_look_speed), + 20.0f, 240.0f, "%.0f", "Arrow-key look rate")); + ImGui::TextDisabled( + "WASD fly, E/Space up, Q/C down, arrows or right-mouse drag look,\n" + "Z/X zoom, Shift fast, Ctrl slow."); +} + } // namespace void NativeDebugDialog::Show() { @@ -203,6 +240,8 @@ void NativeDebugDialog::OnDraw(ImGuiIO& io) { "World env materials cull FRONT like the game's material " "XMLs; off = legacy cull-none (shows interior faces)")); + DrawFreecamControls(); + ImGui::SeparatorText("HDR post effects (live)"); REXCVAR_SET(skate3_native_render_scene_hdr, CvarCheckbox("HDR intermediate", diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index 2fde21f..f4cc1c6 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -292,6 +292,31 @@ REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_haze_density, 0.005, "200 units).") .range(0.0, 0.05) .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_BOOL(skate3_native_render_scene_freecam, false, "Skate 3", + "Detach the render camera from the game (drone / free-fly " + "cam): WASD fly, E/Space up, Q/C down, arrow keys or " + "right-mouse drag look, Z/X zoom, Shift fast, Ctrl slow. " + "The game keeps simulating, but its main render camera is " + "taken over (ViewCamera::SetViewMatrix override), so the " + "game culls and submits the world around the flown pose " + "itself. Also on the End key (bind_skate3_freecam).") + .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_freecam_speed, 8.0, "Skate 3", + "Freecam base fly speed in world units (meters) per " + "second. Shift = 4x, Ctrl = 0.2x.") + .range(0.5, 100.0) + .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_freecam_look_speed, 90.0, + "Skate 3", + "Freecam arrow-key look rate in degrees per second.") + .range(10.0, 360.0) + .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_BOOL(skate3_native_render_scene_freecam_capture_input, true, + "Skate 3", + "While the freecam is engaged, keep keyboard/controller " + "input away from the game so flying doesn't also steer " + "the skater. Turn off to fly and play at the same time.") + .lifecycle(rex::cvar::Lifecycle::kHotReload); REXCVAR_DEFINE_BOOL(skate3_native_render_scene_2d, true, "Skate 3", "Replay the game's 2D/APT (Flash HUD) draws as a native overlay pass " "on top of the 3D scene") @@ -5698,6 +5723,244 @@ void ComposeViewProj(const CamPose& p, const float proj[16], float vp_out[16], std::memcpy(cam_out, p.c, 3 * sizeof(float)); } +// Drone / free-fly camera: overrides the published pose with a user-flown +// one while the game keeps simulating on its own camera. Engaged from the +// current guest pose (roll is dropped; the gameplay camera never rolls), +// the projection is frozen at engage so gameplay FOV changes don't disturb +// the shot, and both linear and angular key input drive exponentially +// smoothed velocities for drone-like motion. +// +// Guest-side culling is handled by taking over the game's own render +// camera: the frame build publishes the flown view matrix below, and the +// Sk8::Render::ViewCamera::SetViewMatrix override (bottom of this file) +// rewrites the game's main-camera argument with it, so the game derives +// its view-proj, frustum planes and cull/submit lists from the drone pose +// itself; statics, characters and props are all submitted exactly as if +// the game were looking there. (A draw-item union like the synthetic-pan +// probe's cannot do this: it can only re-append items the game already +// submitted from poses it actually rendered, and rigid props that ride +// entity animation smear at their stale transforms.) +std::mutex g_freecam_guest_mutex; +float g_freecam_guest_view[16] = {}; // row-vector view, host endianness +float g_freecam_guest_pos[3] = {}; // camera world position +std::atomic g_freecam_guest_active{0}; +std::atomic g_freecam_guest_rewrites{0}; +struct FreecamState { + bool engaged = false; + double yaw = 0.0, pitch = 0.0; // radians: yaw about world Y, then pitch + double pos[3] = {}; + double vel[3] = {}; // smoothed world-space velocity + double yaw_vel = 0.0, pitch_vel = 0.0; // smoothed key-look rates (rad/s) + double zoom = 1.0; // projection scale (Z/X keys) + float proj0[16] = {}; // projection frozen at engage + // Basis and screen-direction signs resolved numerically at engage (the + // view right/up column signs against cross-product candidates, and the + // projection's x/y scale signs), so no handedness convention is assumed; + // the game's projection carries a negative x scale. + float sign_right = 1.0f, sign_up = 1.0f; + double look_sign_x = 1.0, look_sign_y = 1.0; + double last_t = 0.0; +#if defined(_WIN32) + bool mouse_anchored = false; + POINT mouse_last = {}; +#endif +}; +FreecamState g_freecam; + +// Guest render thread only. Returns true while engaged; scene.view_proj, +// scene.proj and scene.cam_pos then hold the flown pose. +bool UpdateFreecam(FrameScene& scene, const float cam_view[16], double now) { + FreecamState& fc = g_freecam; + if (!REXCVAR_GET(skate3_native_render_scene_freecam)) { + if (fc.engaged) { + fc.engaged = false; + g_freecam_guest_active.store(0, std::memory_order_release); + REXLOG_INFO( + "native-scene freecam: off (guest camera restored; {} guest " + "SetViewMatrix rewrites while engaged)", + g_freecam_guest_rewrites.exchange(0, std::memory_order_relaxed)); + } + return false; + } + if (!fc.engaged) { + // Engage from this frame's raw guest pose. The view's upper-3x3 columns + // are the camera axes in world space (row-vector convention): + // 0 = right, 1 = up, 2 = forward. + const float f0[3] = {cam_view[2], cam_view[6], cam_view[10]}; + fc.yaw = std::atan2(double(f0[0]), double(f0[2])); + fc.pitch = std::asin(std::clamp(double(f0[1]), -1.0, 1.0)); + for (int j = 0; j < 3; ++j) { + fc.pos[j] = -(cam_view[12] * cam_view[j * 4 + 0] + + cam_view[13] * cam_view[j * 4 + 1] + + cam_view[14] * cam_view[j * 4 + 2]); + } + // Right candidate cross(world_up, forward) = (f.z, 0, -f.x): its sign + // against the live right column fixes the basis handedness. + fc.sign_right = f0[2] * cam_view[0] - f0[0] * cam_view[8] >= 0.0f ? 1.0f : -1.0f; + const float r0[3] = {f0[2] * fc.sign_right, 0.0f, -f0[0] * fc.sign_right}; + const float u0[3] = {f0[1] * r0[2] - f0[2] * r0[1], + f0[2] * r0[0] - f0[0] * r0[2], + f0[0] * r0[1] - f0[1] * r0[0]}; + fc.sign_up = u0[0] * cam_view[1] + u0[1] * cam_view[5] + u0[2] * cam_view[9] >= 0.0f + ? 1.0f + : -1.0f; + std::memcpy(fc.proj0, scene.proj, sizeof(fc.proj0)); + // Screen-relative look/strafe directions: view +x maps to screen right + // only when the projection x scale is positive (it isn't in this game), + // so fold the projection signs in once. +yaw rotates forward toward +x + // (the right column direction times sign_right), hence the product. + fc.look_sign_x = double(fc.sign_right) * (fc.proj0[0] >= 0.0f ? 1.0 : -1.0); + fc.look_sign_y = double(fc.sign_up) * (fc.proj0[5] >= 0.0f ? 1.0 : -1.0); + fc.vel[0] = fc.vel[1] = fc.vel[2] = 0.0; + fc.yaw_vel = fc.pitch_vel = 0.0; + fc.zoom = 1.0; + fc.last_t = now; +#if defined(_WIN32) + fc.mouse_anchored = false; +#endif + fc.engaged = true; + REXLOG_INFO( + "native-scene freecam: ENGAGED at ({:.1f}, {:.1f}, {:.1f}); WASD fly, " + "E/Space up, Q/C down, arrows/right-drag look, Z/X zoom, Shift fast, " + "Ctrl slow", + fc.pos[0], fc.pos[1], fc.pos[2]); + } + const double dt = std::clamp(now - fc.last_t, 0.0, 0.1); + fc.last_t = now; + + double mv_f = 0.0, mv_r = 0.0, mv_u = 0.0; // move intent (camera-relative) + double lk_yaw = 0.0, lk_pitch = 0.0; // arrow-key look intent + double mouse_yaw = 0.0, mouse_pitch = 0.0; // right-drag deltas (radians) + double speed_mult = 1.0, zoom_dir = 0.0; +#if defined(_WIN32) + const auto down = [](int vk) { return (GetAsyncKeyState(vk) & 0x8000) != 0; }; + mv_f = (down('W') ? 1.0 : 0.0) - (down('S') ? 1.0 : 0.0); + mv_r = (down('D') ? 1.0 : 0.0) - (down('A') ? 1.0 : 0.0); + mv_u = ((down('E') || down(VK_SPACE)) ? 1.0 : 0.0) - + ((down('Q') || down('C')) ? 1.0 : 0.0); + lk_yaw = (down(VK_RIGHT) ? 1.0 : 0.0) - (down(VK_LEFT) ? 1.0 : 0.0); + lk_pitch = (down(VK_UP) ? 1.0 : 0.0) - (down(VK_DOWN) ? 1.0 : 0.0); + zoom_dir = (down('Z') ? 1.0 : 0.0) - (down('X') ? 1.0 : 0.0); + if (down(VK_SHIFT)) { + speed_mult = 4.0; + } else if (down(VK_CONTROL)) { + speed_mult = 0.2; + } + if (down(VK_RBUTTON)) { + POINT p; + if (GetCursorPos(&p)) { + if (fc.mouse_anchored) { + constexpr double kRadPerPixel = 0.0022; // ~0.13 deg per pixel + mouse_yaw = (p.x - fc.mouse_last.x) * kRadPerPixel; + mouse_pitch = -(p.y - fc.mouse_last.y) * kRadPerPixel; + } + fc.mouse_last = p; + fc.mouse_anchored = true; + } + } else { + fc.mouse_anchored = false; + } +#endif + + // Look: arrow keys drive a smoothed angular velocity (cinematic ease-in/ + // out); mouse deltas apply directly. Pitch is clamped short of the poles. + const double look_rate = + REXCVAR_GET(skate3_native_render_scene_freecam_look_speed) * + (3.14159265358979323846 / 180.0); + const double ang_k = dt > 0.0 ? 1.0 - std::exp(-dt / 0.08) : 0.0; + fc.yaw_vel += (lk_yaw * look_rate - fc.yaw_vel) * ang_k; + fc.pitch_vel += (lk_pitch * look_rate - fc.pitch_vel) * ang_k; + fc.yaw += (fc.yaw_vel * dt + mouse_yaw) * fc.look_sign_x; + fc.pitch += (fc.pitch_vel * dt + mouse_pitch) * fc.look_sign_y; + constexpr double kPitchMax = 89.0 * 3.14159265358979323846 / 180.0; + fc.pitch = std::clamp(fc.pitch, -kPitchMax, kPitchMax); + if (zoom_dir != 0.0) { + // Zoom-in only: the guest camera (and therefore the game's culling) + // keeps the unzoomed projection, so a narrower rendered FOV is always a + // subset of what was culled; zooming wider would show unculled edges. + fc.zoom = std::clamp(fc.zoom * std::exp(zoom_dir * dt), 1.0, 4.0); + } + + // Basis from yaw/pitch (unit length by construction, signs from engage). + const double cy = std::cos(fc.yaw), sy = std::sin(fc.yaw); + const double cp = std::cos(fc.pitch), sp = std::sin(fc.pitch); + const float fwd[3] = {float(sy * cp), float(sp), float(cy * cp)}; + const float right[3] = {float(cy) * fc.sign_right, 0.0f, + float(-sy) * fc.sign_right}; + const float up[3] = {float(-sp * sy) * fc.sign_right * fc.sign_up, + float(cp) * fc.sign_right * fc.sign_up, + float(-sp * cy) * fc.sign_right * fc.sign_up}; + + // Move: forward along the view direction (true drone flight), strafe + // along screen right, vertical along world up; combined intent normalized + // so diagonals don't fly faster. Exponential ease toward the target. + const double ilen = std::sqrt(mv_f * mv_f + mv_r * mv_r + mv_u * mv_u); + const double speed = REXCVAR_GET(skate3_native_render_scene_freecam_speed) * + speed_mult * (ilen > 1.0 ? 1.0 / ilen : 1.0); + const double strafe = mv_r * fc.look_sign_x * speed; + const double tvel[3] = {mv_f * speed * fwd[0] + strafe * right[0], + mv_f * speed * fwd[1] + mv_u * speed, + mv_f * speed * fwd[2] + strafe * right[2]}; + const double vel_k = dt > 0.0 ? 1.0 - std::exp(-dt / 0.25) : 0.0; + for (int j = 0; j < 3; ++j) { + fc.vel[j] += (tvel[j] - fc.vel[j]) * vel_k; + fc.pos[j] += fc.vel[j] * dt; + } + + // Compose and publish: view columns = right/up/forward, translation row + // = -pos * R; projection = the engage-frozen one with the zoom folded in + // (published to scene.proj too so depth-based post passes unproject with + // what was actually rendered). + float view[16] = {}; + for (int i = 0; i < 3; ++i) { + view[i * 4 + 0] = right[i]; + view[i * 4 + 1] = up[i]; + view[i * 4 + 2] = fwd[i]; + } + const float posf[3] = {float(fc.pos[0]), float(fc.pos[1]), float(fc.pos[2])}; + for (int k = 0; k < 3; ++k) { + view[12 + k] = -(posf[0] * view[0 * 4 + k] + posf[1] * view[1 * 4 + k] + + posf[2] * view[2 * 4 + k]); + } + view[15] = 1.0f; + // Hand the flown view to the guest-camera takeover (the SetViewMatrix + // override): the game's next camera update culls/submits with this pose. + { + std::lock_guard lock(g_freecam_guest_mutex); + std::memcpy(g_freecam_guest_view, view, sizeof(view)); + std::memcpy(g_freecam_guest_pos, posf, sizeof(posf)); + } + g_freecam_guest_active.store(1, std::memory_order_release); + float pr[16]; + std::memcpy(pr, fc.proj0, sizeof(pr)); + pr[0] *= float(fc.zoom); + pr[5] *= float(fc.zoom); + std::memcpy(scene.proj, pr, sizeof(pr)); + for (int r = 0; r < 4; ++r) { + for (int col = 0; col < 4; ++col) { + float sum = 0.0f; + for (int k = 0; k < 4; ++k) { + sum += view[r * 4 + k] * pr[k * 4 + col]; + } + scene.view_proj[r * 4 + col] = sum; + } + } + std::memcpy(scene.cam_pos, posf, sizeof(posf)); + return true; +} + +// While the freecam is engaged, fills out_pos with the flown camera world +// position and returns true. Consumed by the draw-distance hooks to +// recenter the guest distance culls on the drone. +bool FreecamGuestPose(float out_pos[3]) { + if (g_freecam_guest_active.load(std::memory_order_acquire) == 0) { + return false; + } + std::lock_guard lock(g_freecam_guest_mutex); + std::memcpy(out_pos, g_freecam_guest_pos, 3 * sizeof(float)); + return true; +} + // Guest render thread only. Returns true when vp_out/cam_out hold a // re-timed pose for `now`; false = keep the raw guest pose (no history yet, // or a cut/teleport snapped). @@ -8385,7 +8648,8 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { // ring's coherent history instead of a frozen step. if (REXCVAR_GET(skate3_native_render_scene_retain_offscreen) && REXCVAR_GET(skate3_native_render_scene_dynamic_items) && - g_synpan_active.load(std::memory_order_relaxed) == 0) { + g_synpan_active.load(std::memory_order_relaxed) == 0 && + !REXCVAR_GET(skate3_native_render_scene_freecam)) { // Covers the camera-smoothing lag only; vehicle pose data just ages. constexpr uint64_t kDynRetainFrames = 10; const uint64_t rnow = g_guest_frame; @@ -8879,10 +9143,13 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { // Off-screen retention (see g_retained_items): re-append statics the game // view-culled this frame while the trailing rendered pose can still see // them. Runs AFTER the smoothing block so retained copies never enter the - // dynamic pose histories, and stands down while the synthetic-pan probe - // maintains its own full-surround union. + // dynamic pose histories. Stands down while the synthetic-pan probe + // maintains its own full-surround union, and while the freecam has taken + // over the guest camera (the game then culls around the rendered pose + // directly, so re-appends would only risk double draws). if (REXCVAR_GET(skate3_native_render_scene_retain_offscreen) && - g_synpan_active.load(std::memory_order_relaxed) == 0) { + g_synpan_active.load(std::memory_order_relaxed) == 0 && + !REXCVAR_GET(skate3_native_render_scene_freecam)) { if (g_retained_clear.exchange(false, std::memory_order_relaxed)) { g_retained_items.clear(); g_dyn_retained.clear(); @@ -9125,6 +9392,14 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { } } + // Drone / free-fly camera (skate3_native_render_scene_freecam, End key): + // runs after the smoothing and synthetic-pan blocks so the flown pose + // wins while engaged. No draw-item union here; the SetViewMatrix + // override hands the flown pose to the game, whose own culling then + // submits exactly what the drone sees (statics AND animated entities). + UpdateFreecam(scene, cam_view, + std::chrono::duration(build_t0.time_since_epoch()).count()); + // Publish the frame's captured fog rows and re-arm the OnDrawDone capture // (keyed to this frame's camera) for the next frame. if (g_fog_have) { @@ -9501,3 +9776,36 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { } // namespace skate3::native_scene +// Sk8::Render::ViewCamera::SetViewMatrix(const rw::math::Matrix44&): the +// game passes each frame's freshly computed view matrix here and derives +// the stored view (+0x20), its transpose (+0xE0), the view-proj (+0xA0) and +// the frustum data used for culling from the argument. While the freecam is +// engaged, rewrite the incoming matrix for the MAIN scene camera (the one +// the frame build publishes as g_sampler_viewcam) with the flown view +// before the game consumes it; culling, submission and every derived +// consumer then operate around the drone pose natively. Other ViewCameras +// (shadow cascades, portrait render-to-texture passes) pass through +// untouched. +extern "C" REX_FUNC(sub_82802A00) { + namespace ns = skate3::native_scene; + const uint32_t cam = ctx.r3.u32; + const uint32_t mtx = ctx.r4.u32; + if (ns::g_freecam_guest_active.load(std::memory_order_acquire) != 0 && + mtx != 0 && + cam == ns::g_sampler_viewcam.load(std::memory_order_relaxed)) { + float v[16]; + { + std::lock_guard lock(ns::g_freecam_guest_mutex); + std::memcpy(v, ns::g_freecam_guest_view, sizeof(v)); + } + for (int i = 0; i < 16; ++i) { + uint32_t raw; + std::memcpy(&raw, &v[i], 4); + raw = __builtin_bswap32(raw); + std::memcpy(base + mtx + i * 4, &raw, 4); + } + ns::g_freecam_guest_rewrites.fetch_add(1, std::memory_order_relaxed); + } + __imp__sub_82802A00(ctx, base); +} + diff --git a/src/skate3_native_scene.h b/src/skate3_native_scene.h index 62e8cc0..655f5ae 100644 --- a/src/skate3_native_scene.h +++ b/src/skate3_native_scene.h @@ -447,6 +447,13 @@ void SetSettingsMenuBlur(bool enabled); // override to seed its sliders from (unit vector toward the sun). void GetCapturedSunDir(float out[3]); +// While the drone/free-fly camera is engaged, fills out_pos with the flown +// camera world position and returns true (false = guest camera in control). +// Consumed by the draw-distance hooks to recenter distance culls on the +// drone; the render camera itself is taken over via the ViewCamera:: +// SetViewMatrix override in skate3_native_scene.cpp. +bool FreecamGuestPose(float out_pos[3]); + // The game's per-entity spawn/streaming fade as carried by the item's // validated character-lighting capture (peds c21.x, defaultcharacter c13.x, // cacstamp c22.x, vehicle body c20.x, hair strand scale). 1.0 for items