Skip to content

Fix camera jitter: 4-phase physics/rendering audit#4

Merged
Virciti merged 4 commits intomainfrom
Virciti/audit-both-games
Mar 15, 2026
Merged

Fix camera jitter: 4-phase physics/rendering audit#4
Virciti merged 4 commits intomainfrom
Virciti/audit-both-games

Conversation

@Virciti
Copy link
Copy Markdown
Owner

@Virciti Virciti commented Mar 15, 2026

Summary

  • Phase 1: Eliminate re-render storm — replace per-frame React state with refs for truck position/rotation/speed, throttle HUD sync to 100ms
  • Phase 2: Smooth camera behavior — velocity dampening, angular velocity clamping (2.5 rad/s max), speed-based look-ahead and pull-back, wire up smoothness prop
  • Phase 3: Stabilize physics output — reduce ground height lerp from 12/s to 4/s, smooth boundary corrections (no hard clamp), align gravity (25), AITruck smooth terrain, frame-rate independent friction
  • Phase 4: Optimize rendering pipeline — React.memo on RaceHUD3D + Minimap, throttle AI positions, stable callbacks via raceTimeRef, extract inline allocations

Test plan

  • 107 tests across 4 phase test files, all passing
  • Full suite: 278 passed (2 pre-existing failures unrelated to changes)
  • Zero TypeScript errors in changed files
  • No regressions in Phase 1 tests when Phase 2-4 applied

🤖 Generated with Claude Code

Virciti and others added 4 commits March 15, 2026 13:11
…d HUD

Root cause: truck position/rotation/speed were stored as React state, causing
~60 re-renders/second. Camera consumed stale state props, amplifying jitter.

Phase 1 fix:
- Convert truckPosition/truckRotation/speed from useState to useRef
- FollowCamera reads RefObject props directly in useFrame (zero re-renders)
- Add throttled HUD sync (100ms interval) for minimap/speedometer display
- Gentler camera smoothing (Math.pow base 0.08 → posSmooth, 0.04 → lookSmooth)
- Reduce default shake intensity from 0.4 to 0.2
- Update StadiumGame3D for new FollowCamera ref interface
- Add 29 tests validating all Phase 1 changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d prediction

