Quick answer: Set bounce on a PhysicsMaterial assigned to physics_material_override on your RigidBody, not on the CollisionShape. Both colliding bodies contribute — if the floor has bounce 0, your ball’s bounce alone may not produce a visible effect. Set bounce on both surfaces or use absorbent=false.

Here is how to fix Godot PhysicsMaterial bounce not working. You drop a ball RigidBody3D onto a floor StaticBody3D. You set bounce to 1.0 on the ball expecting a perfect rebound. The ball hits the floor and stops dead. No bounce, no rebound, just a thud. The bounce value is clearly set, but the physics engine ignores it. The problem is almost always where and how the material is assigned.

The Symptom

A RigidBody with a non-zero bounce value collides with a surface and does not rebound. The body either stops immediately on contact or slides along the surface with zero vertical velocity after impact. Setting bounce to higher values (even above 1.0) makes no difference.

What Causes This

PhysicsMaterial not assigned to the body. In Godot 4, bounce is a property of PhysicsMaterial, which must be set on physics_material_override on the RigidBody node itself. Setting bounce-related properties on the CollisionShape or as a standalone resource not connected to a body does nothing.

Only one surface has bounce. The final bounce value is computed from both surfaces in contact. Godot uses the maximum of the two bounce values by default. However, if either body has absorbent set to true on its PhysicsMaterial, the bounce is forced to zero regardless of the other body’s value.

Friction killing velocity before bounce. High friction values can absorb enough energy on the contact frame that the resulting bounce velocity is negligible. This is especially noticeable with small bodies or low-speed impacts.

Continuous collision detection interfering. CCD mode (continuous_cd on RigidBody3D) can sometimes resolve penetration in ways that zero out the rebound velocity. This is rare but happens with thin surfaces.

The Fix

Step 1: Assign PhysicsMaterial correctly.

# In the editor:
# 1. Select your RigidBody3D node
# 2. Inspector > physics_material_override > New PhysicsMaterial
# 3. Expand the material > set bounce = 0.8

# Or via code:
extends RigidBody3D

func _ready():
    var mat = PhysicsMaterial.new()
    mat.bounce = 0.8
    mat.absorbent = false
    physics_material_override = mat

The material must be on physics_material_override, not anywhere else. This applies to both RigidBody3D and RigidBody2D.

Step 2: Set bounce on both colliding surfaces. For reliable bouncing, assign PhysicsMaterial to the floor as well:

# floor_setup.gd - attached to StaticBody3D
extends StaticBody3D

func _ready():
    var mat = PhysicsMaterial.new()
    mat.bounce = 0.5
    mat.friction = 0.3
    mat.absorbent = false
    physics_material_override = mat

With ball bounce 0.8 and floor bounce 0.5, Godot takes the maximum (0.8) for the collision response. If you want the average or minimum, you need a custom approach since Godot 4 uses max by default.

Step 3: Disable absorbent flag. The absorbent property on PhysicsMaterial forces bounce to zero for that contact. Ensure both materials have absorbent = false:

var mat = PhysicsMaterial.new()
mat.bounce = 1.0
mat.absorbent = false  # Critical - true kills all bounce
physics_material_override = mat

Step 4: Reduce friction for clean bounces. If the body is rotating on impact, friction converts kinetic energy into angular velocity rather than rebound. Lower friction for bouncy objects:

var mat = PhysicsMaterial.new()
mat.bounce = 0.9
mat.friction = 0.1  # Low friction preserves bounce energy
mat.absorbent = false
physics_material_override = mat

Testing Bounce Properly

Drop the body from a known height and measure the rebound height. A bounce of 1.0 should return to original height (perfect elastic collision). Use a simple script to verify:

extends RigidBody3D

var max_height: float = 0.0
var tracking: bool = false

func _physics_process(_delta):
    if tracking and linear_velocity.y > 0:
        max_height = max(max_height, global_position.y)
    elif tracking and linear_velocity.y <= 0 and max_height > 0:
        print("Rebound height: ", max_height)
        tracking = false

Understanding the issue

Game physics is a contract between authoring (the body, mass, collision shapes you set) and the solver (how the engine integrates them per tick). Bugs at this boundary usually surface as 'the values look right but the behavior is wrong' - a sign that one side of the contract isn't honoring the other.

The specific bug described above is the kind that surfaces during integration rather than unit testing. It depends on a combination of factors: the asset configuration, the runtime state, the platform's specific behavior. In isolation, each piece looks correct; in combination, the bug emerges. This is why thorough integration testing - playing the actual game in realistic conditions - catches things that automated tests miss.

Why this happens

This bug class disproportionately affects late-stage development. The work to surface it is interactive testing in realistic conditions, which only really happens after the gameplay is in place and assets are populated. Catching it early requires deliberate testing of conditions that look unimportant.

At the engine level, the behavior comes from a deliberate design decision in Godot. The engine team chose a particular trade-off - usually performance versus convenience, or generality versus specificity - and that trade-off has consequences when you push against it. Understanding the trade-off is what turns 'this bug is mysterious' into 'this bug is the expected consequence of this design'.

Verifying the fix

Verifying this fix in isolation is straightforward: reproduce the bug, apply the change, confirm the bug no longer reproduces. The harder verification is regression - did this fix introduce a new bug elsewhere? Run your standard regression suite, plus any tests that exercise the same code path with different inputs.

Reproducibility is the prerequisite for verification. If you can't reliably reproduce the bug pre-fix, you can't reliably verify it post-fix. Spend time getting a clean reproduction before you write any fix code. The fix is fast once you understand the reproduction; the reproduction is the slow part.

Variations to watch for

There's almost always a less obvious case where the same problem applies. The reported case is the one a player hit; the related cases hide because they're rarer or affect fewer players. After fixing the reported case, search the codebase for the pattern - one fix often unlocks several.

Adjacent bugs often share a root cause. After fixing the case you've found, spend an hour searching the codebase for similar patterns. What's the same call with different arguments? The same data flow with a different entity type? The same lifecycle issue in a sibling system? Each match is a candidate for the same fix, or a related fix that prevents future bugs of the same class.

In production

For shipping titles with a long support window, watch for this issue resurfacing after dependency updates. Engine upgrades, driver updates, OS releases - each one can resurface a bug class you thought you'd fixed because the underlying behavior changed slightly. Regression tests catch the obvious ones; player reports catch the rest.

When triaging a similar issue in production, prioritize gathering data over hypothesizing causes. A player report describes a symptom; what you need is a build SHA, a session timestamp, and ideally a screen recording or session replay. With those, the bug becomes tractable. Without them, you're guessing at hypothetical reproductions that may not match what the player actually hit.

Performance considerations

Performance implications matter when this bug class scales with player count or asset count. A bug that fires once per session is annoying; a bug that fires once per frame compounds. After fixing, profile the affected code path under realistic load. The fix that's correct for one entity may be too slow for ten thousand.

Diagnostic approach

The diagnostic tools available depend on your engine and platform. Use the engine's native profilers and debug overlays before reaching for external tools. The native tools have context that external tools lack - they know which subsystem owns the code, which assets are loaded, and what state the engine is in.

For Godot-specific diagnostics, the editor's profiler is the canonical starting point. Capture a representative frame with the symptom present; compare against a frame without the symptom; the diff often points directly at the cause. If the symptom is non-deterministic, capture multiple frames and look for the pattern - the cause is usually a state transition or a specific input value rather than a continuous effect.

Tooling and ecosystem

The tooling around this bug class matters as much as the fix itself. Good logging, accessible profilers, and clear error messages turn 30-minute investigations into 5-minute ones. If your project doesn't have visibility into this code path, the first fix should add the visibility - the second fix uses it.

Within Godot, the relevant diagnostic surfaces include the standard frame debugger, memory profiler, and engine-specific debug overlays. Each one shows a different facet of what's happening. The frame debugger reveals draw call ordering and state transitions; the memory profiler shows allocation patterns; the debug overlay reveals per-system state. Bugs that resist one tool usually surrender to another - the trick is knowing which tool to reach for first.

Edge cases and pitfalls

Edge cases for this class of issue often involve specific timing: the first frame after a state change, the last frame before a transition, frames where multiple subsystems update simultaneously. Reproducing these reliably is part of what makes the bug class hard to test.

When writing a regression test for this fix, focus on the boundary conditions that surfaced the original bug. Tests that exercise the happy path catch obvious regressions; tests that exercise the boundary catch the subtler regressions that look like new bugs but are really the original returning. The latter are the tests that earn their keep over the long life of the project.

Team communication

Document the fix and its rationale in the commit message or attached engineering doc. Future engineers will encounter related issues; the rationale tells them whether your fix is reusable or specific to the case at hand. Without rationale, the fix gets reverted or copied incorrectly.

If this fix touches a system several engineers work in, a short writeup in the team's engineering channel helps. Not a full design doc - a paragraph explaining what was wrong, what's fixed, and what to watch for. Future engineers encountering similar symptoms will search for the fix; making it findable is a small investment that pays back later.

“Bounce is a two-body property. Both surfaces contribute. If your floor absorbs everything, no amount of bounce on the ball will help.”

Related Issues

For collision shape issues, see CollisionShape Disabled Not Re-Enabling. For physics body movement on slopes, see CharacterBody2D Floor Snap.

PhysicsMaterial on the body, not the shape. Both surfaces matter. Disable absorbent. Bounce works.