Quellcode durchsuchen

tests: Add some callbacks/simulation tests for Bullet

Sam Edwards vor 6 Jahren
Ursprung
Commit
fd62daca7b

+ 0 - 0
tests/bullet/__init__.py


+ 71 - 0
tests/bullet/conftest.py

@@ -0,0 +1,71 @@
+import pytest
+
[email protected]
+def world():
+    bullet = pytest.importorskip("panda3d.bullet")
+    world = bullet.BulletWorld()
+
+    return world
+
+def simulate_until(world, cond, dt=1.0/60, limit=600):
+    for x in range(limit):
+        world.do_physics(dt)
+        if cond():
+            return True
+
+    return False
+
[email protected]
+def scene(world):
+    """
+    This test scene contains a "ramp" which slopes down at a 45-degree angle at
+    locations where X<0, and is flat at X>0. A bowling ball is placed above the
+    ramp at X=-5, Z=7. A stack of 2 very light boxes is placed at X=5, Z=0.
+    The world is given gravity.
+
+    When running the Bullet simulation, the ball should fall, travel down the
+    ramp, and hit the lower box with enough force to displace it and cause the
+    upper box to topple to the ground.
+    """
+
+    core = pytest.importorskip("panda3d.core")
+    bullet = pytest.importorskip("panda3d.bullet")
+
+    bodies = []
+
+    ramp = bullet.BulletRigidBodyNode('ramp')
+    ramp.add_shape(bullet.BulletPlaneShape(core.Vec4(0, 0, 1, 0)))
+    ramp.add_shape(bullet.BulletPlaneShape(core.Vec4(1, 0, 1, 0).normalized()))
+    bodies.append(ramp)
+
+    ball = bullet.BulletRigidBodyNode('ball')
+    ball.add_shape(bullet.BulletSphereShape(1))
+    ball.set_mass(100)
+    ball.set_transform(core.TransformState.make_pos(core.Point3(-5, 0, 7)))
+    bodies.append(ball)
+
+    lower_box = bullet.BulletRigidBodyNode('lower_box')
+    lower_box.add_shape(bullet.BulletBoxShape(core.Vec3(2.5)))
+    lower_box.set_mass(1)
+    lower_box.set_transform(core.TransformState.make_pos(core.Point3(5, 0, 2.5)))
+    bodies.append(lower_box)
+
+    upper_box = bullet.BulletRigidBodyNode('upper_box')
+    upper_box.add_shape(bullet.BulletBoxShape(core.Vec3(2.5)))
+    upper_box.set_mass(1)
+    upper_box.set_transform(core.TransformState.make_pos(core.Point3(5, 0, 7.5)))
+    bodies.append(upper_box)
+
+    world.set_gravity(core.Vec3(0, 0, -9.8))
+
+    scene = core.NodePath('scene')
+    for body in bodies:
+        scene.attach_new_node(body)
+        world.attach(body)
+
+    yield scene
+
+    scene.remove_node()
+
+    for body in bodies:
+        world.remove(body)

+ 79 - 0
tests/bullet/test_bullet_callbacks.py