Phase 2 of camera/physics audit — smooth camera behavior:
- Velocity-based look-ahead: camera looks further ahead at higher speeds
- Speed-dependent pull-back: camera pulls back up to 4 units at max speed
- Camera velocity dampening: smooths acceleration to prevent overshoot on turns
- Angular velocity clamping: limits camera rotation to 2.5 rad/s max (no whip-pan)
- smoothness prop now actually drives interpolation (was ignored before)
- speedRef prop added: FollowCamera receives live speed data from truck
- Update RaceGame3D and StadiumGame3D to pass speedRef
- 31 Phase 2 tests + updated Phase 1 test (60 total, all passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ravity

Phase 3 of camera/physics audit — stabilize physics output:
- Reduce ground height lerp from 12/s to 4/s (both jump and race mode)
  Prevents vertical jitter that amplifies through camera
- Smooth boundary corrections: jump arena and radial boundary use
  gradual push-back instead of hard position clamping
- Align gravity: MonsterTruck 30→25, Scene3D Physics -20→-25
  Consistent gravity across manual and Rapier physics
- AITruck smooth terrain: add smoothGroundHeight lerp (was snapping
  directly to getRaceTrackHeight causing visible AI truck jitter)
- Frame-rate independent friction: spin decay, coasting, mud pit,
  and boundary penalties use Math.pow(factor, delta*60)
- 25 Phase 3 tests (85 total across all phases, all passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 4 of camera/physics audit — rendering pipeline optimization:
- Wrap RaceHUD3D in React.memo to skip re-renders when props unchanged
- Wrap Minimap sub-component in memo() to avoid SVG re-rendering
- Throttle AI positions: move aiPositions from inline .map() in JSX
  to hudAiPositions state synced every 100ms (eliminates per-render
  array/object allocation that broke memoization)
- Add raceTimeRef: handlePositionUpdate and position ranking useEffect
  now read from ref instead of depending on raceTime state — eliminates
  callback recreation 10x/sec
- Extract StadiumGame3D constants: STADIUM_CAMERA_OFFSET (was inline
  new THREE.Vector3 every render) and STADIUM_STAR_POSITIONS
- 22 Phase 4 tests (107 total across all phases, all passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Virciti Virciti merged commit 38ac13e into main Mar 15, 2026
0 of 2 checks passed
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the game's visual and physical stability by addressing camera jitter, improving physics calculations, and optimizing the rendering pipeline. The changes focus on reducing React re-renders by leveraging refs for frequently updated game state, smoothing camera movements with advanced interpolation and clamping, and making physics interactions more consistent across different frame rates. These improvements collectively lead to a much smoother and more predictable gameplay experience.

Highlights

  • Eliminated Re-render Storm: Replaced per-frame React state with useRef for truck position, rotation, and speed, throttling HUD synchronization to 100ms to reduce unnecessary component re-renders.
  • Smoothed Camera Behavior: Implemented velocity dampening, angular velocity clamping (max 2.5 rad/s), speed-based look-ahead and pull-back, and wired up a smoothness prop for configurable camera feel.
  • Stabilized Physics Output: Reduced ground height linear interpolation (lerp) from 12/s to 4/s, introduced smooth boundary corrections instead of hard clamping, aligned gravity to 25, smoothed AI truck terrain following, and made friction calculations frame-rate independent.
  • Optimized Rendering Pipeline: Applied React.memo to RaceHUD3D and Minimap components, throttled AI positions for HUD display, ensured stable callbacks via raceTimeRef, and extracted inline allocations to module-level constants.
Changelog
  • tests/components/games3d/phase1-rerender-fix.test.ts
    • Added new test file to validate ref-based state management and throttled HUD updates.
  • tests/components/games3d/phase2-smooth-camera.test.ts
    • Added new test file to verify camera smoothing, velocity dampening, and angular clamping.
  • tests/components/games3d/phase3-physics-stabilization.test.ts
    • Added new test file to confirm physics stabilization, including ground lerp and boundary corrections.
  • tests/components/games3d/phase4-rendering-optimization.test.ts
    • Added new test file to check rendering optimizations like React.memo and stable constants.
  • components/games3d/AITruck.tsx
    • Added smoothGroundHeight ref to manage AI truck's ground height.
    • Implemented smooth ground height lerp for AI truck to prevent vertical jitter.
    • Updated chassis translation to use the smoothed ground height.
  • components/games3d/FollowCamera.tsx
    • Defined MAX_ANGULAR_VELOCITY constant for camera rotation clamping.
    • Updated FollowCameraProps to accept RefObject for target position, rotation, and speed.
    • Reduced default shakeIntensity for camera shake effect.
    • Introduced refs for camera velocity and previous positions to enable velocity dampening.
    • Added refs for previous and smoothed rotation to implement angular velocity clamping.
    • Implemented speed-dependent look-ahead and camera pull-back for dynamic framing.
    • Refactored camera position smoothing to incorporate velocity dampening and frame-rate independent calculations.
    • Adjusted smoothing calculations to use the smoothness prop for configurable interpolation.
  • components/games3d/MonsterTruck.tsx
    • Adjusted GRAVITY constant from 30 to 25 for better alignment with Scene3D physics.
    • Updated spin decay, coasting friction, mud pit, and boundary speed penalties to use frame-rate independent Math.pow calculations.
    • Replaced hard clamping for jump mode and radial boundaries with smooth push-back logic.
    • Reduced ground height lerp speed from 12 to 4 for gentler terrain following.
  • components/games3d/RaceGame3D.tsx
    • Removed truckPosition, truckRotation, and speed from React state to prevent re-renders.
    • Added raceTimeRef to track race time using a ref, alongside the state variable.
    • Introduced speedRef to store the truck's current speed as a ref.
    • Added hudPosition, hudRotationY, hudSpeed, and hudAiPositions state variables for throttled HUD updates.
    • Declared AI_COLORS as a stable ref to avoid re-creation.
    • Updated raceTime state to also sync raceTimeRef.current.
    • Removed raceTime from the position ranking useEffect dependencies.
    • Implemented a new useEffect to synchronize HUD-specific state variables from refs every 100ms.
    • Modified handlePositionUpdate to write directly to truckPositionRef, truckRotationRef, and speedRef, eliminating state updates for per-frame data.
    • Updated lap time recording to read from raceTimeRef.current.
    • Updated banana collection logic to use truckPositionRef.current.
    • Removed truckPosition from the banana collision useEffect dependencies.
    • Reset raceTimeRef, speedRef, hudSpeed, truckPositionRef, truckRotationRef, hudPosition, and hudRotationY in handleRestart.
    • Updated JumpArena and FollowCamera props to receive RefObjects for truck position, rotation, and speed.
    • Updated RaceHUD3D props to use the new throttled HUD state variables.
  • components/games3d/RaceHUD3D.tsx
    • Imported memo from 'react' for performance optimization.
    • Wrapped the Minimap sub-component in memo() to prevent unnecessary re-renders.
    • Wrapped the main RaceHUD3D component in memo() and updated its export.
  • components/games3d/Scene3D.tsx
    • Updated the Physics gravity setting from [0, -20, 0] to [0, -25, 0] to align with MonsterTruck gravity.
  • components/games3d/StadiumGame3D.tsx
    • Imported useRef for managing mutable values without triggering re-renders.
    • Extracted STADIUM_CAMERA_OFFSET and STADIUM_STAR_POSITIONS as module-level constants to prevent inline allocations.
    • Replaced truckPosition and truckRotation state with truckPositionRef and truckRotationRef.
    • Added speedRef to track the truck's speed in the stadium game.
    • Updated handlePositionUpdate to write to the new refs and accept a speed parameter.
    • Reset truckPositionRef, truckRotationRef, and speedRef in handleRestart.
    • Updated FollowCamera props to use the new refs and the STADIUM_CAMERA_OFFSET constant.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an excellent and comprehensive pull request that systematically addresses camera jitter and performance through a well-structured four-phase audit. The changes are of high quality, including the move from React state to refs for dynamic data, the implementation of advanced camera smoothing and physics stabilization techniques, and rendering optimizations with React.memo. The inclusion of tests for each phase is also a great practice. I have two main points of feedback: one to improve clarity and maintainability in the new camera logic, and another to address a potential bug where passing a ref's value instead of the ref object could undermine the performance gains. Overall, this is a fantastic improvement.

{gameMode === 'jump' && (
<>
<JumpArena isActive={true} truckPosition={truckPosition} />
<JumpArena isActive={true} truckPosition={truckPositionRef.current} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Here, you're passing the value truckPositionRef.current to JumpArena. This means JumpArena will only receive an updated position when RaceGame3D re-renders (currently every 100ms due to HUD updates). If JumpArena performs any per-frame logic based on the truck's position, this could lead to jittery or incorrect behavior, which is what this PR aims to fix.

To ensure JumpArena has access to the live, per-frame position, you should pass the ref object itself, similar to how you're doing it for FollowCamera. This will require updating JumpArena to accept a truckPositionRef prop and use .current inside its useFrame loop.

Suggested change
<JumpArena isActive={true} truckPosition={truckPositionRef.current} />
<JumpArena isActive={true} truckPositionRef={truckPositionRef} />

const intendedVelZ = (lerpedZ - prevCameraPos.current.z) / delta;

// Smooth camera velocity toward intended velocity (dampening factor)
const velDamp = 1 - Math.pow(0.001, delta); // ~6 frames to reach full velocity
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment here is a bit misleading. At 60fps, it takes about 6 frames to cover half the distance to the target velocity, not to reach "full velocity". Reaching ~99% of the target velocity would take closer to 40 frames.

Additionally, the value 0.001 is a magic number. For better readability and easier tuning, consider extracting it into a constant at the top of the file, e.g., const CAMERA_VELOCITY_DAMP_FACTOR = 0.001;.

Suggested change
const velDamp = 1 - Math.pow(0.001, delta); // ~6 frames to reach full velocity
const velDamp = 1 - Math.pow(0.001, delta); // ~6 frames to reach 50% of target velocity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant