def step_physics(task): dt = globalClock.getDt() physics.doPhysics(dt) return task.cont s.taskMgr.add(step_physics, 'physics simulation') # A physical object in the simulation node = BulletRigidBodyNode('Box') node.setMass(1.0) node.addShape(BulletSphereShape(1)) physics.attachRigidBody(node) # Attaching the physical object in the scene graph and adding # a visible model to it np = s.render.attachNewNode(node) np.setPos(0, 0, 0) m = loader.loadModel("models/smiley") m.reparentTo(np) # Let's actually see what's happening base.cam.setPos(0, -10, 0) base.cam.lookAt(0, 0, 0) # Give the object a nudge and run the program node.apply_impulse(Vec3(0, 1, 0), Point3(0, 0, 0)) s.run()
def step_physics(task): dt = globalClock.getDt() physics.doPhysics(dt) return task.cont s.taskMgr.add(step_physics, 'physics simulation') # A physical object in the simulation node = BulletRigidBodyNode('Box') node.setMass(1.0) node.addShape(BulletSphereShape(1)) # Attaching the physical object in the scene graph and adding # a visible model to it np = s.render.attachNewNode(node) np.set_pos(0, 0, 0) np.set_hpr(45, 0, 45) m = loader.loadModel("models/smiley") m.reparentTo(np) physics.attachRigidBody(node) # Let's actually see what's happening base.cam.setPos(0, -10, 0) base.cam.lookAt(0, 0, 0) # Give the object a nudge and run the program # the impulse vector is in world space, the position at which it is # applied is in object space. node.apply_impulse(Vec3(0, 0, 1), Point3(1, 0, 0)) node.apply_impulse(Vec3(0, 0, -1), Point3(-1, 0, 0)) s.run()
physics = BulletWorld() physics.setGravity(Vec3(0, 0, 0)) def step_physics(task): dt = globalClock.getDt() physics.doPhysics(dt) return task.cont s.taskMgr.add(step_physics, 'physics simulation') # A physical object in the simulation node = BulletRigidBodyNode('Box') node.setMass(1.0) node.addShape(BulletSphereShape(1)) physics.attachRigidBody(node) # Attaching the physical object in the scene graph and adding # a visible model to it np = s.render.attachNewNode(node) np.setPos(0, 0, 0) m = loader.loadModel("models/smiley") m.reparentTo(np) # Let's actually see what's happening base.cam.setPos(0, -10, 0) base.cam.lookAt(0, 0, 0) # Give the object a nudge and run the program node.apply_impulse(Vec3(0,1,0), Point3(0,0,0)) s.run()
class Vehicle: def __init__(self, app, model_name): model_file_name = 'assets/cars/{}/{}.bam'.format( model_name, model_name) self.app = app def animation_path(model, animation): base_path = 'assets/cars/animations/{}/{}-{}.bam' return base_path.format(model, model, animation) self.model = Actor( model_file_name, { # AIRBRAKE: 'assets/cars/animations/{}-{}.bam'.format(model_name, AIRBRAKE), # AIRBRAKE: animation_path(model_name, AIRBRAKE), # STABILIZER_FINS: animation_path(model_name, STABILIZER_FINS), }) self.model.enableBlend() self.model.setControlEffect(AIRBRAKE, 1) self.model.setControlEffect(STABILIZER_FINS, 1) # FIXME: This code fails due to a bug in Actor # airbrake_joints = [joint.name # for joint in self.model.getJoints() # if joint.name.startswith(AIRBRAKE) # ] # self.model.makeSubpart(AIRBRAKE, airbrake_joints) # stabilizer_joints = [joint.name # for joint in self.model.getJoints() # if joint.name.startswith(STABILIZER_FINS) # ] # self.model.makeSubpart(STABILIZER_FINS, stabilizer_joints) puppet = self.app.loader.load_model(model_file_name) puppet.find("**/armature").hide() puppet.reparentTo(self.model) # Get the vehicle data self.vehicle_data = VehicleData(puppet, model_name, 'cars') # Configure the physics node self.physics_node = BulletRigidBodyNode('vehicle') self.physics_node.set_friction(self.vehicle_data.friction) self.physics_node.set_linear_sleep_threshold(0) self.physics_node.set_angular_sleep_threshold(0) self.physics_node.setCcdMotionThreshold(1e-7) self.physics_node.setCcdSweptSphereRadius(0.5) self.physics_node.setMass(self.vehicle_data.mass) shape = BulletConvexHullShape() for geom_node in self.model.find_all_matches("**/+GeomNode"): for geom in geom_node.node().get_geoms(): vertices = GeomVertexReader(geom.get_vertex_data(), 'vertex') while not vertices.is_at_end(): v_geom = vertices.getData3f() v_model = self.model.get_relative_point(geom_node, v_geom) shape.add_point(v_model) self.physics_node.add_shape(shape) self.vehicle = NodePath(self.physics_node) self.vehicle.set_collide_mask(CM_VEHICLE | CM_COLLIDE) self.model.reparent_to(self.vehicle) # Navigational aids self.target_node = self.app.loader.load_model('models/zup-axis') self.target_node.reparent_to(self.model) self.target_node.set_scale(1) self.target_node.set_render_mode_wireframe() self.target_node.hide() self.delta_node = self.app.loader.load_model('models/smiley') self.delta_node.set_pos(1, 10, 1) self.delta_node.reparent_to(base.cam) self.delta_node.hide() self.airbrake_state = 0.0 self.airbrake_factor = 0.5 self.airbrake_speed = 1 / self.vehicle_data.airbrake_duration self.stabilizer_fins_state = 0.0 self.stabilizer_fins_speed = 1 / self.vehicle_data.stabilizer_fins_duration self.centroid = base.loader.load_model('models/smiley') self.centroid.reparent_to(self.vehicle) self.centroid.hide() # Gyro sound sound_file = 'assets/cars/{}/{}.wav'.format( model_name, GYROSCOPE_SOUND, ) sound = base.audio3d.load_sfx(sound_file) self.model.set_python_tag(GYROSCOPE_SOUND, sound) base.audio3d.attach_sound_to_object(sound, self.model) sound.set_volume(0) sound.set_play_rate(0) sound.set_loop(True) sound.play() # Thruster limiting self.thruster_state = 0.0 self.thruster_heat = 0.0 for repulsor in self.vehicle_data.repulsor_nodes: self.add_repulsor(repulsor, model_name) for thruster in self.vehicle_data.thruster_nodes: self.add_thruster(thruster, model_name) # ECU data storage from frame to frame self.last_flight_height = None # FIXME: Move into a default controller self.inputs = { # Repulsors REPULSOR_ACTIVATION: 0.0, ACCELERATE: 0.0, TURN: 0.0, STRAFE: 0.0, HOVER: 0.0, FULL_REPULSORS: False, # Gyro ACTIVE_STABILIZATION_ON_GROUND: PASSIVE, ACTIVE_STABILIZATION_CUTOFF_ANGLE: PASSIVE, ACTIVE_STABILIZATION_IN_AIR: PASSIVE, TARGET_ORIENTATION: Vec3(0, 0, 0), # Thrust THRUST: 0.0, # Air foils AIRBRAKE: 0.0, STABILIZER_FINS: 0.0, } self.sensors = {} self.commands = {} def np(self): return self.vehicle def place(self, spawn_point): # FIXME: Pass a root node to __init__ instead self.vehicle.reparent_to(self.app.environment.model) connector = self.model.find("**/" + SPAWN_POINT_CONNECTOR) self.vehicle.set_hpr(-connector.get_hpr(spawn_point)) self.vehicle.set_pos(-connector.get_pos(spawn_point)) self.app.environment.add_physics_node(self.physics_node) def set_inputs(self, inputs): self.inputs = inputs def add_repulsor(self, node, model_name): ground_contact = self.app.loader.load_model( "assets/effect/repulsorhit.egg") ground_contact.set_scale(1) ground_contact.reparent_to(self.app.render) node.set_python_tag('ray_end', ground_contact) sound_file = 'assets/cars/{}/{}.wav'.format( model_name, REPULSOR_SOUND, ) sound = base.audio3d.load_sfx(sound_file) node.set_python_tag(REPULSOR_SOUND, sound) base.audio3d.attach_sound_to_object(sound, node) sound.set_volume(0) sound.set_play_rate(0) sound.set_loop(True) sound.play() def add_thruster(self, node, model_name): node.set_python_tag(THRUSTER_POWER, 0.0) node.set_python_tag(THRUSTER_OVERHEATED, False) # Basic jet sound sound_file = 'assets/cars/{}/{}.wav'.format( model_name, THRUSTER_SOUND, ) sound = base.audio3d.load_sfx(sound_file) node.set_python_tag(THRUSTER_SOUND, sound) base.audio3d.attach_sound_to_object(sound, node) sound.set_volume(0) sound.set_play_rate(0) sound.set_loop(True) sound.play() # Overheating sound sound_file = 'assets/cars/{}/{}.wav'.format( model_name, THRUSTER_OVERHEAT_SOUND, ) sound = base.audio3d.load_sfx(sound_file) node.set_python_tag(THRUSTER_OVERHEAT_SOUND, sound) base.audio3d.attach_sound_to_object(sound, node) def game_loop(self): self.gather_sensors() self.ecu() self.apply_air_drag() self.apply_repulsors() self.apply_gyroscope() self.apply_thrusters() self.apply_airbrake() self.apply_stabilizer_fins() def gather_sensors(self): # Gather data repulsor ray collisions with ground repulsor_data = [] for node in self.vehicle_data.repulsor_nodes: data = RepulsorData() repulsor_data.append(data) max_distance = node.get_python_tag(ACTIVATION_DISTANCE) data.position = node.get_pos(self.vehicle) data.direction = self.app.render.get_relative_vector( node, Vec3(0, 0, -max_distance), ) # FIXME: `self.app.environment.physics_world` is ugly. feeler = self.app.environment.physics_world.ray_test_closest( base.render.get_relative_point(self.vehicle, data.position), base.render.get_relative_point(self.vehicle, data.position + data.direction), CM_TERRAIN, ) if feeler.has_hit(): data.active = True data.fraction = feeler.get_hit_fraction() data.contact = self.vehicle.get_relative_point( self.app.render, feeler.get_hit_pos(), ) else: data.active = False data.fraction = 1.0 # Find the local ground's normal vector local_up = False contacts = [ data.contact for data in repulsor_data if data.active and self.inputs[REPULSOR_ACTIVATION] ] if len(contacts) > 2: local_up = True target_mode = self.inputs[ACTIVE_STABILIZATION_ON_GROUND] # We can calculate a local up as the smallest base vector of the # point cloud of contacts. centroid = reduce(lambda a, b: a + b, contacts) / len(contacts) covariance = [ [c.x, c.y, c.z] for c in [contact - centroid for contact in contacts] ][0:3] # We need exactly 3, no PCA yet. :( eigenvalues, eigenvectors = numpy.linalg.eig( numpy.array(covariance)) # FIXME: These few lines look baaaad... indexed_eigenvalues = enumerate(eigenvalues) def get_magnitude(indexed_element): index, value = indexed_element return abs(value) sorted_indexed_eigenvalues = sorted( indexed_eigenvalues, key=get_magnitude, ) # The smallest eigenvalue leads to the ground plane's normal up_vec_idx, _ = sorted_indexed_eigenvalues[0] up_vec = VBase3(*eigenvectors[:, up_vec_idx]) # Point into the upper half-space if up_vec.z < 0: up_vec *= -1 # Calculate the forward of the centroid centroid_forward = Vec3(0, 1, 0) - up_vec * (Vec3(0, 1, 0).dot(up_vec)) # FIXME: Huh? forward_planar = centroid_forward - up_vec * ( centroid_forward.dot(up_vec)) # Now let's orient and place the centroid self.centroid.set_pos(self.vehicle, (0, 0, 0)) self.centroid.heads_up(forward_planar, up_vec) self.centroid.set_pos(self.vehicle, centroid) # Flight height for repulsor attenuation flight_height = -self.centroid.get_z(self.vehicle) if self.last_flight_height is not None: climb_speed = (flight_height - self.last_flight_height) / globalClock.dt else: climb_speed = 0 self.last_flight_height = flight_height else: local_up = False self.last_flight_height = None flight_height = 0.0 climb_speed = 0.0 # Air drag movement = self.physics_node.get_linear_velocity() drag_coefficient = self.vehicle_data.drag_coefficient drag_area = self.vehicle_data.drag_area + \ self.vehicle_data.airbrake_area * self.airbrake_state + \ self.vehicle_data.stabilizer_fins_area * self.stabilizer_fins_state air_density = self.app.environment.env_data.air_density air_speed = -self.vehicle.get_relative_vector( base.render, movement, ) # drag_force = 1/2*drag_coefficient*mass_density*area*flow_speed**2 result = Vec3(air_speed) result = (result**3) / result.length() result.componentwise_mult(drag_area) result.componentwise_mult(self.vehicle_data.drag_coefficient) drag_force = result * 1 / 2 * air_density self.sensors = { CURRENT_ORIENTATION: self.vehicle.get_hpr(self.app.render), CURRENT_MOVEMENT: movement, CURRENT_ROTATION: self.physics_node.get_angular_velocity(), LOCAL_DRAG_FORCE: drag_force, REPULSOR_DATA: repulsor_data, LOCAL_UP: local_up, FLIGHT_HEIGHT: flight_height, CLIMB_SPEED: climb_speed, } def ecu(self): repulsor_target_angles = self.ecu_repulsor_reorientation() repulsor_activation, delta_height, projected_delta_height, \ power_frac_needed = self.ecu_repulsor_activation() gyro_rotation = self.ecu_gyro_stabilization() thruster_activation = [ self.inputs[THRUST] for _ in self.vehicle_data.thruster_nodes ] airbrake = self.inputs[AIRBRAKE] stabilizer_fins = self.inputs[STABILIZER_FINS] self.commands = { # Steering commands REPULSOR_TARGET_ORIENTATIONS: repulsor_target_angles, REPULSOR_ACTIVATION: repulsor_activation, GYRO_ROTATION: gyro_rotation, THRUSTER_ACTIVATION: thruster_activation, AIRBRAKE: airbrake, STABILIZER_FINS: stabilizer_fins, # ECU data output; Interesting numbers we found along the way. HEIGHT_OVER_TARGET: delta_height, HEIGHT_OVER_TARGET_PROJECTED: projected_delta_height, REPULSOR_POWER_FRACTION_NEEDED: power_frac_needed, } def ecu_repulsor_reorientation(self): # Calculate effective repulsor motion blend values accelerate = self.inputs[ACCELERATE] turn = self.inputs[TURN] strafe = self.inputs[STRAFE] hover = self.inputs[HOVER] length = sum([abs(accelerate), abs(turn), abs(strafe), hover]) if length > 1: accelerate /= length turn /= length strafe /= length hover /= length # Split the turn signal into animation blend factors if turn > 0.0: turn_left = 0.0 turn_right = turn else: turn_left = -turn turn_right = 0.0 # Blend the repulsor angle repulsor_target_angles = [] for node in self.vehicle_data.repulsor_nodes: acc_angle = -(node.get_python_tag(ACCELERATE)) * accelerate turn_left_angle = node.get_python_tag(TURN_LEFT) * turn_left turn_right_angle = node.get_python_tag(TURN_RIGHT) * turn_right strafe_angle = node.get_python_tag(STRAFE) * strafe hover_angle = node.get_python_tag(HOVER) * hover angle = acc_angle + turn_left_angle + turn_right_angle + \ strafe_angle + hover_angle repulsor_target_angles.append(angle) return repulsor_target_angles def ecu_repulsor_activation(self): # Do we know how high we are flying? if self.sensors[LOCAL_UP]: tau = self.inputs[TARGET_FLIGHT_HEIGHT_TAU] # What would we be at in tau seconds if we weren't using repulsors? flight_height = self.sensors[FLIGHT_HEIGHT] target_flight_height = self.inputs[TARGET_FLIGHT_HEIGHT] delta_height = flight_height - target_flight_height gravity_z = self.centroid.get_relative_vector( self.app.render, self.app.environment.physics_world.get_gravity(), ).get_z() # Since gravity is an acceleration gravity_h = 1 / 2 * gravity_z * tau**2 climb_rate = self.sensors[CLIMB_SPEED] * tau projected_delta_height = delta_height + gravity_h + climb_rate # Are we sinking? if projected_delta_height <= 0: # Our projected height will be under our target height, so we # will need to apply the repulsors to make up the difference. # How much climb can each repulsor provide at 100% power right # now? max_powers = [ node.get_python_tag(FORCE) for node in self.vehicle_data.repulsor_nodes ] transferrable_powers = [ max_power * cos(0.5 * pi * data.fraction) for max_power, data in zip( max_powers, self.sensors[REPULSOR_DATA], ) ] angle_ratios = [ cos(node.get_quat(self.vehicle).get_angle_rad()) for node in self.vehicle_data.repulsor_nodes ] angled_powers = [ power * ratio for power, ratio in zip(transferrable_powers, angle_ratios) ] # We don't want to activate the repulsors unevenly, so we'll # have to go by the weakest link. total_angled_power = min(angled_powers) * len(angled_powers) # How high can we climb under 100% repulsor power? max_climb = 1 / 2 * total_angled_power * tau**2 / self.vehicle_data.mass # The fraction of power needed to achieve the desired climb power_frac_needed = -projected_delta_height / max_climb # ...and store it. repulsor_activation = [ power_frac_needed for _ in self.vehicle_data.repulsor_nodes ] else: # We're not sinking. repulsor_activation = [ 0.0 for _ in self.vehicle_data.repulsor_nodes ] power_frac_needed = 0.0 else: # We do not have ground contact. repulsor_activation = [ 0.0 for _ in self.vehicle_data.repulsor_nodes ] delta_height = 0.0 projected_delta_height = 0.0 power_frac_needed = 0.0 # The driver gives 100% repulsor power, no matter how high we are, or # whether we even have ground contact. if self.inputs[FULL_REPULSORS]: repulsor_activation = [ self.inputs[REPULSOR_ACTIVATION] for _ in self.vehicle_data.repulsor_nodes ] return repulsor_activation, delta_height, projected_delta_height,\ power_frac_needed def ecu_gyro_stabilization(self): # Active stabilization and angular dampening # FIXME: Get from self.inputs tau = 0.2 # Seconds until target orientation is reached if self.sensors[LOCAL_UP]: target_mode = self.inputs[ACTIVE_STABILIZATION_ON_GROUND] else: target_mode = self.inputs[ACTIVE_STABILIZATION_IN_AIR] if target_mode == TO_HORIZON: # Stabilize to the current heading, but in a horizontal # orientation self.target_node.set_hpr( self.app.render, self.vehicle.get_h(), 0, 0, ) elif target_mode == TO_GROUND: # Stabilize towards the local up self.target_node.set_hpr(self.centroid, (0, 0, 0)) if target_mode != PASSIVE: xyz_driver_modification = self.inputs[TARGET_ORIENTATION] hpr_driver_modification = VBase3( xyz_driver_modification.z, xyz_driver_modification.x, xyz_driver_modification.y, ) self.target_node.set_hpr( self.target_node, hpr_driver_modification, ) # Now comes the math. orientation = self.vehicle.get_quat(self.app.render) target_orientation = self.target_node.get_quat(self.app.render) delta_orientation = target_orientation * invert(orientation) self.delta_node.set_quat(invert(delta_orientation)) delta_angle = delta_orientation.get_angle_rad() if abs(delta_angle) < (pi / 360 * 0.1) or isnan(delta_angle): delta_angle = 0 axis_of_torque = VBase3(0, 0, 0) else: axis_of_torque = delta_orientation.get_axis() axis_of_torque.normalize() axis_of_torque = self.app.render.get_relative_vector( self.vehicle, axis_of_torque, ) if delta_angle > pi: delta_angle -= 2 * pi # If the mass was standing still, this would be the velocity that # has to be reached to achieve the targeted orientation in tau # seconds. target_angular_velocity = axis_of_torque * delta_angle / tau else: # `elif target_mode == PASSIVE:`, since we might want an OFF mode # Passive stabilization, so this is the pure commanded impulse target_angular_velocity = self.app.render.get_relative_vector( self.vehicle, self.inputs[TARGET_ORIENTATION] * tau / pi, ) # But we also have to cancel out the current velocity for that. angular_velocity = self.physics_node.get_angular_velocity() countering_velocity = -angular_velocity # An impulse of 1 causes an angular velocity of 2.5 rad on a unit mass, # so we have to adjust accordingly. target_impulse = target_angular_velocity / 2.5 * self.vehicle_data.mass countering_impulse = countering_velocity / 2.5 * self.vehicle_data.mass # Now just sum those up, and we have the impulse that needs to be # applied to steer towards target. impulse = target_impulse + countering_impulse return impulse def apply_air_drag(self): drag_force = self.sensors[LOCAL_DRAG_FORCE] global_drag_force = base.render.get_relative_vector( self.vehicle, drag_force, ) if not isnan(global_drag_force.x): self.physics_node.apply_central_impulse( global_drag_force * globalClock.dt, ) def apply_repulsors(self): dt = globalClock.dt repulsor_data = zip(self.vehicle_data.repulsor_nodes, self.sensors[REPULSOR_DATA], self.commands[REPULSOR_ACTIVATION], self.commands[REPULSOR_TARGET_ORIENTATIONS]) for node, data, activation, angle in repulsor_data: active = data.active frac = data.fraction # Repulse in current orientation if active and activation: if activation > 1.0: activation = 1.0 if activation < 0.0: activation = 0.0 # Repulsor power at zero distance base_strength = node.get_python_tag(FORCE) # Effective fraction of repulsors force transfer_frac = cos(0.5 * pi * frac) # Effective repulsor force strength = base_strength * activation * transfer_frac # Resulting impulse impulse_dir = Vec3(0, 0, 1) impulse_dir_world = self.app.render.get_relative_vector( node, impulse_dir, ) impulse = impulse_dir_world * strength # Apply repulsor_pos = node.get_pos(self.vehicle) # FIXME! The position at which an impulse is applied seems to be # centered around node it is applied to, but offset in the world # orientation. So, right distance, wrong angle. This is likely a # bug in Panda3D's Bullet wrapper. Or an idiosyncracy of Bullet. self.physics_node.apply_impulse( impulse * dt, self.app.render.get_relative_vector( self.vehicle, repulsor_pos, ), ) # Contact visualization node max_distance = node.get_python_tag(ACTIVATION_DISTANCE) contact_distance = -impulse_dir_world * max_distance * frac contact_node = node.get_python_tag('ray_end') contact_node.set_pos( node.get_pos(self.app.render) + contact_distance, ) #contact_node.set_hpr(node, 0, -90, 0) # Look towards repulsor #contact_node.set_hpr(0, -90, 0) # Look up contact_node.set_hpr(0, -90, contact_node.getR() + 4) # Look up and rotate contact_node.set_scale(1 - frac) contact_node.show() # Sound sound = node.get_python_tag(REPULSOR_SOUND) sound.set_volume(activation * 20) sound.set_play_rate((1 + activation / 2) * 2) else: node.get_python_tag('ray_end').hide() sound = node.get_python_tag(REPULSOR_SOUND) sound.set_volume(0.0) sound.set_play_rate(0.0) # Reorient old_hpr = node.get_python_tag(REPULSOR_OLD_ORIENTATION) want_hpr = VBase3(angle.z, angle.x, angle.y) delta_hpr = want_hpr - old_hpr max_angle = node.get_python_tag(REPULSOR_TURNING_ANGLE) * dt if delta_hpr.length() > max_angle: delta_hpr = delta_hpr / delta_hpr.length() * max_angle new_hpr = old_hpr + delta_hpr node.set_hpr(new_hpr) node.set_python_tag(REPULSOR_OLD_ORIENTATION, new_hpr) def apply_gyroscope(self): impulse = self.commands[GYRO_ROTATION] # Clamp the impulse to what the "motor" can produce. max_impulse = self.vehicle_data.max_gyro_torque if impulse.length() > max_impulse: clamped_impulse = impulse / impulse.length() * max_impulse else: clamped_impulse = impulse self.physics_node.apply_torque_impulse(clamped_impulse) impulse_ratio = clamped_impulse.length() / max_impulse sound = self.model.get_python_tag(GYROSCOPE_SOUND) sound.set_volume(0.5 * impulse_ratio) sound.set_play_rate(0.9 + 0.1 * impulse_ratio) def apply_thrusters(self): dt = globalClock.dt thruster_data = zip( self.vehicle_data.thruster_nodes, self.commands[THRUSTER_ACTIVATION], ) for node, thrust in thruster_data: if self.thruster_heat >= 1.0: thrust = 0.0 current_power = node.get_python_tag(THRUSTER_POWER) # Adjust thrust to be within bounds of thruster's ability to adjust # thrust. ramp_time = node.get_python_tag(THRUSTER_RAMP_TIME) ramp_step = (1 / ramp_time) * globalClock.dt thrust = adjust_within_limit(thrust, current_power, ramp_step) node.set_python_tag(THRUSTER_POWER, thrust) max_force = node.get_python_tag(FORCE) real_force = max_force * thrust # FIXME: See repulsors above for the shortcoming that this suffers thruster_pos = base.render.get_relative_vector( self.vehicle, node.get_pos(self.vehicle), ) thrust_direction = self.app.render.get_relative_vector( node, Vec3(0, 0, 1)) self.physics_node.apply_impulse( thrust_direction * real_force * dt, thruster_pos, ) heating = node.get_python_tag(THRUSTER_HEATING) cooling = node.get_python_tag(THRUSTER_COOLING) effective_heating = -cooling + (heating - cooling) * thrust self.thruster_heat += effective_heating * dt if self.thruster_heat < 0.0: self.thruster_heat = 0.0 # Sound sound = node.get_python_tag(THRUSTER_SOUND) sound.set_volume(thrust * 5) sound.set_play_rate((1 + thrust / 20) / 3) was_overheated = node.get_python_tag(THRUSTER_OVERHEATED) is_overheated = self.thruster_heat > 1.0 if is_overheated and not was_overheated: sound = node.get_python_tag(THRUSTER_OVERHEAT_SOUND) sound.set_volume(5) sound.play() node.set_python_tag(THRUSTER_OVERHEATED, True) if not is_overheated: node.set_python_tag(THRUSTER_OVERHEATED, False) def apply_airbrake(self): # Animation and state update only, since the effect is in air drag dt = globalClock.dt target = self.commands[AIRBRAKE] max_movement = self.airbrake_speed * dt # Clamp change to available speed delta = target - self.airbrake_state if abs(delta) > max_movement: self.airbrake_state += copysign(max_movement, delta) else: self.airbrake_state = target if self.airbrake_state > 1.0: self.airbrake_state = 1.0 if self.airbrake_state < 0.0: self.airbrake_state = 0.0 #self.model.pose(AIRBRAKE, self.airbrake_state, partName=AIRBRAKE) self.model.pose(AIRBRAKE, self.airbrake_state) def apply_stabilizer_fins(self): # Animation and state update only, since the effect is in air drag dt = globalClock.dt target = self.commands[STABILIZER_FINS] max_movement = self.stabilizer_fins_speed * dt # Clamp change to available speed delta = target - self.stabilizer_fins_state if abs(delta) > max_movement: self.stabilizer_fins_state += copysign(max_movement, delta) else: self.stabilizer_fins_state = target if self.stabilizer_fins_state > 1.0: self.stabilizer_fins_state = 1.0 if self.stabilizer_fins_state < 0.0: self.stabilizer_fins_state = 0.0 self.model.pose( STABILIZER_FINS, self.stabilizer_fins_state, #partName=STABILIZER_FINS, ) # FIXME: Implement stabilizing effect def shock(self, x=0, y=0, z=0): self.physics_node.apply_impulse( Vec3(0, 0, 0), Vec3(random(), random(), random()) * 10, ) self.physics_node.apply_torque_impulse(Vec3(x, y, z))
dt = globalClock.getDt() physics.doPhysics(dt) return task.cont s.taskMgr.add(step_physics, 'physics simulation') # A physical object in the simulation node = BulletRigidBodyNode('Box') node.setMass(1.0) node.addShape(BulletSphereShape(1)) # Attaching the physical object in the scene graph and adding # a visible model to it np = s.render.attachNewNode(node) np.set_pos(0, 0, 0) np.set_hpr(45, 0, 45) m = loader.loadModel("models/smiley") m.reparentTo(np) physics.attachRigidBody(node) # Let's actually see what's happening base.cam.setPos(0, -10, 0) base.cam.lookAt(0, 0, 0) # Give the object a nudge and run the program # the impulse vector is in world space, the position at which it is # applied is in object space. node.apply_impulse(Vec3(0, 0, 1), Point3(1, 0, 0)) node.apply_impulse(Vec3(0, 0, -1), Point3(-1, 0, 0)) s.run()