mirror of
https://github.com/panda3d/panda3d.git
synced 2026-02-21 22:59:11 -06:00
tests: Add some callbacks/simulation tests for Bullet
This commit is contained in:
0
tests/bullet/__init__.py
Normal file
0
tests/bullet/__init__.py
Normal file
71
tests/bullet/conftest.py
Normal file
71
tests/bullet/conftest.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
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
|
||||
|
||||
@pytest.fixture
|
||||
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
tests/bullet/test_bullet_callbacks.py
Normal file
79
tests/bullet/test_bullet_callbacks.py
Normal file
@@ -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]
|
||||
|
||||
@pytest.mark.skipif(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
tests/bullet/test_bullet_simulation.py
Normal file
64
tests/bullet/test_bullet_simulation.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user