Collision Jitter from Thin Objects: Why Your Floor is Bouncing

Collision Jitter from Thin Objects: Why Your Floor is Bouncing

*Understanding how thin collision shapes cause physics jitter and best practices for stable floor placement*

Godot Version: 4.7

Project Type: 3D Space Ship Game with Physics Objects

Introduction

Have you ever noticed objects jittering or bouncing when they should be sitting still on a floor? Or a character that seems to vibrate when standing on a platform? This frustrating issue is often caused by collision shapes that are too thin.

In this post, we’ll explore why thin collision objects cause jitter, how to identify the problem, and best practices for creating stable floors and platforms in Godot 4.

The Problem: Jitter and Bouncing

Symptoms

You might experience:

  • Micro-bouncing: Objects appear to vibrate or bounce slightly when at rest
  • Jittery movement: Objects slide or shake when they should be stable
  • Penetration issues: Objects sink into or pop out of floors
  • Unstable physics: RigidBody3D objects behave erratically on surfaces
  • CharacterBody3D issues: Characters bounce or jitter when standing still
  • Real-World Example

    In our space ship game, we initially created floors with very thin collision shapes (0.05-0.1 units thick). When crates or the player stood on these floors, they would:

  • Slightly bounce up and down
  • Jitter when the ship moved
  • Occasionally sink into the floor
  • Feel “unstable” during physics interactions
  • Why Thin Collision Shapes Cause Jitter

    1. Physics Solver Precision

    Godot’s physics engine (GodotPhysics) uses iterative solvers to resolve collisions. When a collision shape is very thin:

  • Penetration Depth: The solver needs to push objects out of penetration. With thin shapes, the penetration depth can be a significant portion of the shape’s thickness.
  • Resolution Iterations: The solver may need multiple iterations to resolve, causing visible jitter.
  • Numerical Precision: Floating-point errors become more significant relative to the shape size.
  • Example:

    Thin floor: 0.05 units thick
    Object penetration: 0.01 units
    Penetration ratio: 20% of shape thickness ?
    
    Thick floor: 0.2 units thick  
    Object penetration: 0.01 units
    Penetration ratio: 5% of shape thickness ?

    2. Contact Point Generation

    Thin collision shapes generate fewer or less stable contact points:

  • Edge Contacts: Objects often contact the edges of thin shapes, which are less stable than face contacts.
  • Contact Normal Issues: The contact normal (direction of collision) can flip or change rapidly with thin shapes.
  • Resting Contact: The physics engine struggles to maintain stable “resting” contact on thin surfaces.
  • 3. Restitution and Damping

    When objects collide with thin shapes:

  • Multiple Contacts: An object might contact both the top and bottom of a thin shape simultaneously.
  • Bounce Amplification: Small bounces get amplified because the collision resolution is less stable.
  • Damping Inefficiency: Linear/angular damping has less effect when contact points are unstable.
  • 4. Sub-stepping Issues

    Godot’s physics sub-stepping (used for fast-moving objects) can cause issues with thin shapes:

  • Objects may “skip through” thin shapes between sub-steps.
  • The solver may over-correct, causing jitter.
  • The Physics Behind It

    Penetration Resolution

    When two objects collide, the physics engine:

    1. Detects penetration (overlap)

    2. Calculates separation vector

    3. Applies impulses to separate objects

    With thin shapes, step 2 becomes problematic:

    # Simplified penetration resolution
    var penetration_depth = collision_shape.thickness - object_radius
    var separation_force = penetration_depth * solver_strength
    
    # If thickness is 0.05 and penetration is 0.01:
    # separation_force = 0.04 * strength (small margin for error)
    
    # If thickness is 0.2 and penetration is 0.01:
    # separation_force = 0.19 * strength (large margin, stable)

    Contact Manifold Stability

    A contact manifold is the set of contact points between two objects. Thin shapes create:

  • Unstable manifolds: Contact points change rapidly
  • Edge cases: Objects contact edges more than faces
  • Flickering contacts: Contacts appear/disappear between frames
  • Best Practices for Floor Placement

    1. Minimum Thickness Guidelines

    Rule of Thumb: Floor collision shapes should be at least 0.1-0.2 units thick (in world units).

    For different object sizes:

  • Small objects (player, crates): 0.1-0.2 units minimum
  • Medium objects (vehicles): 0.2-0.3 units minimum
  • Large objects (ships): 0.3-0.5 units minimum
  • Our Codebase Examples:

    “`18:19:main.tscn

    [sub_resource type=”BoxShape3D” id=”Shape_Ground”]

    size = Vector3(50, 0.1, 50)

    This is at the minimum threshold. For better stability, we could use 0.15-0.2:

    gdscript

    Better: More stable

    size = Vector3(50, 0.15, 50) # 50% thicker = more stable

    Best: Very stable

    size = Vector3(50, 0.2, 50) # 100% thicker = most stable

    ### 2. Positioning the Collision Shape
    
    **Key Principle**: Position the collision shape so objects contact the **top face**, not the edges.
    
    **Bad Positioning:**

    gdscript

    Collision shape centered on visual mesh

    Objects contact edges, causing instability

    CollisionShape3D.position = Vector3.ZERO # ?

    **Good Positioning:**

    gdscript

    Collision shape positioned so top face aligns with visual floor

    Objects contact the stable top face

    CollisionShape3D.position = Vector3(0, -0.05, 0) # ?

    **Our Implementation:**

    22:24:ships/interiors/ship_deck.tscn

    [node name=”FloorCollision” type=”CollisionShape3D” parent=”.”]

    transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0)

    shape = SubResource(“BoxShape3D_Floor”)

    The collision shape is positioned at `y = -0.1`, which means:
    - Visual floor is at `y = 0`
    - Collision shape top face is at `y = 0` (since shape is 0.2 thick, centered at -0.1)
    - Objects contact the stable top face ?
    
    ### 3. Shape Type Selection
    
    **For Floors, Use BoxShape3D:**

    gdscript

    ? Good: BoxShape3D for floors

    var floor_shape = BoxShape3D.new()

    floor_shape.size = Vector3(width, 0.2, depth) # Thick enough

    ? Avoid: PlaneMesh for collision (no thickness)

    PlaneMesh is visual only, not a collision shape

    ?? Use with caution: ConcavePolygonShape3D

    Only for StaticBody3D, can be less stable than BoxShape3D

    ### 4. StaticBody3D vs RigidBody3D for Floors
    
    **Use StaticBody3D for floors:**

    gdscript

    ? Good: StaticBody3D (immovable, optimized)

    StaticBody3D

    ??? CollisionShape3D

    ? Avoid: RigidBody3D for floors (unless floor moves)

    RigidBody3D adds unnecessary physics calculations

    **Our Implementation:**

    43:50:main.tscn

    [node name=”Ground” type=”StaticBody3D” parent=”.”]

    [node name=”MeshInstance3D” type=”MeshInstance3D” parent=”Ground”]

    mesh = SubResource(“Mesh_Ground”)

    [node name=”CollisionShape3D” type=”CollisionShape3D” parent=”Ground”]

    transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)

    shape = SubResource(“Shape_Ground”)

    ### 5. Collision Layers and Masks
    
    Ensure proper collision layer setup:

    gdscript

    Floor (StaticBody3D)

    collision_layer = 1 # World layer

    Objects that should collide with floor

    collision_mask = 1 # Include world layer in their mask

    ## Complete Floor Setup Example
    
    Here's a complete, stable floor setup:

    gdscript

    Floor Scene Structure

    StaticBody3D (name: “Floor”)

    ??? MeshInstance3D (visual floor)

    ? ??? PlaneMesh (size: Vector2(10, 10))

    ? ??? position: Vector3(0, 0, 0)

    ?

    ??? CollisionShape3D (collision)

    ??? BoxShape3D (size: Vector3(10, 0.2, 10))

    ??? position: Vector3(0, -0.1, 0) # Top face at y=0

    **Key Points:**
    1. **Thickness**: 0.2 units (stable)
    2. **Position**: Collision shape positioned so top face aligns with visual floor
    3. **Type**: StaticBody3D (optimized for immovable objects)
    4. **Shape**: BoxShape3D (stable, predictable)
    
    ## Debugging Jitter Issues
    
    ### 1. Visualize Collision Shapes
    
    Enable collision shape visualization in the editor:
    - **View ? Debug Settings ? Visible Collision Shapes**
    - Or use `CollisionShape3D.debug_color` in code
    
    ### 2. Check Shape Thickness

    gdscript

    func _ready() -> void:

    var collision = $CollisionShape3D

    if collision.shape is BoxShape3D:

    var box = collision.shape as BoxShape3D

    print(“Floor thickness: “, box.size.y)

    if box.size.y < 0.1:

    print(“?? WARNING: Floor too thin! Minimum recommended: 0.1”)

    ### 3. Monitor Physics Contacts
    
    Add debug output to see contact stability:

    gdscript

    func _on_body_entered(body: Node3D) -> void:

    if body is RigidBody3D:

    print(“Object on floor: “, body.name)

    print(” Position: “, body.global_position)

    print(” Velocity: “, body.linear_velocity)

    # Check for jitter (rapid velocity changes)

    if body.linear_velocity.length() > 0.1 and body.sleeping == false:

    print(” ?? Possible jitter detected”)

    ### 4. Test with Different Thicknesses
    
    Create a test scene with multiple floors of different thicknesses:

    gdscript

    Test different thicknesses

    var thicknesses = [0.05, 0.1, 0.15, 0.2, 0.3]

    for thickness in thicknesses:

    var floor = create_test_floor(thickness)

    # Place object on floor and observe stability

    ## Common Mistakes and Fixes
    
    ### Mistake 1: Using PlaneMesh for Collision

    gdscript

    ? Wrong: PlaneMesh has no thickness

    var plane = PlaneMesh.new()

    plane.size = Vector2(10, 10)

    This creates a visual mesh, not a collision shape

    **Fix:**

    gdscript

    ? Correct: Use BoxShape3D

    var box = BoxShape3D.new()

    box.size = Vector3(10, 0.2, 10) # Has thickness

    ### Mistake 2: Centering Thin Shape on Visual

    gdscript

    ? Wrong: Thin shape centered, objects contact edges

    CollisionShape3D.position = Vector3.ZERO

    shape.size = Vector3(10, 0.05, 10) # Too thin, centered

    **Fix:**

    gdscript

    ? Correct: Thick shape, positioned for top-face contact

    CollisionShape3D.position = Vector3(0, -0.1, 0)

    shape.size = Vector3(10, 0.2, 10) # Thick enough

    ### Mistake 3: Using RigidBody3D for Static Floors

    gdscript

    ? Wrong: Unnecessary physics calculations

    RigidBody3D (mass: 0, freeze: true) # Still processes physics

    **Fix:**

    gdscript

    ? Correct: Use StaticBody3D

    StaticBody3D # Optimized, no physics processing

    ### Mistake 4: Non-Uniform Scaling

    gdscript

    ?? Caution: Non-uniform scale can cause issues

    floor.scale = Vector3(1, 0.5, 1) # Makes shape thinner!

    **Fix:**

    gdscript

    ? Better: Scale the shape resource, not the node

    box.size = Vector3(10 * scale.x, 0.2, 10 * scale.z)

    Keep thickness constant

    ## Performance Considerations
    
    ### Thickness vs Performance
    
    - **Thicker shapes**: Slightly more collision calculations, but more stable
    - **Thinner shapes**: Fewer calculations, but unstable and may require more solver iterations
    
    **Recommendation**: Use the minimum stable thickness (0.1-0.2) for best balance.
    
    ### StaticBody3D Optimization
    
    StaticBody3D is optimized for immovable objects:
    - No velocity calculations
    - No force integration
    - Optimized collision detection
    - Can be baked into collision space for even better performance
    
    ## Real-World Example: Fixing Our Floor
    
    ### Before (Problematic)

    6:7:ships/interiors/simple_floor.tscn

    [sub_resource type=”BoxShape3D” id=”BoxShape3D_1″]

    size = Vector3(4, 0.1, 2)

    **Issues:**
    - Thickness of 0.1 is at minimum threshold
    - Objects might jitter on fast-moving ships
    - Contact points less stable
    
    ### After (Improved)

    gdscript

    Improved version

    [sub_resource type=”BoxShape3D” id=”BoxShape3D_1″]

    size = Vector3(4, 0.2, 2) # Doubled thickness

    Position adjusted to maintain top-face contact

    transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0)

    **Benefits:**
    - More stable contact points
    - Less jitter on moving ships
    - Better penetration resolution
    - Smoother physics interactions
    
    ## Advanced: Custom Floor Solutions
    
    ### For Moving Floors (Ships)
    
    When floors are on moving RigidBody3D (like ships), consider:
    
    1. **Thicker collision shapes** (0.2-0.3) for extra stability
    2. **Inertial dampeners** (see [blog_inertial_dampeners.md](blog_inertial_dampeners.md)) to prevent sliding
    3. **Continuous collision detection** on objects:

    gdscript

    # On RigidBody3D objects that interact with floors

    continuous_cd = RigidBody3D.CCD_MODE_CAST_RAY

    “`

    For Sloped Floors

    For ramps or slopes:

  • Use thicker shapes (0.2-0.3) to handle edge contacts
  • Consider ConvexPolygonShape3D for complex slopes
  • Position collision shape to match visual slope exactly
  • For Multi-Level Floors

    When stacking floors:

  • Ensure adequate spacing between floors (at least 0.3 units)
  • Use separate StaticBody3D nodes for each floor
  • Check collision layers to prevent unwanted interactions
  • Testing Your Floor

    Stability Test

    # Place this script on a test RigidBody3D
    extends RigidBody3D
    
    var position_samples: Array[Vector3] = []
    var jitter_threshold: float = 0.01  # 1cm movement = jitter
    
    func _physics_process(_delta: float) -> void:
        position_samples.append(global_position)
        if position_samples.size() > 60:  # 1 second at 60fps
            position_samples.pop_front()
            
            # Check for jitter (rapid position changes)
            var variance = calculate_variance(position_samples)
            if variance > jitter_threshold:
                print("?? JITTER DETECTED: Variance = ", variance)
                print("  Consider increasing floor thickness")
    
    func calculate_variance(samples: Array[Vector3]) -> float:
        if samples.size() < 2:
            return 0.0
        
        var mean = Vector3.ZERO
        for pos in samples:
            mean += pos
        mean /= samples.size()
        
        var variance = 0.0
        for pos in samples:
            variance += (pos - mean).length_squared()
        variance /= samples.size()
        
        return sqrt(variance)

    Summary: Quick Reference

    ? Do’s

  • Use 0.1-0.2 unit thickness minimum for floor collision shapes
  • Position collision shape so top face aligns with visual floor
  • Use StaticBody3D for immovable floors
  • Use BoxShape3D for simple floors
  • Test with actual objects to verify stability
  • ? Don’ts

  • Don’t use PlaneMesh for collision (visual only)
  • Don’t center thin shapes on visual mesh
  • Don’t use RigidBody3D for static floors
  • Don’t scale collision nodes non-uniformly
  • Don’t go below 0.1 units thickness
  • ?? Thickness Guidelines

    Object Size Minimum Thickness Recommended Thickness
    Small (player, crate) 0.1 0.15-0.2
    Medium (vehicle) 0.15 0.2-0.3
    Large (ship deck) 0.2 0.3-0.5

    Conclusion

    Thin collision shapes are a common source of physics jitter in Godot 4. By understanding why they cause problems and following best practices, you can create stable, smooth physics interactions.

    Key Takeaways:

    1. Thickness matters: Use at least 0.1-0.2 units for floors

    2. Position matters: Align collision top face with visual floor

    3. Type matters: Use StaticBody3D + BoxShape3D for floors

    4. Test matters: Verify stability with actual game objects

    The best floors are the ones players never think about-they just work smoothly, without jitter or bouncing. Follow these guidelines, and your floors will be rock-solid.


    Want to see it in action? Check out our floor implementations:

  • main.tscn – Ground floor setup
  • ships/interiors/ship_deck.tscn – Ship deck floor
  • ships/interiors/simple_floor.tscn – Simple floor template
  • Related Systems:

  • Inertial Dampeners – Keeping objects stable on moving ships
  • Physics Mass System – Mass values for realistic physics
  • Crate Carrying System – Physics object interactions

  • Posted

    in

    by

    Tags:

    Comments

    Leave a Reply

    Your email address will not be published. Required fields are marked *