@@ -0,0 +1,79 @@
+import pytest
+from .conftest import simulate_until
+
+# Skip these tests if we can't import bullet.
+bullet = pytest.importorskip("panda3d.bullet")
+from panda3d import core
+
+bullet_filter_algorithm = core.ConfigVariableString('bullet-filter-algorithm')
+
+
+def test_tick(world):
+    fired = []
+
+    def callback(cd):
+        fired.append(isinstance(cd, bullet.BulletTickCallbackData))
+
+    world.set_tick_callback(callback, False)
+
+    assert fired == []
+
+    world.do_physics(0.1)
+
+    assert fired == [True]
+
+    world.clear_tick_callback()
+
+    world.do_physics(0.1)
+
+    assert fired == [True]
+
[email protected](bullet_filter_algorithm != 'callback', reason='bullet-filter-algorithm not set to callback')
+def test_filter(world, scene):
+    # This is very similar to the basic physics test, but we're using
+    # a filter callback to prevent collisions between the lower box and ball.
+    # This should have the effect of the ball rolling to X>+10 without the
+    # upper box falling at all.
+
+    def callback(cd):
+        assert isinstance(cd, bullet.BulletFilterCallbackData)
+        if {cd.node_0.name, cd.node_1.name} == {'ball', 'lower_box'}:
+            # ball<->lower_box collisions are excluded
+            cd.collide = False
+        else:
+            # Everything else can collide
+            cd.collide = True
+
+    world.set_filter_callback(callback)
+
+    ball = scene.find('**/ball')
+    assert simulate_until(world, lambda: ball.get_x() > 10)
+
+    # The upper box shouldn't fall
+    upper_box = scene.find('**/upper_box')
+    assert not simulate_until(world, lambda: upper_box.get_z() < 5)
+
+def test_contact(world, scene):
+    # This just runs the basic physics test, but detects the toppling of the
+    # upper box by a contact between upper_box<->ramp
+
+    contacts = []
+
+    def callback(cd):
+        assert isinstance(cd, bullet.BulletContactCallbackData)
+        if {cd.node0.name, cd.node1.name} == {'upper_box', 'ramp'}:
+            if not contacts:
+                contacts.append(True)
+
+    world.set_contact_added_callback(callback)
+
+    ball = scene.find('**/ball')
+    ramp = scene.find('**/ramp')
+
+    ball.node().notify_collisions(True)
+    ramp.node().notify_collisions(True)
+
+    assert simulate_until(world, lambda: ball.get_x() > 0)
+
+    # Now we wait for the upper box to topple
+    assert simulate_until(world, lambda: bool(contacts))

+ 64 - 0
tests/bullet/test_bullet_simulation.py

@@ -0,0 +1,64 @@
+import pytest
+from .conftest import simulate_until
+
+# Skip these tests if we can't import bullet.
+bullet = pytest.importorskip("panda3d.bullet")
+from panda3d import core
+
+
+def test_basics(world, scene):
+    # N.B. see `scene` fixture's docstring in conftest.py to understand what's
+    # being simulated here
+
+    # Step forward until the ball crosses the threshold
+    ball = scene.find('**/ball')
+    assert simulate_until(world, lambda: ball.get_x() >= 0)
+
+    # Continue simulating until upper box falls
+    upper_box = scene.find('**/upper_box')
+    assert upper_box.get_z() > 5.0
+    assert simulate_until(world, lambda: upper_box.get_z() < 5.0)
+
+def test_restitution(world, scene):
+    ball = scene.find('**/ball')
+    scene.find('**/ramp').node().restitution = 1.0
+
+    for with_bounce in (False, True):
+        # Reset ball
+        ball.node().set_angular_velocity(core.Vec3(0))
+        ball.node().set_linear_velocity(core.Vec3(0))
+        ball.set_pos(-2, 0, 100)
+
+        ball.node().restitution = 1.0 * with_bounce
+
+        # Simulate until ball rolls/bounces across Y axis
+        assert simulate_until(world, lambda: ball.get_x() >= 0)
+
+        if with_bounce:
+            # The ball bounced across, so it should be off the ground a bit
+            assert ball.get_z() > 1.2
+        else:
+            # The ball rolled, so it should be on the ground
+            assert ball.get_z() < 1.2
+
+def test_friction(world, scene):
+    ball = scene.find('**/ball')
+
+    for with_friction in (False, True):
+        # Reset ball, give it a huge negative (CCW) spin about the X axis so
+        # it'll roll in +Y direction if there's any friction
+        ball.node().set_angular_velocity(core.Vec3(-1000,0,0))
+        ball.node().set_linear_velocity(core.Vec3(0))
+        ball.set_pos(-2, 0, 5)
+
+        ball.node().friction = 1.0 * with_friction
+
+        # Simulate until ball crosses Y axis
+        assert simulate_until(world, lambda: ball.get_x() >= 0)
+
+        if with_friction:
+            # The ball had friction, so should've gone off in the +Y direction
+            assert ball.get_y() > 1
+        else:
+            # No friction means the Y axis should be unaffected
+            assert abs(ball.get_y()) < 0.1