class HeightfieldVehicle(ShowBase):

    def __init__(self, heightfield_fn="heightfield.png"):
        # Store the heightfield's filename.
        self.heightfield_fn = heightfield_fn
        
        """
        Load some configuration variables, it's important for this to happen
        before ShowBase is initialized
        """
        load_prc_file_data("", """
            sync-video #t
            textures-power-2 none
            ###gl-coordinate-system default
            notify-level-gobj warning
            notify-level-grutil debug
            notify-level-shader_terrain debug
            notify-level-bullet debug
            ### model paths
            model-path /usr/share/panda3d
            model-path /home/juzzuj/code/prereq/bullet-samples/bullet-samples
            """)            
        ShowBase.__init__(self)
        base.set_background_color(0.1, 0.1, 0.8, 1)
        base.set_frame_rate_meter(True)
        
        # Increase camera Field Of View and set near and far planes
        base.camLens.set_fov(90)
        base.camLens.set_near_far(0.1, 50000)

        # Lights
        alight = AmbientLight('ambientLight')
        alight.set_color(LVector4(0.5, 0.5, 0.5, 1))
        alightNP = render.attach_new_node(alight)

        dlight = DirectionalLight('directionalLight')
        dlight.set_direction(LVector3(1, 1, -1))
        dlight.set_color(LVector4(0.7, 0.7, 0.7, 1))
        dlightNP = render.attach_new_node(dlight)

        render.clear_light()
        render.set_light(alightNP)
        render.set_light(dlightNP)

        # Basic game controls
        self.accept('escape', self.do_exit)
        self.accept('f1', base.toggle_wireframe)
        self.accept('f2', base.toggle_texture)
        self.accept('f3', self.toggle_debug)
        self.accept('f5', self.do_screenshot)
        self.accept('r', self.do_reset)
        
        # Vehicle controls
        inputState.watchWithModifiers('forward', 'w')
        inputState.watchWithModifiers('turnLeft', 'a')
        inputState.watchWithModifiers('reverse', 's')
        inputState.watchWithModifiers('turnRight', 'd')
        inputState.watchWithModifiers('forward', 'arrow_up')
        inputState.watchWithModifiers('turnLeft', 'arrow_left')
        inputState.watchWithModifiers('reverse', 'arrow_down')
        inputState.watchWithModifiers('turnRight', 'arrow_right')
        
        self.accept('space', self.reset_vehicle)
        
        # Controls to do with the terrain
        #self.accept('t', self.rise_in_front)
        self.accept('t', self.deform_terrain, ["raise"])
        self.accept('g', self.deform_terrain, ["depress"])
        self.accept('b', self.drop_boxes)
        
        # Some debugging and miscellaneous controls
        self.accept('e', self.query_elevation)
        self.accept('c', self.convert_coordinates)
        self.accept('p', self.save)
        self.accept('h', self.hide_terrain)
        
        # Task
        taskMgr.add(self.update, 'updateWorld')

        self.setup()

    """
    Macro-like function used to reduce the amount to code needed to create the
    on screen instructions
    """
    def genLabelText(self, i, text, parent="a2dTopLeft"):
        return OnscreenText(text=text, parent=getattr(base, parent), scale=.05,
                            pos=(0.06, -.065 * i), fg=(1, 1, 1, 1),
                            align=TextNode.ALeft)

    # _____HANDLER_____

    def cleanup(self):
        self.world = None
        self.worldNP.remove_node()
        self.terrain.remove_node()
        self.skybox.remove_node()
        del self.terrain_node

    def do_exit(self):
        self.cleanup()
        sys.exit(1)

    def do_reset(self):
        self.cleanup()
        self.setup()

    def toggle_debug(self):
        if self.debugNP.is_hidden():
          self.debugNP.show()
        else:
          self.debugNP.hide()

    def do_screenshot(self):
        base.screenshot('HeightfieldVehicle')

    # ____TASK___

    def update(self, task):
        # Get the time passed since the last frame
        dt = globalClock.get_dt()
        # Pass dt as parameter (we need it for sensible steering calculations)
        self.process_input(dt)
        
        """
        Basically, we want to put our camera where the camera floater is. We
        need the floater's world (=render-relative) position (it's parented to
        the vehicle)
        """
        floater_pos = render.get_relative_point(self.camera_floater,
                                                LVector3(0))
        """
        If the camera floater is under the terrain surface, adjust it, so that
        it stays above the terrain.
        """         
        elevation_at_floater_pos = self.query_elevation(floater_pos)
        if elevation_at_floater_pos.z >= floater_pos.z:
            floater_pos.z = elevation_at_floater_pos.z + 1.
        # Now we set our camera's position and make it look at the vehicle.
        base.cam.set_pos(floater_pos)
        base.cam.look_at(self.vehicleNP)
        
        # Let the Bullet library do physics calculations.
        self.world.do_physics(dt, 10, 0.008)
        return task.cont

    def process_input(self, dt):
        # Relax steering towards straight
        self.steering *= 1 - self.steering_relax_factor * dt
        
        engine_force = 0.0
        brake_force = 0.0

        if inputState.isSet('forward'):
            engine_force = 1000.0
            brake_force = 0.0

        if inputState.isSet('reverse'):
            engine_force = 0.0
            brake_force = 100.0

        if inputState.isSet('turnLeft'):
            self.steering += dt * self.steering_increment
            self.steering = min(self.steering, self.steering_clamp)

        if inputState.isSet('turnRight'):
            self.steering -= dt * self.steering_increment
            self.steering = max(self.steering, -self.steering_clamp)

        # Lower steering intensity for high speeds
        self.steering *= 1. - (self.vehicle.get_current_speed_km_hour() * 
                               self.steering_speed_reduction_factor)

        # Apply steering to front wheels
        #self.vehicle.get_wheels()[0].set_steering(self.steering)
        #self.vehicle.get_wheels()[1].set_steering(self.steering)        
        #self.vehicle.wheels[0].steering = self.steering
        #self.vehicle.wheels[1].steering = self.steering
        self.vehicle.set_steering_value(self.steering, 0)
        self.vehicle.set_steering_value(self.steering, 1)

        # Apply engine and brake to rear wheels
        #self.vehicle.wheels[2].engine_force = engine_force
        #self.vehicle.wheels[3].engine_force = engine_force
        #self.vehicle.wheels[2].brake = brake_force
        #self.vehicle.wheels[3].brake = brake_force
        self.vehicle.apply_engine_force(engine_force, 2)
        self.vehicle.apply_engine_force(engine_force, 3)
        self.vehicle.set_brake(brake_force, 2)
        self.vehicle.set_brake(brake_force, 3)

    def setup(self):        
        # Bullet physics world
        self.worldNP = render.attach_new_node('World')
        self.debugNP = self.worldNP.attach_new_node(BulletDebugNode('Debug'))
        self.world = BulletWorld()
        self.world.set_gravity(LVector3(0, 0, -9.81))
        self.world.set_debug_node(self.debugNP.node())

        # Vehicle
        # Steering info
        self.steering = 0.0              # degrees
        self.steering_clamp = 45.0       # degrees
        self.steering_increment = 120.0  # degrees per second        
        # How fast steering relaxes to straight ahead
        self.steering_relax_factor = 2.0
        # How much steering intensity decreases with higher speeds
        self.steering_speed_reduction_factor = 0.003
        
        # Chassis collision box (note: Bullet uses half-measures)
        shape = BulletBoxShape(LVector3(0.6, 1.4, 0.5))
        ts = TransformState.make_pos(LPoint3(0, 0, 0.5))
        self.vehicleNP = self.worldNP.attach_new_node(
            BulletRigidBodyNode('Vehicle'))
        self.vehicleNP.node().add_shape(shape, ts)
        # Set initial position
        self.vehicleNP.set_pos(0, 70, -10)
        self.vehicleNP.node().set_mass(800.0)
        self.vehicleNP.node().set_deactivation_enabled(False)
        self.world.attach(self.vehicleNP.node())
        # Camera floater
        self.attach_camera_floater()
        # BulletVehicle
        self.vehicle = BulletVehicle(self.world, self.vehicleNP.node())
        self.world.attach(self.vehicle)
        
        # Vehicle model
        self.yugoNP = loader.load_model('models/yugo/yugo.egg')
        self.yugoNP.reparent_to(self.vehicleNP)
        # Right front wheel
        np = loader.load_model('models/yugo/yugotireR.egg')
        np.reparent_to(self.worldNP)
        self.add_wheel(LPoint3( 0.70,  1.05, 0.3), True, np)
        # Left front wheel
        np = loader.load_model('models/yugo/yugotireL.egg')
        np.reparent_to(self.worldNP)
        self.add_wheel(LPoint3(-0.70,  1.05, 0.3), True, np)
        # Right rear wheel
        np = loader.load_model('models/yugo/yugotireR.egg')
        np.reparent_to(self.worldNP)
        self.add_wheel(LPoint3( 0.70, -1.05, 0.3), False, np)
        # Left rear wheel
        np = loader.load_model('models/yugo/yugotireL.egg')
        np.reparent_to(self.worldNP)
        self.add_wheel(LPoint3(-0.70, -1.05, 0.3), False, np)
        
        # Load a skybox
        self.skybox = self.loader.load_model(
            "samples/shader-terrain/models/skybox.bam")
        self.skybox.reparent_to(self.render)
        self.skybox.set_scale(20000)
        skybox_texture = self.loader.load_texture(
            "samples/shader-terrain/textures/skybox.jpg")
        skybox_texture.set_minfilter(SamplerState.FT_linear)
        skybox_texture.set_magfilter(SamplerState.FT_linear)
        skybox_texture.set_wrap_u(SamplerState.WM_repeat)
        skybox_texture.set_wrap_v(SamplerState.WM_mirror)
        skybox_texture.set_anisotropic_degree(16)
        self.skybox.set_texture(skybox_texture)
        skybox_shader = Shader.load(Shader.SL_GLSL, 
            "samples/shader-terrain/skybox.vert.glsl", 
            "samples/shader-terrain/skybox.frag.glsl")
        self.skybox.set_shader(skybox_shader)

        # Terrain
        self.setup_terrain()
        
    def add_wheel(self, pos, front, np):
        wheel = self.vehicle.create_wheel()

        wheel.set_node(np.node())
        wheel.set_chassis_connection_point_cs(pos)
        wheel.set_front_wheel(front)

        wheel.set_wheel_direction_cs(LVector3(0, 0, -1))
        wheel.set_wheel_axle_cs(LVector3(1, 0, 0))
        wheel.set_wheel_radius(0.25)
        wheel.set_max_suspension_travel_cm(40.0)

        wheel.set_suspension_stiffness(40.0)
        wheel.set_wheels_damping_relaxation(2.3)
        wheel.set_wheels_damping_compression(4.4)
        wheel.set_friction_slip(100.0)
        wheel.set_roll_influence(0.1)

    def attach_camera_floater(self):
        """
        Set up an auxiliary camera floater, which is parented to the vehicle.
        Every frame base.cam's position will be set to the camera floater's.
        """
        camera_behind = 8
        camera_above = 3
        self.camera_floater = NodePath("camera_floater")
        self.camera_floater.reparent_to(self.vehicleNP)
        self.camera_floater.set_y(-camera_behind)
        self.camera_floater.set_z(camera_above)

    def reset_vehicle(self):
        reset_pos = self.vehicleNP.get_pos()
        reset_pos.z += 3
        self.vehicleNP.node().clear_forces()
        self.vehicleNP.node().set_linear_velocity(LVector3(0))
        self.vehicleNP.node().set_angular_velocity(LVector3(0))
        self.vehicleNP.set_pos(reset_pos)
        self.vehicleNP.set_hpr(LVector3(0))
        
    def drop_boxes(self):
        """
        Puts a stack of boxes at a distance in front of the vehicle.
        The boxes will not necessarily spawn above the terrain and will not
        be cleaned up.
        """ 
        model = loader.load_model('models/box.egg')
        model.set_pos(-0.5, -0.5, -0.5)
        model.flatten_light()
        shape = BulletBoxShape(LVector3(0.5, 0.5, 0.5))
        ahead = self.vehicleNP.get_pos() + self.vehicle.get_forward_vector()*15
        
        for i in range(6):
            node = BulletRigidBodyNode('Box')
            node.set_mass(5.0)
            node.add_shape(shape)
            node.set_deactivation_enabled(False)
            np = render.attach_new_node(node)
            np.set_pos(ahead.x, ahead.y, ahead.z + i*2)
            self.world.attach(node)
            model.copy_to(np)
        
    def query_elevation(self, xy_pos=None):
        """
        Query elevation for xy_pos if present, else for vehicle position.
        No xy_pos means verbose mode.
        """
        query_pos = xy_pos or self.vehicleNP.get_pos()
        """
        This method is accurate and may be useful for placing 
        objects on the terrain surface.
        """
        result = self.world.ray_test_closest(
            LPoint3(query_pos.x, query_pos.y, -10000),
            LPoint3(query_pos.x, query_pos.y, 10000))
        if result.has_hit():
            hit_pos = result.get_hit_pos()
            if not xy_pos:
                print("Bullet heightfield elevation at "
                    "X {:.2f} | Y {:.2f} is {:.3f}".format(
                    hit_pos.x, hit_pos.y, hit_pos.z))
        else:
            hit_pos = None
            if not xy_pos:
                print("Could not query elevation at {}".format(xy_pos))
        
        """
        This method is less accurate than the one above.
        Under heavy ray-testing stress (ray tests are performed for all vehicle
        wheels, the above elevation query etc.) Bullet sometimes seems to be a
        little unreliable.
        """
        texspace_pos = self.terrain.get_relative_point(render, query_pos)
        stm_pos = self.terrain_node.uv_to_world(
            LTexCoord(texspace_pos.x, texspace_pos.y))
        if not xy_pos:
            print("ShaderTerrainMesh elevation at "
                "X {:.2f} | Y {:.2f} is {:.3f}".format(
                stm_pos.x, stm_pos.y, stm_pos.z))
        
        return hit_pos or stm_pos
    
    def setup_terrain(self):
        """
        Terrain info
        Units are meters, which is preferable when working with Bullet.
        """
        self.terrain_scale = LVector3(512, 512, 100)
        self.terrain_pos = LVector3(-256, -256, -70)
        # sample values for a 4096 x 4096px heightmap.
        #self.terrain_scale = LVector3(4096, 4096, 1000)
        #self.terrain_pos = LVector3(-2048, -2048, -70)
        """
        Diamond_subdivision is an alternating triangulation scheme and may
        produce better results.
        """
        use_diamond_subdivision = True
        
        """
        Construct the terrain
        Without scaling, any ShaderTerrainMesh is 1x1x1 units.
        """
        self.terrain_node = ShaderTerrainMesh()
        """
        Set a heightfield, the heightfield should be a 16-bit png and
        have a quadratic size of a power of two.
        """
        heightfield = Texture()
        heightfield.read(self.heightfield_fn)
        heightfield.set_keep_ram_image(True)        
        self.terrain_node.heightfield = heightfield
        
        # Display characteristic values of the heightfield texture
        #minpoint, maxpoint, avg = LPoint3(), LPoint3(), LPoint3()
        #heightfield.calc_min_max(minpoint, maxpoint)
        #heightfield.calc_average_point(avg, 0.5, 0.5, 0.5)
        #print("avg: {} min: {} max: {}".format(avg.x, minpoint.x, maxpoint.x))

        """
        Set the target triangle width. For a value of 10.0 for example,
        the ShaderTerrainMesh will attempt to make every triangle 10 pixels
        wide on screen.
        """
        self.terrain_node.target_triangle_width = 10.0
        if use_diamond_subdivision:
            """
            This has to be specified before calling .generate()
            The default is false.
            """
            load_prc_file_data("", "stm-use-hexagonal-layout true")
        
        self.terrain_node.generate()
        """
        Attach the terrain to the main scene and set its scale. With no scale
        set, the terrain ranges from (0, 0, 0) to (1, 1, 1)
        """
        self.terrain = self.render.attach_new_node(self.terrain_node)
        self.terrain.set_scale(self.terrain_scale)
        self.terrain.set_pos(self.terrain_pos)
        """
        Set a vertex and a fragment shader on the terrain. The
        ShaderTerrainMesh only works with an applied shader.
        """
        terrain_shader = Shader.load(Shader.SL_GLSL, 
            "samples/shader-terrain/terrain.vert.glsl", 
            "samples/shader-terrain/terrain.frag.glsl")
        self.terrain.set_shader(terrain_shader)
        self.terrain.set_shader_input("camera", base.camera)
        # Set some texture on the terrain
        grass_tex = self.loader.load_texture(
            "samples/shader-terrain/textures/grass.png")
        grass_tex.set_minfilter(SamplerState.FT_linear_mipmap_linear)
        grass_tex.set_anisotropic_degree(16)
        self.terrain.set_texture(grass_tex)

        """
        Set up the DynamicHeightfield (it's a type of PfmFile). We load the
        same heightfield image as with ShaderTerrainMesh.
        """
        self.DHF = DynamicHeightfield()
        self.DHF.read(self.heightfield_fn)
        """
        Set up empty PfmFiles to prepare stuff in that is going to
        dynamically modify our terrain.
        """
        self.StagingPFM = PfmFile()
        self.RotorPFM = PfmFile()
        
        """
        Set up the BulletHeightfieldShape (=collision terrain) and give it
        some sensible physical properties.
        """
        self.HFS = BulletHeightfieldShape(self.DHF, self.terrain_scale.z,
                                          STM=True)
        if use_diamond_subdivision:
            self.HFS.set_use_diamond_subdivision(True)
        HFS_rigidbody = BulletRigidBodyNode("BulletTerrain")
        HFS_rigidbody.set_static(True)
        friction = 2.0
        HFS_rigidbody.set_anisotropic_friction(
            LVector3(friction, friction, friction/1.3))
        HFS_rigidbody.set_restitution(0.3)
        HFS_rigidbody.add_shape(self.HFS)
        self.world.attach(HFS_rigidbody)
        
        HFS_NP = NodePath(HFS_rigidbody)
        HFS_NP.reparent_to(self.worldNP)
        """
        This aligns the Bullet terrain with the ShaderTerrainMesh rendered
        terrain. It will be exact as long as the terrain vertex shader from
        the STM sample is used and no additional tessellation shader.
        For Bullet (as for other physics engines) the origin of objects is at
        the center.
        """
        HFS_NP.set_pos(self.terrain_pos + self.terrain_scale/2)
        HFS_NP.set_sx(self.terrain_scale.x / heightfield.get_x_size())
        HFS_NP.set_sy(self.terrain_scale.y / heightfield.get_y_size())
        
        # Disables Bullet debug rendering for the terrain, because it is slow.
        #HFS_NP.node().set_debug_enabled(False)
        
        """
        Finally, link the ShaderTerrainMesh and the BulletHeightfieldShape to
        the DynamicHeightfield. From now on changes to the DynamicHeightfield
        will propagate to the (visible) ShaderTerrainMesh and the (collidable)
        BulletHeightfieldShape.
        """
        self.HFS.set_dynamic_heightfield(self.DHF)
        self.terrain_node.set_dynamic_heightfield(self.DHF)

    def deform_terrain(self, mode="raise", duration=1.0):
        self.deform_duration = duration
        """
        Calculate distance to the spot at which we want to raise the terrain.
        At its current speed the vehicle would reach it in 2.5 seconds.
        [km/h] / 3.6 = [m/s] * [s] = [m]
        """
        distance = self.vehicle.get_current_speed_km_hour() / 3.6 * 2.5
        spot_pos_world = (self.vehicleNP.get_pos() +
                          self.vehicle.get_forward_vector() * distance)

        spot_pos_heightfield = self.terrain_node.world_to_heightfield(
            spot_pos_world)
        xcenter = spot_pos_heightfield.x
        ycenter = spot_pos_heightfield.y
        """
        To create a smooth hill/depression we call PfmFile.pull_spot to create
        a sort of gradient. You can use self.cardmaker_debug to view it.
        
        From the Panda3D API documentation of PfmFile.pull_spot:
        Applies delta * t to the point values within radius (xr, yr)
        distance of (xc, yc). The t value is scaled from 1.0 at the center
        to 0.0 at radius (xr, yr), and this scale follows the specified
        exponent. Returns the number of points affected.
        """
        # Delta to apply to the point values in DynamicHeightfield array.
        delta = LVector4(0.001)
        # Make the raised spot elliptical.
        xradius = 10.0  # meters
        yradius = 6.0  # meters
        # Choose an exponent
        exponent = 0.6
        # Counter-clockwise angle between Y-axis 
        angle = self.vehicle.get_forward_vector().signed_angle_deg(
            LVector3.forward(), LVector3.down())
            
        # Define all we need to repeatedly deform the terrain using a task.    
        self.spot_params = [delta, xcenter, ycenter, xradius, yradius,
                            exponent, mode]
                            
        # Clear staging area to a size that fits our raised region.
        self.StagingPFM.clear(int(xradius)*2, int(yradius)*2, num_channels=1)
        
        """
        There are two options:
        (1) Rotate our hill/depression to be perpendicular
            to the vehicle's trajectory. This is the default.
        (2) Just put it in the terrain unrotated.
        """

        # Option (1)
        self.StagingPFM.pull_spot(delta,
                                  xradius-0.5, yradius-0.5,
                                  xradius, yradius,
                                  exponent)
        
        # Rotate wider side so it's perpendicular to vehicle's trajectory.
        self.RotorPFM.rotate_from(self.StagingPFM, angle)
        
        self.raise_start_time = globalClock.get_real_time()
        taskMgr.do_method_later(0.03, self.deform_perpendicular,
                                "DeformPerpendicularSpot")
                                
        # Option (2)
        #taskMgr.do_method_later(0.03, self.deform_regular, "RaiseASpot")
        
    def deform_perpendicular(self, task):
        if (globalClock.get_real_time() - self.raise_start_time <
            self.deform_duration):
            (delta, xcenter, ycenter,
             xradius, yradius, exponent, mode) = self.spot_params

            """
            Copy rotated hill to our DynamicHeightfield.
            The values from RotorPFM are added to the ones found in the
            DynamicHeightfield.
            """
            self.DHF.add_sub_image(self.RotorPFM, 
                int(xcenter - self.RotorPFM.get_x_size()/2),
                int(ycenter - self.RotorPFM.get_y_size()/2),
                0, 0, self.RotorPFM.get_x_size(), self.RotorPFM.get_y_size(),
                1.0 if mode == "raise" else -1.0)
            
            # Output images of our PfmFiles.
            #self.RotorPFM.write("dbg_RotorPFM.png")
            #self.StagingPFM.write("dbg_StagingPFM.png")
            return task.again
        
        self.cardmaker_debug()
        return task.done
        
    def deform_regular(self, task):
        if (globalClock.get_real_time() - self.raise_start_time <
            self.deform_duration):
            (delta, xcenter, ycenter,
             xradius, yradius, exponent, mode) = self.spot_params
            self.DHF.pull_spot(delta * (1.0 if mode == "raise" else -1.0),
                               xcenter, ycenter, xradius, yradius, exponent)
            return task.again
        return task.done

    def convert_coordinates(self):
        vpos = self.vehicleNP.get_pos()
        print("VPOS world: ", vpos)
        W2H = self.terrain_node.world_to_heightfield(vpos)
        print("W2H: ", W2H)
        H2W = self.terrain_node.heightfield_to_world(LTexCoord(W2H.x, W2H.y))
        print("H2W: ", H2W)
        
        W2U = self.terrain_node.world_to_uv(vpos)
        print("W2U: ", W2U)
        U2W = self.terrain_node.uv_to_world(LTexCoord(W2U.x, W2U.y))
        print("U2W: ", U2W)

    def hide_terrain(self):
        if self.terrain.is_hidden():
            self.terrain.show()
        else:
            self.terrain.hide()

    def save(self):
        self.DHF.write("dbg_dynamicheightfield.png")

    def pfmvizzer_debug(self, pfm):
        vizzer = PfmVizzer(pfm)
        """
        To align the mesh we generate with PfmVizzer with our 
        ShaderTerrainMesh, BulletHeightfieldShape, and DynamicHeightfield, we
        need to set a negative scale on the Y axis. This reverses the winding
        order of the triangles in the mesh, which is why we choose
        PfmVizzer.MF_back
        Note that this does not accurately match up with our mesh because the
        interpolation differs. Still, PfmVizzer can be useful when
        inspecting, distorting, etc. PfmFiles.
        """
        self.vizNP = vizzer.generate_vis_mesh(PfmVizzer.MF_back)
        self.vizNP.set_texture(loader.load_texture("maps/grid.rgb"))
        self.vizNP.reparent_to(render)
        if pfm == self.DHF or pfm == self.terrain_node.heightfield:
            self.vizNP.set_pos(self.terrain_pos.x, -self.terrain_pos.y,
                               self.terrain_pos.z)
            self.vizNP.set_scale(self.terrain_scale.x, -self.terrain_scale.y,
                                 self.terrain_scale.z)

    def cardmaker_debug(self):
        for node in render2d.find_all_matches("pfm"):
            node.remove_node()
        for text in base.a2dBottomLeft.find_all_matches("*"):
            text.remove_node()
        width = 0.2  # render2d coordinates range: [-1..1]
        # Pseudo-normalize our PfmFile for better contrast.
        normalized_pfm = PfmFile(self.RotorPFM)
        max_p = LVector3()
        normalized_pfm.calc_min_max(LVector3(), max_p)
        normalized_pfm *= 1.0 / max_p.x
        # Put it in a texture
        tex = Texture()
        tex.load(normalized_pfm)
        # Apply the texture to a quad and put it in the lower left corner.
        cm = CardMaker("pfm")
        cm.set_frame(0, width, 0,
            width / normalized_pfm.get_x_size() * normalized_pfm.get_y_size())
        card = base.render2d.attach_new_node(cm.generate())
        card.set_pos(-1, 0, -1)        
        card.set_texture(tex)
        # Display max value text
        self.genLabelText(-3,
                          "Max value: {:.3f} == {:.2f}m".format(max_p.x,
                            max_p.x * self.terrain_scale.z),
                          parent="a2dBottomLeft")
Exemplo n.º 2
0
class BaseVehicle(DynamicElement):
    MODEL = None
    """
    Vehicle chassis and its wheels index
                    0       1
                    II-----II
                        |
                        |  <---chassis
                        |
                    II-----II
                    2       3
    """
    PARAMETER_SPACE = PGSpace(
        VehicleParameterSpace.BASE_VEHICLE
    )  # it will not sample config from parameter space
    COLLISION_MASK = CollisionGroup.EgoVehicle
    STEERING_INCREMENT = 0.05

    LENGTH = None
    WIDTH = None

    def __init__(
        self,
        pg_world: PGWorld,
        vehicle_config: Union[dict, PGConfig] = None,
        physics_config: dict = None,
        random_seed: int = 0,
        name: str = None,
    ):
        """
        This Vehicle Config is different from self.get_config(), and it is used to define which modules to use, and
        module parameters. And self.physics_config defines the physics feature of vehicles, such as length/width
        :param pg_world: PGWorld
        :param vehicle_config: mostly, vehicle module config
        :param physics_config: vehicle height/width/length, find more physics para in VehicleParameterSpace
        :param random_seed: int
        """
        self.vehicle_config = PGConfig(vehicle_config)

        # self.vehicle_config = self.get_vehicle_config(vehicle_config) \
        #     if vehicle_config is not None else self._default_vehicle_config()

        # observation, action
        self.action_space = self.get_action_space_before_init(
            extra_action_dim=self.vehicle_config["extra_action_dim"])

        super(BaseVehicle, self).__init__(random_seed, name=name)
        # config info
        self.set_config(self.PARAMETER_SPACE.sample())
        if physics_config is not None:
            self.set_config(physics_config)
        self.increment_steering = self.vehicle_config["increment_steering"]
        self.enable_reverse = self.vehicle_config["enable_reverse"]
        self.max_speed = self.vehicle_config["max_speed"]
        self.max_steering = self.vehicle_config["max_steering"]

        self.pg_world = pg_world
        self.node_path = NodePath("vehicle")

        # create
        self.spawn_place = (0, 0)
        self._add_chassis(pg_world.physics_world)
        self.wheels = self._create_wheel()

        # modules
        self.image_sensors = {}
        self.lidar: Optional[Lidar] = None
        self.side_detector: Optional[SideDetector] = None
        self.lane_line_detector: Optional[LaneLineDetector] = None
        self.routing_localization: Optional[RoutingLocalizationModule] = None
        self.lane: Optional[AbstractLane] = None
        self.lane_index = None
        self.vehicle_panel = VehiclePanel(self.pg_world) if (
            self.pg_world.mode == RENDER_MODE_ONSCREEN) else None

        # state info
        self.throttle_brake = 0.0
        self.steering = 0
        self.last_current_action = deque([(0.0, 0.0), (0.0, 0.0)], maxlen=2)
        self.last_position = self.spawn_place
        self.last_heading_dir = self.heading
        self.dist_to_left_side = None
        self.dist_to_right_side = None

        # collision info render
        self.collision_info_np = self._init_collision_info_render(pg_world)
        self.collision_banners = {}  # to save time
        self.current_banner = None
        self.attach_to_pg_world(self.pg_world.pbr_render,
                                self.pg_world.physics_world)

        # step info
        self.out_of_route = None
        self.on_lane = None
        # self.step_info = None
        self._init_step_info()

        # others
        self._add_modules_for_vehicle(self.vehicle_config["use_render"])
        self.takeover = False
        self._expert_takeover = False
        self.energy_consumption = 0

        # overtake_stat
        self.front_vehicles = set()
        self.back_vehicles = set()

    def _add_modules_for_vehicle(self, use_render: bool):
        # add self module for training according to config
        vehicle_config = self.vehicle_config
        self.add_routing_localization(
            vehicle_config["show_navi_mark"])  # default added

        if self.vehicle_config["side_detector"]["num_lasers"] > 0:
            self.side_detector = SideDetector(
                self.pg_world.render,
                self.vehicle_config["side_detector"]["num_lasers"],
                self.vehicle_config["side_detector"]["distance"],
                self.vehicle_config["show_side_detector"])

        if self.vehicle_config["lane_line_detector"]["num_lasers"] > 0:
            self.lane_line_detector = LaneLineDetector(
                self.pg_world.render,
                self.vehicle_config["lane_line_detector"]["num_lasers"],
                self.vehicle_config["lane_line_detector"]["distance"],
                self.vehicle_config["show_lane_line_detector"])

        if not self.vehicle_config["use_image"]:
            if vehicle_config["lidar"]["num_lasers"] > 0 and vehicle_config[
                    "lidar"]["distance"] > 0:
                self.add_lidar(
                    num_lasers=vehicle_config["lidar"]["num_lasers"],
                    distance=vehicle_config["lidar"]["distance"],
                    show_lidar_point=vehicle_config["show_lidar"])
            else:
                import logging
                logging.warning(
                    "You have set the lidar config to: {}, which seems to be invalid!"
                    .format(vehicle_config["lidar"]))

            if use_render:
                rgb_cam_config = vehicle_config["rgb_cam"]
                rgb_cam = RGBCamera(rgb_cam_config[0], rgb_cam_config[1],
                                    self.chassis_np, self.pg_world)
                self.add_image_sensor("rgb_cam", rgb_cam)

                mini_map = MiniMap(vehicle_config["mini_map"], self.chassis_np,
                                   self.pg_world)
                self.add_image_sensor("mini_map", mini_map)
            return

        if vehicle_config["use_image"]:
            # 3 types image observation
            if vehicle_config["image_source"] == "rgb_cam":
                rgb_cam_config = vehicle_config["rgb_cam"]
                rgb_cam = RGBCamera(rgb_cam_config[0], rgb_cam_config[1],
                                    self.chassis_np, self.pg_world)
                self.add_image_sensor("rgb_cam", rgb_cam)
            elif vehicle_config["image_source"] == "mini_map":
                mini_map = MiniMap(vehicle_config["mini_map"], self.chassis_np,
                                   self.pg_world)
                self.add_image_sensor("mini_map", mini_map)
            elif vehicle_config["image_source"] == "depth_cam":
                cam_config = vehicle_config["depth_cam"]
                depth_cam = DepthCamera(*cam_config, self.chassis_np,
                                        self.pg_world)
                self.add_image_sensor("depth_cam", depth_cam)
            else:
                raise ValueError("No module named {}".format(
                    vehicle_config["image_source"]))

        # load more sensors for visualization when render, only for beauty...
        if use_render:
            if vehicle_config["image_source"] == "mini_map":
                rgb_cam_config = vehicle_config["rgb_cam"]
                rgb_cam = RGBCamera(rgb_cam_config[0], rgb_cam_config[1],
                                    self.chassis_np, self.pg_world)
                self.add_image_sensor("rgb_cam", rgb_cam)
            else:
                mini_map = MiniMap(vehicle_config["mini_map"], self.chassis_np,
                                   self.pg_world)
                self.add_image_sensor("mini_map", mini_map)

    def _init_step_info(self):
        # done info will be initialized every frame
        self.chassis_np.node().getPythonTag(
            BodyName.Base_vehicle).init_collision_info()
        self.out_of_route = False  # re-route is required if is false
        self.on_lane = True  # on lane surface or not
        # self.step_info = {"reward": 0, "cost": 0}

    def _preprocess_action(self, action):
        if self.vehicle_config["action_check"]:
            assert self.action_space.contains(
                action
            ), "Input {} is not compatible with action space {}!".format(
                action, self.action_space)

        # protect agent from nan error
        action = safe_clip_for_small_array(action,
                                           min_val=self.action_space.low[0],
                                           max_val=self.action_space.high[0])
        return action, {'raw_action': (action[0], action[1])}

    def prepare_step(self, action):
        """
        Save info and make decision before action
        """
        # init step info to store info before each step
        self._init_step_info()
        action, step_info = self._preprocess_action(action)

        self.last_position = self.position
        self.last_heading_dir = self.heading
        self.last_current_action.append(
            action
        )  # the real step of physics world is implemented in taskMgr.step()
        if self.increment_steering:
            self.set_incremental_action(action)
        else:
            self.set_act(action)
        if self.vehicle_panel is not None:
            self.vehicle_panel.renew_2d_car_para_visualization(self)
        return step_info

    def update_state(self, pg_world=None, detector_mask="WRONG"):
        # lidar
        if self.lidar is not None:
            self.lidar.perceive(self.position,
                                self.heading_theta,
                                self.pg_world.physics_world.dynamic_world,
                                extra_filter_node={self.chassis_np.node()},
                                detector_mask=detector_mask)
        if self.routing_localization is not None:
            self.lane, self.lane_index, = self.routing_localization.update_navigation_localization(
                self)
        if self.side_detector is not None:
            self.side_detector.perceive(
                self.position, self.heading_theta,
                self.pg_world.physics_world.static_world)
        if self.lane_line_detector is not None:
            self.lane_line_detector.perceive(
                self.position, self.heading_theta,
                self.pg_world.physics_world.static_world)
        self._state_check()
        self.update_dist_to_left_right()
        step_energy, episode_energy = self._update_energy_consumption()
        self.out_of_route = self._out_of_route()
        step_info = self._update_overtake_stat()
        step_info.update({
            "velocity": float(self.speed),
            "steering": float(self.steering),
            "acceleration": float(self.throttle_brake),
            "step_energy": step_energy,
            "episode_energy": episode_energy
        })
        return step_info

    def _out_of_route(self):
        left, right = self._dist_to_route_left_right()
        return True if right < 0 or left < 0 else False

    def _update_energy_consumption(self):
        """
        The calculation method is from
        https://www.researchgate.net/publication/262182035_Reduction_of_Fuel_Consumption_and_Exhaust_Pollutant_Using_Intelligent_Transport_System
        default: 3rd gear, try to use ae^bx to fit it, dp: (90, 8), (130, 12)
        :return: None
        """
        distance = norm(*(self.last_position - self.position)) / 1000  # km
        step_energy = 3.25 * math.pow(np.e, 0.01 * self.speed) * distance / 100
        # step_energy is in Liter, we return mL
        step_energy = step_energy * 1000
        self.energy_consumption += step_energy  # L/100 km
        return step_energy, self.energy_consumption

    def reset(self, map: Map, pos: np.ndarray = None, heading: float = 0.0):
        """
        pos is a 2-d array, and heading is a float (unit degree)
        if pos is not None, vehicle will be reset to the position
        else, vehicle will be reset to spawn place
        """
        if pos is None:
            lane = map.road_network.get_lane(
                self.vehicle_config["spawn_lane_index"])
            pos = lane.position(self.vehicle_config["spawn_longitude"],
                                self.vehicle_config["spawn_lateral"])
            heading = np.rad2deg(
                lane.heading_at(self.vehicle_config["spawn_longitude"]))
            self.spawn_place = pos
        heading = -np.deg2rad(heading) - np.pi / 2
        self.set_static(False)
        self.chassis_np.setPos(panda_position(Vec3(*pos, 1)))
        self.chassis_np.setQuat(
            LQuaternionf(math.cos(heading / 2), 0, 0, math.sin(heading / 2)))
        self.update_map_info(map)
        self.chassis_np.node().clearForces()
        self.chassis_np.node().setLinearVelocity(Vec3(0, 0, 0))
        self.chassis_np.node().setAngularVelocity(Vec3(0, 0, 0))
        self.system.resetSuspension()
        # np.testing.assert_almost_equal(self.position, pos, decimal=4)

        # done info
        self._init_step_info()

        # other info
        self.throttle_brake = 0.0
        self.steering = 0
        self.last_current_action = deque([(0.0, 0.0), (0.0, 0.0)], maxlen=2)
        self.last_position = self.spawn_place
        self.last_heading_dir = self.heading

        self.update_dist_to_left_right()
        self.takeover = False
        self.energy_consumption = 0

        # overtake_stat
        self.front_vehicles = set()
        self.back_vehicles = set()

        # for render
        if self.vehicle_panel is not None:
            self.vehicle_panel.renew_2d_car_para_visualization(self)

        if "depth_cam" in self.image_sensors and self.image_sensors[
                "depth_cam"].view_ground:
            for block in map.blocks:
                block.node_path.hide(CamMask.DepthCam)

        assert self.routing_localization
        # Please note that if you respawn agent to some new place and might have a new destination,
        # you should reset the routing localization too! Via: vehicle.routing_localization.set_route or
        # vehicle.update

    """------------------------------------------- act -------------------------------------------------"""

    def set_act(self, action):
        steering = action[0]
        self.throttle_brake = action[1]
        self.steering = steering
        self.system.setSteeringValue(self.steering * self.max_steering, 0)
        self.system.setSteeringValue(self.steering * self.max_steering, 1)
        self._apply_throttle_brake(action[1])

    def set_incremental_action(self, action: np.ndarray):
        self.throttle_brake = action[1]
        self.steering += action[0] * self.STEERING_INCREMENT
        self.steering = clip(self.steering, -1, 1)
        steering = self.steering * self.max_steering
        self.system.setSteeringValue(steering, 0)
        self.system.setSteeringValue(steering, 1)
        self._apply_throttle_brake(action[1])

    def _apply_throttle_brake(self, throttle_brake):
        max_engine_force = self.vehicle_config["max_engine_force"]
        max_brake_force = self.vehicle_config["max_brake_force"]
        for wheel_index in range(4):
            if throttle_brake >= 0:
                self.system.setBrake(2.0, wheel_index)
                if self.speed > self.max_speed:
                    self.system.applyEngineForce(0.0, wheel_index)
                else:
                    self.system.applyEngineForce(
                        max_engine_force * throttle_brake, wheel_index)
            else:
                if self.enable_reverse:
                    self.system.applyEngineForce(
                        max_engine_force * throttle_brake, wheel_index)
                    self.system.setBrake(0, wheel_index)
                else:
                    self.system.applyEngineForce(0.0, wheel_index)
                    self.system.setBrake(
                        abs(throttle_brake) * max_brake_force, wheel_index)

    """---------------------------------------- vehicle info ----------------------------------------------"""

    def update_dist_to_left_right(self):
        self.dist_to_left_side, self.dist_to_right_side = self._dist_to_route_left_right(
        )

    def _dist_to_route_left_right(self):
        current_reference_lane = self.routing_localization.current_ref_lanes[0]
        _, lateral_to_reference = current_reference_lane.local_coordinates(
            self.position)
        lateral_to_left = lateral_to_reference + self.routing_localization.get_current_lane_width(
        ) / 2
        lateral_to_right = self.routing_localization.get_current_lateral_range(
            self.position, self.pg_world) - lateral_to_left
        return lateral_to_left, lateral_to_right

    @property
    def position(self):
        return pgdrive_position(self.chassis_np.getPos())

    @property
    def speed(self):
        """
        km/h
        """
        velocity = self.chassis_np.node().get_linear_velocity()
        speed = norm(velocity[0], velocity[1]) * 3.6
        return clip(speed, 0.0, 100000.0)

    @property
    def heading(self):
        real_heading = self.heading_theta
        # heading = np.array([math.cos(real_heading), math.sin(real_heading)])
        heading = PGVector((math.cos(real_heading), math.sin(real_heading)))
        return heading

    @property
    def heading_theta(self):
        """
        Get the heading theta of vehicle, unit [rad]
        :return:  heading in rad
        """
        return (pgdrive_heading(self.chassis_np.getH()) - 90) / 180 * math.pi

    @property
    def velocity(self) -> np.ndarray:
        return self.speed * self.velocity_direction

    @property
    def velocity_direction(self):
        direction = self.system.getForwardVector()
        return np.asarray([direction[0], -direction[1]])

    @property
    def current_road(self):
        return Road(*self.lane_index[0:-1])

    """---------------------------------------- some math tool ----------------------------------------------"""

    def heading_diff(self, target_lane):
        lateral = None
        if isinstance(target_lane, StraightLane):
            lateral = np.asarray(
                get_vertical_vector(target_lane.end - target_lane.start)[1])
        elif isinstance(target_lane, CircularLane):
            if target_lane.direction == -1:
                lateral = self.position - target_lane.center
            else:
                lateral = target_lane.center - self.position
        else:
            raise ValueError("Unknown target lane type: {}".format(
                type(target_lane)))
        lateral_norm = norm(lateral[0], lateral[1])
        forward_direction = self.heading
        # print(f"Old forward direction: {self.forward_direction}, new heading {self.heading}")
        forward_direction_norm = norm(forward_direction[0],
                                      forward_direction[1])
        if not lateral_norm * forward_direction_norm:
            return 0
        cos = ((forward_direction[0] * lateral[0] +
                forward_direction[1] * lateral[1]) /
               (lateral_norm * forward_direction_norm))
        # return cos
        # Normalize to 0, 1
        return clip(cos, -1.0, 1.0) / 2 + 0.5

    def projection(self, vector):
        # Projected to the heading of vehicle
        # forward_vector = self.vehicle.get_forward_vector()
        # forward_old = (forward_vector[0], -forward_vector[1])

        forward = self.heading

        # print(f"[projection] Old forward {forward_old}, new heading {forward}")

        norm_velocity = norm(forward[0], forward[1]) + 1e-6
        project_on_heading = (vector[0] * forward[0] +
                              vector[1] * forward[1]) / norm_velocity

        side_direction = get_vertical_vector(forward)[1]
        side_norm = norm(side_direction[0], side_direction[1]) + 1e-6
        project_on_side = (vector[0] * side_direction[0] +
                           vector[1] * side_direction[1]) / side_norm
        return project_on_heading, project_on_side

    def lane_distance_to(self, vehicle, lane: AbstractLane = None) -> float:
        assert self.routing_localization is not None, "a routing and localization module should be added " \
                                                      "to interact with other vehicles"
        if not vehicle:
            return np.nan
        if not lane:
            lane = self.lane
        return lane.local_coordinates(
            vehicle.position)[0] - lane.local_coordinates(self.position)[0]

    """-------------------------------------- for vehicle making ------------------------------------------"""

    def _add_chassis(self, pg_physics_world: PGPhysicsWorld):
        para = self.get_config()
        self.LENGTH = self.vehicle_config["vehicle_length"]
        self.WIDTH = self.vehicle_config["vehicle_width"]
        chassis = BaseVehicleNode(BodyName.Base_vehicle, self)
        chassis.setIntoCollideMask(BitMask32.bit(CollisionGroup.EgoVehicle))
        chassis_shape = BulletBoxShape(
            Vec3(self.WIDTH / 2, self.LENGTH / 2,
                 para[Parameter.vehicle_height] / 2))
        ts = TransformState.makePos(
            Vec3(0, 0, para[Parameter.chassis_height] * 2))
        chassis.addShape(chassis_shape, ts)
        heading = np.deg2rad(-para[Parameter.heading] - 90)
        chassis.setMass(para[Parameter.mass])
        self.chassis_np = self.node_path.attachNewNode(chassis)
        # not random spawn now
        self.chassis_np.setPos(Vec3(*self.spawn_place, 1))
        self.chassis_np.setQuat(
            LQuaternionf(math.cos(heading / 2), 0, 0, math.sin(heading / 2)))
        chassis.setDeactivationEnabled(False)
        chassis.notifyCollisions(
            True
        )  # advance collision check, do callback in pg_collision_callback
        self.dynamic_nodes.append(chassis)

        chassis_beneath = BulletGhostNode(BodyName.Base_vehicle_beneath)
        chassis_beneath.setIntoCollideMask(
            BitMask32.bit(CollisionGroup.EgoVehicleBeneath))
        chassis_beneath.addShape(chassis_shape)
        self.chassis_beneath_np = self.chassis_np.attachNewNode(
            chassis_beneath)
        self.dynamic_nodes.append(chassis_beneath)

        self.system = BulletVehicle(pg_physics_world.dynamic_world, chassis)
        self.system.setCoordinateSystem(ZUp)
        self.dynamic_nodes.append(
            self.system
        )  # detach chassis will also detach system, so a waring will generate

        if self.render:
            if self.MODEL is None:
                model_path = 'models/ferra/scene.gltf'
                self.MODEL = self.loader.loadModel(
                    AssetLoader.file_path(model_path))
                self.MODEL.setZ(para[Parameter.vehicle_vis_z])
                self.MODEL.setY(para[Parameter.vehicle_vis_y])
                self.MODEL.setH(para[Parameter.vehicle_vis_h])
                self.MODEL.set_scale(para[Parameter.vehicle_vis_scale])
            self.MODEL.instanceTo(self.chassis_np)

    def _create_wheel(self):
        para = self.get_config()
        f_l = para[Parameter.front_tire_longitude]
        r_l = -para[Parameter.rear_tire_longitude]
        lateral = para[Parameter.tire_lateral]
        axis_height = para[Parameter.tire_radius] + 0.05
        radius = para[Parameter.tire_radius]
        wheels = []
        for k, pos in enumerate([
                Vec3(lateral, f_l, axis_height),
                Vec3(-lateral, f_l, axis_height),
                Vec3(lateral, r_l, axis_height),
                Vec3(-lateral, r_l, axis_height)
        ]):
            wheel = self._add_wheel(pos, radius, True if k < 2 else False,
                                    True if k == 0 or k == 2 else False)
            wheels.append(wheel)
        return wheels

    def _add_wheel(self, pos: Vec3, radius: float, front: bool, left):
        wheel_np = self.node_path.attachNewNode("wheel")
        if self.render:
            # TODO something wrong with the wheel render
            model_path = 'models/yugo/yugotireR.egg' if left else 'models/yugo/yugotireL.egg'
            wheel_model = self.loader.loadModel(
                AssetLoader.file_path(model_path))
            wheel_model.reparentTo(wheel_np)
            wheel_model.set_scale(1.4, radius / 0.25, radius / 0.25)
        wheel = self.system.create_wheel()
        wheel.setNode(wheel_np.node())
        wheel.setChassisConnectionPointCs(pos)
        wheel.setFrontWheel(front)
        wheel.setWheelDirectionCs(Vec3(0, 0, -1))
        wheel.setWheelAxleCs(Vec3(1, 0, 0))

        # TODO add them to PGConfig in the future
        wheel.setWheelRadius(radius)
        wheel.setMaxSuspensionTravelCm(40)
        wheel.setSuspensionStiffness(30)
        wheel.setWheelsDampingRelaxation(4.8)
        wheel.setWheelsDampingCompression(1.2)
        wheel.setFrictionSlip(self.vehicle_config["wheel_friction"])
        wheel.setRollInfluence(1.5)
        return wheel

    def add_image_sensor(self, name: str, sensor: ImageBuffer):
        self.image_sensors[name] = sensor

    def add_lidar(self, num_lasers=240, distance=50, show_lidar_point=False):
        assert num_lasers > 0
        assert distance > 0
        self.lidar = Lidar(self.pg_world.render, num_lasers, distance,
                           show_lidar_point)

    def add_routing_localization(self, show_navi_mark: bool = False):
        config = self.vehicle_config
        self.routing_localization = RoutingLocalizationModule(
            self.pg_world,
            show_navi_mark=show_navi_mark,
            random_navi_mark_color=config["random_navi_mark_color"],
            show_dest_mark=config["show_dest_mark"],
            show_line_to_dest=config["show_line_to_dest"])

    def update_map_info(self, map):
        """
        Update map info after reset()
        :param map: new map
        :return: None
        """
        lane, new_l_index = ray_localization(np.array(self.heading.tolist()),
                                             np.array(self.spawn_place),
                                             self.pg_world)
        self.routing_localization.update(map, current_lane_index=new_l_index)
        assert lane is not None, "spawn place is not on road!"
        self.lane_index = new_l_index
        self.lane = lane

    def _state_check(self):
        """
        Check States and filter to update info
        """
        result_1 = self.pg_world.physics_world.static_world.contactTest(
            self.chassis_beneath_np.node(), True)
        result_2 = self.pg_world.physics_world.dynamic_world.contactTest(
            self.chassis_beneath_np.node(), True)
        contacts = set()
        for contact in result_1.getContacts() + result_2.getContacts():
            node0 = contact.getNode0()
            node1 = contact.getNode1()
            name = [node0.getName(), node1.getName()]
            name.remove(BodyName.Base_vehicle_beneath)
            if name[0] == "Ground" or name[0] == BodyName.Lane:
                continue
            if name[0] == BodyName.Sidewalk:
                self.chassis_np.node().getPythonTag(
                    BodyName.Base_vehicle).crash_sidewalk = True
            elif name[0] == BodyName.White_continuous_line:
                self.chassis_np.node().getPythonTag(
                    BodyName.Base_vehicle).on_white_continuous_line = True
            elif name[0] == BodyName.Yellow_continuous_line:
                self.chassis_np.node().getPythonTag(
                    BodyName.Base_vehicle).on_yellow_continuous_line = True
            elif name[0] == BodyName.Broken_line:
                self.chassis_np.node().getPythonTag(
                    BodyName.Base_vehicle).on_broken_line = True
            contacts.add(name[0])
        if self.render:
            self.render_collision_info(contacts)

    @staticmethod
    def _init_collision_info_render(pg_world):
        if pg_world.mode == "onscreen":
            info_np = NodePath("Collision info nodepath")
            info_np.reparentTo(pg_world.aspect2d)
        else:
            info_np = None
        return info_np

    def render_collision_info(self, contacts):
        contacts = sorted(list(contacts),
                          key=lambda c: COLLISION_INFO_COLOR[COLOR[c]][0])
        text = contacts[0] if len(contacts) != 0 else None
        if text is None:
            text = "Normal" if time.time(
            ) - self.pg_world._episode_start_time > 10 else "Press H to see help message"
            self.render_banner(text, COLLISION_INFO_COLOR["green"][1])
        else:
            if text == BodyName.Base_vehicle_beneath:
                text = BodyName.Traffic_vehicle
            self.render_banner(text, COLLISION_INFO_COLOR[COLOR[text]][1])

    def render_banner(self, text, color=COLLISION_INFO_COLOR["green"][1]):
        """
        Render the banner in the left bottom corner.
        """
        if self.collision_info_np is None:
            return
        if self.current_banner is not None:
            self.current_banner.detachNode()
        if text in self.collision_banners:
            self.collision_banners[text].reparentTo(self.collision_info_np)
            self.current_banner = self.collision_banners[text]
        else:
            new_banner = NodePath(TextNode("collision_info:{}".format(text)))
            self.collision_banners[text] = new_banner
            text_node = new_banner.node()
            text_node.setCardColor(color)
            text_node.setText(text)
            text_node.setCardActual(-5 * self.pg_world.w_scale,
                                    5.1 * self.pg_world.w_scale, -0.3, 1)
            text_node.setCardDecal(True)
            text_node.setTextColor(1, 1, 1, 1)
            text_node.setAlign(TextNode.A_center)
            new_banner.setScale(0.05)
            new_banner.setPos(-0.75 * self.pg_world.w_scale, 0,
                              -0.8 * self.pg_world.h_scale)
            new_banner.reparentTo(self.collision_info_np)
            self.current_banner = new_banner

    def destroy(self, _=None):
        self.chassis_np.node().getPythonTag(BodyName.Base_vehicle).destroy()
        if self.chassis_np.node() in self.dynamic_nodes:
            self.dynamic_nodes.remove(self.chassis_np.node())
        super(BaseVehicle, self).destroy(self.pg_world)
        self.pg_world.physics_world.dynamic_world.clearContactAddedCallback()
        self.routing_localization.destroy()
        self.routing_localization = None

        if self.side_detector is not None:
            self.side_detector.destroy()

        if self.lane_line_detector is not None:
            self.lane_line_detector.destroy()

        self.side_detector = None
        self.lane_line_detector = None

        if self.lidar is not None:
            self.lidar.destroy()
            self.lidar = None
        if len(self.image_sensors) != 0:
            for sensor in self.image_sensors.values():
                sensor.destroy(self.pg_world)
        self.image_sensors = None
        if self.vehicle_panel is not None:
            self.vehicle_panel.destroy(self.pg_world)
        self.pg_world = None

    def set_position(self, position, height=0.4):
        """
        Should only be called when restore traffic from episode data
        :param position: 2d array or list
        :return: None
        """
        self.chassis_np.setPos(panda_position(position, height))

    def set_heading(self, heading_theta) -> None:
        """
        Should only be called when restore traffic from episode data
        :param heading_theta: float in rad
        :return: None
        """
        self.chassis_np.setH((panda_heading(heading_theta) * 180 / np.pi) - 90)

    def get_state(self):
        return {
            "heading":
            self.heading_theta,
            "position":
            self.position.tolist(),
            "done":
            self.crash_vehicle or self.out_of_route or self.crash_sidewalk
            or not self.on_lane
        }

    def set_state(self, state: dict):
        self.set_heading(state["heading"])
        self.set_position(state["position"])

    def _update_overtake_stat(self):
        if self.vehicle_config["overtake_stat"]:
            surrounding_vs = self.lidar.get_surrounding_vehicles()
            routing = self.routing_localization
            ckpt_idx = routing._target_checkpoints_index
            for surrounding_v in surrounding_vs:
                if surrounding_v.lane_index[:-1] == (
                        routing.checkpoints[ckpt_idx[0]],
                        routing.checkpoints[ckpt_idx[1]]):
                    if self.lane.local_coordinates(self.position)[0] - \
                            self.lane.local_coordinates(surrounding_v.position)[0] < 0:
                        self.front_vehicles.add(surrounding_v)
                        if surrounding_v in self.back_vehicles:
                            self.back_vehicles.remove(surrounding_v)
                    else:
                        self.back_vehicles.add(surrounding_v)
        return {"overtake_vehicle_num": self.get_overtake_num()}

    def get_overtake_num(self):
        return len(self.front_vehicles.intersection(self.back_vehicles))

    @classmethod
    def get_action_space_before_init(cls, extra_action_dim: int = 0):
        return gym.spaces.Box(-1.0,
                              1.0,
                              shape=(2 + extra_action_dim, ),
                              dtype=np.float32)

    def remove_display_region(self):
        if self.render:
            self.vehicle_panel.remove_display_region(self.pg_world)
            self.vehicle_panel.buffer.set_active(False)
            self.collision_info_np.detachNode()
            self.routing_localization._arrow_node_path.detachNode()
        for sensor in self.image_sensors.values():
            sensor.remove_display_region(self.pg_world)
            sensor.buffer.set_active(False)

    def add_to_display(self):
        if self.render:
            self.vehicle_panel.add_to_display(
                self.pg_world, self.vehicle_panel.default_region)
            self.vehicle_panel.buffer.set_active(True)
            self.collision_info_np.reparentTo(self.pg_world.aspect2d)
            self.routing_localization._arrow_node_path.reparentTo(
                self.pg_world.aspect2d)
        for sensor in self.image_sensors.values():
            sensor.add_to_display(self.pg_world, sensor.default_region)
            sensor.buffer.set_active(True)

    def __del__(self):
        super(BaseVehicle, self).__del__()
        self.pg_world = None
        self.lidar = None
        self.mini_map = None
        self.rgb_cam = None
        self.routing_localization = None
        self.wheels = None

    @property
    def arrive_destination(self):
        long, lat = self.routing_localization.final_lane.local_coordinates(
            self.position)
        flag = (self.routing_localization.final_lane.length - 5 < long <
                self.routing_localization.final_lane.length + 5) and (
                    self.routing_localization.get_current_lane_width() / 2 >=
                    lat >=
                    (0.5 - self.routing_localization.get_current_lane_num()) *
                    self.routing_localization.get_current_lane_width())
        return flag

    @property
    def crash_vehicle(self):
        return self.chassis_np.node().getPythonTag(
            BodyName.Base_vehicle).crash_vehicle

    @property
    def crash_object(self):
        return self.chassis_np.node().getPythonTag(
            BodyName.Base_vehicle).crash_object

    @property
    def crash_sidewalk(self):
        return self.chassis_np.node().getPythonTag(
            BodyName.Base_vehicle).crash_sidewalk

    @property
    def on_yellow_continuous_line(self):
        return self.chassis_np.node().getPythonTag(
            BodyName.Base_vehicle).on_yellow_continuous_line

    @property
    def on_white_continuous_line(self):
        return self.chassis_np.node().getPythonTag(
            BodyName.Base_vehicle).on_white_continuous_line

    @property
    def on_broken_line(self):
        return self.chassis_np.node().getPythonTag(
            BodyName.Base_vehicle).on_broken_line

    def set_static(self, flag):
        self.chassis_np.node().setStatic(flag)

    @property
    def reference_lanes(self):
        return self.routing_localization.current_ref_lanes

    def set_wheel_friction(self, new_friction):
        raise DeprecationWarning("Bug exists here")
        for wheel in self.wheels:
            wheel.setFrictionSlip(new_friction)
Exemplo n.º 3
0
class CarPhys(PhysColleague):
    def __init__(self, mediator, car_props, tuning, players):
        PhysColleague.__init__(self, mediator)
        self.pnode = self.vehicle = self.__track_phys = self.coll_mesh = \
            self.max_speed = self.friction_slip = \
            self.friction_slip_rear = self.cfg = None
        self.turbo = False
        self._tuning = tuning
        self._players = players
        self.roll_influence = []
        self.ai_meshes = []
        self.curr_speed_mul = 1.0
        self.roll_influence_k = self.friction_slip_k = 1.0
        self.__prev_speed = 0
        self.__last_drift_time = 0
        self.__finds = {}  # cache for find's results
        self.__whl2flytime = {}
        self.cprops = car_props
        self._load_phys()
        self.__set_collision_mesh()
        self.__set_ai_meshes()
        self.__set_phys_node()
        self.__set_vehicle()
        self.__set_wheels()
        self.eng.attach_obs(self.on_end_frame)

    def _load_phys(self):
        ppath = self.cprops.race_props.season_props.gameprops.phys_path
        fpath = ppath % self.cprops.name
        with open(fpath) as phys_file:
            self.cfg = load(phys_file)

        # they may be changed by drivers and tuning
        self.cfg['max_speed'] = self.get_speed()
        self.cfg['friction_slip'] = self.get_friction_static()[0]
        self.cfg['friction_slip_rear'] = self.get_friction_static()[1]
        self.cfg['roll_influence'] = self.get_roll_influence_static()
        self.friction_slip = self.cfg['friction_slip']
        self.friction_slip_rear = self.cfg['friction_slip_rear']
        self.__log_props()
        set_a = lambda field: setattr(self, field, self.cfg[field])
        list(map(set_a, self.cfg.keys()))

    def __log_props(self, starting=True):
        s_s = self.cfg['max_speed'] if starting else self.max_speed
        s_f = self.cfg['friction_slip'] if starting else \
            self.get_friction_static()[0]
        s_fr = self.cfg['friction_slip_rear'] if starting else \
            self.get_friction_static()[1]
        s_r = self.cfg['roll_influence'] if starting else \
            self.get_roll_influence_static()
        log_info = [('speed', self.cprops.name, round(s_s, 2),
                     self.cprops.driver_engine),
                    ('friction 0', self.cprops.name, round(s_f[0], 2),
                     self.cprops.driver_tires),
                    ('friction 1', self.cprops.name, round(s_f[1], 2),
                     self.cprops.driver_tires),
                    ('friction_rear 0', self.cprops.name, round(s_fr[0], 2),
                     self.cprops.driver_tires),
                    ('friction_rear 1', self.cprops.name, round(s_fr[1], 2),
                     self.cprops.driver_tires),
                    ('roll min', self.cprops.name, round(s_r[0], 2),
                     self.cprops.driver_suspensions),
                    ('roll max', self.cprops.name, round(s_r[1], 2),
                     self.cprops.driver_suspensions)]
        for l_i in log_info:
            info('%s %s: %s (%s)' % l_i)

    def __set_collision_mesh(self):
        fpath = self.cprops.race_props.coll_path % self.cprops.name
        try:
            self.coll_mesh = self.eng.load_model(fpath)
        except OSError:  # new cars don't have collision meshes
            self.coll_mesh = self.eng.load_model(
                fpath.replace('capsule', 'car'))
        # chassis_shape = BulletConvexHullShape()
        # for geom in self.eng.lib.find_geoms(
        #         self.coll_mesh, self.cprops.race_props.coll_name):
        #     chassis_shape.add_geom(geom.node().get_geom(0),
        #                            geom.get_transform())
        # self.mediator.gfx.nodepath.get_node().add_shape(chassis_shape)
        chassis_shape = BulletBoxShape(tuple(self.cfg['box_size']))
        boxpos = self.cfg['box_pos']
        boxpos[2] += self.cfg['center_mass_offset']
        pos = TransformState.makePos(tuple(boxpos))
        self.mediator.gfx.nodepath.p3dnode.add_shape(chassis_shape, pos)
        car_names = [player.car for player in self._players]
        car_idx = car_names.index(self.cprops.name)
        car_bit = BitMask32.bit(BitMasks.car(car_idx))
        ghost_bit = BitMask32.bit(BitMasks.ghost)
        track_bit = BitMask32.bit(BitMasks.track_merged)
        mask = car_bit | ghost_bit | track_bit
        self.mediator.gfx.nodepath.set_collide_mask(mask)

    def __set_ai_meshes(self):
        return
        # if we attach these meshes (or even only one mesh, box, sphere,
        # whatever) then the collision between the goal and the vehicle doesn't
        # work properly
        h = .5
        boxsz = self.cfg['box_size']
        hs = []
        hs += [
            h / 2 + boxsz[2] * 2 + self.cfg['box_pos'][2] +
            self.cfg['center_mass_offset']
        ]
        hs += [
            -h / 2 - boxsz[2] * 2 + self.cfg['box_pos'][2] +
            self.cfg['center_mass_offset']
        ]
        for _h in hs:
            shape = BulletBoxShape((boxsz[0], boxsz[1], h))
            ghost = GhostNode('Vehicle')
            pos = TransformState.makePos((0, 0, _h))
            ghost.node.addShape(shape, pos)
            self.ai_meshes += [self.eng.attach_node(ghost.node)]
            car_names = self.cprops.race_props.season_props.car_names
            car_idx = car_names.index(self.cprops.name)
            car_bit = BitMask32.bit(BitMasks.car(car_idx))
            ghost_bit = BitMask32.bit(BitMasks.ghost)
            mask = car_bit | ghost_bit
            self.ai_meshes[-1].set_collide_mask(mask)
            self.eng.phys_mgr.attach_ghost(ghost.node)

    def __set_phys_node(self):
        self.pnode = self.mediator.gfx.nodepath.p3dnode
        self.pnode.set_mass(self.mass)  # default 0
        self.pnode.set_deactivation_enabled(False)
        self.eng.phys_mgr.attach_rigid_body(self.pnode)
        self.eng.phys_mgr.add_collision_obj(self.pnode)

    def __set_vehicle(self):
        self.vehicle = BulletVehicle(self.eng.phys_mgr.root._wld, self.pnode)
        # access to a protected member
        self.vehicle.set_coordinate_system(ZUp)
        self.vehicle.set_pitch_control(self.pitch_control)
        tuning = self.vehicle.get_tuning()
        tuning.set_suspension_compression(self.suspension_compression)
        # default .83
        tuning.set_suspension_damping(self.suspension_damping)
        # default .88
        self.eng.phys_mgr.attach_vehicle(self.vehicle)

    def __set_wheels(self):
        wheels = self.mediator.gfx.wheels
        f_bounds = wheels['fr'].tight_bounds
        f_radius = (f_bounds[1][2] - f_bounds[0][2]) / 2.0 + .01
        r_bounds = wheels['rr'].tight_bounds
        r_radius = (r_bounds[1][2] - r_bounds[0][2]) / 2.0 + .01
        wheel_names = self.cprops.race_props.wheel_names
        ffr = self.coll_mesh.find('**/' + wheel_names.frontrear.fr)
        ffl = self.coll_mesh.find('**/' + wheel_names.frontrear.fl)
        rrr = self.coll_mesh.find('**/' + wheel_names.frontrear.rr)
        rrl = self.coll_mesh.find('**/' + wheel_names.frontrear.rl)
        meth = self.coll_mesh.find
        fr_node = ffr if ffr else meth('**/' + wheel_names.both.fr)
        fl_node = ffl if ffl else meth('**/' + wheel_names.both.fl)
        rr_node = rrr if rrr else meth('**/' + wheel_names.both.rr)
        rl_node = rrl if rrl else meth('**/' + wheel_names.both.rl)
        if not fr_node:  # new cars
            fr_node = meth('**/w_fr')
            fl_node = meth('**/w_fl')
            rr_node = meth('**/w_rr')
            rl_node = meth('**/w_rl')
        offset = self.cfg['center_mass_offset']
        fr_pos = fr_node.get_pos() + (0, 0, f_radius + offset)
        fl_pos = fl_node.get_pos() + (0, 0, f_radius + offset)
        rr_pos = rr_node.get_pos() + (0, 0, r_radius + offset)
        rl_pos = rl_node.get_pos() + (0, 0, r_radius + offset)
        wheels_info = [(fr_pos, True, wheels['fr'], f_radius),
                       (fl_pos, True, wheels['fl'], f_radius),
                       (rr_pos, False, wheels['rr'], r_radius),
                       (rl_pos, False, wheels['rl'], r_radius)]
        for i, (pos, front, nodepath, radius) in enumerate(wheels_info):
            self.__add_wheel(pos, front, nodepath.p3dnode, radius, i)

    def __add_wheel(self, pos, is_front, node, radius, i):
        whl = self.vehicle.create_wheel()
        whl.set_node(node)
        whl.set_chassis_connection_point_cs(LPoint3f(*pos))
        whl.set_front_wheel(is_front)
        whl.set_wheel_direction_cs((0, 0, -1))
        whl.set_wheel_axle_cs((1, 0, 0))
        whl.set_wheel_radius(radius)
        whl.set_suspension_stiffness(self.suspension_stiffness[0])
        # default 5.88
        whl.set_wheels_damping_relaxation(self.wheels_damping_relaxation[0])
        # default .88
        whl.set_wheels_damping_compression(self.wheels_damping_compression[0])
        # default .83
        idx = 0 if is_front else 1
        whl.set_friction_slip(self.get_friction_static()[idx][0])
        # default 10.5
        # friction slip high -> more adherence
        whl.set_roll_influence(self.roll_influence[0])
        # low ->  more stability  # default .1
        whl.set_max_suspension_force(self.max_suspension_force)
        # default 6000
        whl.set_max_suspension_travel_cm(self.max_suspension_travel_cm)
        # default 500
        whl.set_skid_info(self.skid_info)  # default 0
        self.__whl2flytime[i] = 0

    @property
    def lateral_force(self):
        vel = self.vehicle.get_chassis().get_linear_velocity()
        rot_mat = Mat4()
        rot_mat.setRotateMat(-90, (0, 0, 1))
        car_lat = rot_mat.xformVec(self.mediator.logic.car_vec._vec)
        # access to a protected member
        proj_frc = vel.project(car_lat)
        return proj_frc.length()

    @property
    def lin_vel(self):
        return self.vehicle.get_chassis().get_linear_velocity().length() * 3.6

    @property
    def is_flying(self):  # no need to be cached
        rays = [whl.get_raycast_info() for whl in self.vehicle.get_wheels()]
        return not any(ray.is_in_contact() for ray in rays)

    @property
    def is_drifting(self):
        return self.lateral_force > 4.0

    @property
    def last_drift_time(self):
        return self.__last_drift_time

    @property
    def prev_speed(self):
        return self.__prev_speed

    @property
    def prev_speed_ratio(self):
        return max(0, min(1.0, self.prev_speed / self.max_speed))

    def on_end_frame(self):
        self.__prev_speed = self.speed

    @property
    def speed(self):
        if self.mediator.fsm.getCurrentOrNextState() == 'Countdown':
            return 0  # getCurrentSpeedKmHour returns odd values otherwise
        return self.vehicle.get_current_speed_km_hour()

    @property
    def speed_ratio(self):
        return max(0, min(1.0, self.speed / self.max_speed))

    @property
    def lin_vel_ratio(self):
        return max(0, min(1.0, self.lin_vel / self.max_speed))

    def set_forces(self, eng_frc, brake_frc, brk_ratio, steering):
        idx = 1 if self.mediator.logic.is_drifting else 0
        eng_frc_ratio = self.engine_acc_frc_ratio[idx]
        self.vehicle.set_steering_value(steering, 0)
        self.vehicle.set_steering_value(steering, 1)
        self.vehicle.apply_engine_force(eng_frc * eng_frc_ratio, 0)
        self.vehicle.apply_engine_force(eng_frc * eng_frc_ratio, 1)
        self.vehicle.apply_engine_force(eng_frc * (1 - eng_frc_ratio), 2)
        self.vehicle.apply_engine_force(eng_frc * (1 - eng_frc_ratio), 3)
        self.vehicle.set_brake((1 - brk_ratio) * brake_frc, 2)
        self.vehicle.set_brake((1 - brk_ratio) * brake_frc, 3)
        self.vehicle.set_brake(brk_ratio * brake_frc, 0)
        self.vehicle.set_brake(brk_ratio * brake_frc, 1)

    def update_car_props(self):
        wheels = zip(self.vehicle.get_wheels(), range(4))
        speeds = list(map(lambda whi: self.__update_whl_props(*whi), wheels))
        speeds = [speed for speed in speeds if speed]
        if self.is_drifting:
            self.__last_drift_time = self.eng.curr_time
        self.curr_speed_mul = (sum(speeds) / len(speeds)) if speeds else 1.0

    def __update_whl_props(self, whl, i):
        susp_min = self.suspension_stiffness[0]
        susp_max = self.suspension_stiffness[1]
        susp_diff = susp_max - susp_min
        whl.set_suspension_stiffness(susp_min + self.speed_ratio * susp_diff)
        relax_min = self.wheels_damping_relaxation[0]
        relax_max = self.wheels_damping_relaxation[1]
        relax_diff = relax_max - relax_min
        relax = relax_min + self.speed_ratio * relax_diff
        whl.set_wheels_damping_relaxation(relax)
        compr_min = self.wheels_damping_compression[0]
        compr_max = self.wheels_damping_compression[1]
        compr_diff = compr_max - compr_min
        compr = compr_min + self.speed_ratio * compr_diff
        whl.set_wheels_damping_compression(compr)
        roll_infl_min = self.roll_influence[0]
        roll_infl_max = self.roll_influence[1]
        roll_infl_diff = roll_infl_max - roll_infl_min
        roll_infl = roll_infl_min + self.speed_ratio * roll_infl_diff
        whl.set_roll_influence(self.roll_influence_k * roll_infl)
        contact_pt = whl.get_raycast_info().getContactPointWs()
        gnd_name = self.gnd_name(contact_pt)
        if not gnd_name or gnd_name in ['Vehicle', 'Wall', 'Respawn']:
            return
        if gnd_name not in self.__finds:
            gnd = self.cprops.race.track.phys.model.find('**/' + gnd_name)
            self.__finds[gnd_name] = gnd
        gfx_node = self.__finds[gnd_name]
        if not gfx_node:
            print('ground error', gnd_name)
            return
        fric = 1.0
        if gfx_node.has_tag('friction'):
            fric = float(gfx_node.get_tag('friction'))
        if not whl.get_raycast_info().is_in_contact():
            self.__whl2flytime[i] = self.eng.curr_time
        gnd_time = self.eng.curr_time - self.__whl2flytime[i]
        gnd_recovery_time = .2 if whl.is_front_wheel() else .1
        gnd_factor = min(1, gnd_time / gnd_recovery_time)
        idx = 0 if whl.is_front_wheel() else 1
        turbo_factor = 1.24 if self.turbo else 1.0
        whl.setFrictionSlip(self.get_friction()[idx] * fric * gnd_factor *
                            turbo_factor)
        if gfx_node.has_tag('speed'):
            return float(gfx_node.get_tag('speed'))

    @property
    def gnd_names(self):  # no need to be cached
        whls = self.vehicle.get_wheels()
        pos = list(
            map(lambda whl: whl.get_raycast_info().get_contact_point_ws(),
                whls))
        return list(map(self.gnd_name, pos))

    @staticmethod
    def gnd_name(pos):
        top, bottom = pos + (0, 0, 20), pos + (0, 0, -20)
        result = CarPhys.eng.phys_mgr.ray_test_closest(bottom, top)
        ground = result.get_node()
        return ground.get_name() if ground else ''

    @staticmethod
    def gnd_height(pos):  # this should be a method of the track
        top, bottom = pos + (0, 0, 20), pos + (0, 0, -20)
        result = CarPhys.eng.phys_mgr.ray_test_closest(bottom, top)
        hit_pos = result.get_hit_pos()
        return hit_pos.z if hit_pos else None

    def apply_damage(self, reset=False):
        # wheels = self.vehicle.get_wheels()
        if reset:
            self.max_speed = self.get_speed()
            self.friction_slip_k = 1.0
            self.roll_influence_k = 1.0
        else:
            self.max_speed *= .95
            self.friction_slip_k *= .95
            self.roll_influence_k *= 1.05
        # map(lambda whl: whl.set_friction_slip(self.friction_slip), wheels)
        self.__log_props(False)

    def get_speed(self):
        return self.cfg['max_speed'] * (1 + .01 * self.cprops.driver_engine)

    def get_friction(self):
        k = (1 + .01 * self.cprops.driver_tires)
        return self.friction_slip[0] * k, self.friction_slip_rear[0] * k

    def get_roll_influence_static(self):
        min_r = self.cfg['roll_influence'][0]
        max_r = self.cfg['roll_influence'][1]
        k = 1 + .01 * self.cprops.driver_suspensions
        return [min_r * k, max_r * k]

    def get_friction_static(self):
        k = 1 + .01 * self.cprops.driver_tires
        fstr = 'friction_slip'
        return [(self.cfg[fstr][0] * k, self.cfg[fstr][1] * k),
                (self.cfg[fstr + '_rear'][0] * k,
                 self.cfg[fstr + '_rear'][1] * k)]

    def get_roll_influence(self):
        min_r = self.cfg['roll_influence'][0]
        max_r = self.cfg['roll_influence'][1]
        diff_r = max_r - min_r
        curr_r = min_r + self.speed_ratio * diff_r
        return curr_r * (1 + .01 * self.cprops.driver_suspensions)

    def rotate(self):
        self.pnode.apply_torque((0, 0, 80000))
        self.mediator.logic.applied_torque = True

    def destroy(self):
        self.eng.detach_obs(self.on_end_frame)
        self.eng.phys_mgr.remove_vehicle(self.vehicle)
        self.pnode = self.vehicle = self.__finds = self.__track_phys = \
            self.coll_mesh = None
        PhysColleague.destroy(self)
Exemplo n.º 4
0
class BaseVehicle(DynamicElement):
    Ego_state_obs_dim = 9
    """
    Vehicle chassis and its wheels index
                    0       1
                    II-----II
                        |
                        |  <---chassis
                        |
                    II-----II
                    2       3
    """
    PARAMETER_SPACE = PGSpace(
        VehicleParameterSpace.BASE_VEHICLE
    )  # it will not sample config from parameter space
    COLLISION_MASK = CollisionGroup.EgoVehicle
    STEERING_INCREMENT = 0.05

    default_vehicle_config = PGConfig(
        dict(
            lidar=(240, 50, 4),  # laser num, distance, other vehicle info num
            mini_map=(84, 84, 250),  # buffer length, width
            rgb_cam=(160, 120),  # buffer length, width
            depth_cam=(84, 84, True),  # buffer length, width, view_ground
            show_navi_mark=False,
            increment_steering=False,
            wheel_friction=0.6,
        ))

    born_place = (5, 0)
    LENGTH = None
    WIDTH = None

    def __init__(self,
                 pg_world: PGWorld,
                 vehicle_config: dict = None,
                 random_seed: int = 0,
                 config: dict = None):
        """
        This Vehicle Config is different from self.get_config(), and it is used to define which modules to use, and
        module parameters.
        """
        super(BaseVehicle, self).__init__(random_seed)
        self.pg_world = pg_world
        self.node_path = NodePath("vehicle")

        # config info
        self.set_config(self.PARAMETER_SPACE.sample())
        if config is not None:
            self.set_config(config)

        self.vehicle_config = self.get_vehicle_config(
            vehicle_config
        ) if vehicle_config is not None else self.default_vehicle_config
        self.increment_steering = self.vehicle_config["increment_steering"]
        self.max_speed = self.get_config()[Parameter.speed_max]
        self.max_steering = self.get_config()[Parameter.steering_max]

        # create
        self._add_chassis(pg_world.physics_world)
        self.wheels = self._create_wheel()

        # modules
        self.image_sensors = {}
        self.lidar = None
        self.routing_localization = None
        self.lane = None
        self.lane_index = None

        self.vehicle_panel = VehiclePanel(self.pg_world) if (
            self.pg_world.mode == RENDER_MODE_ONSCREEN) else None

        # other info
        self.throttle_brake = 0.0
        self.steering = 0
        self.last_current_action = deque([(0.0, 0.0), (0.0, 0.0)], maxlen=2)
        self.last_position = self.born_place
        self.last_heading_dir = self.heading

        # collision info render
        self.collision_info_np = self._init_collision_info_render(pg_world)
        self.collision_banners = {}  # to save time
        self.current_banner = None

        # done info
        self.crash = False
        self.out_of_road = False

        self.attach_to_pg_world(self.pg_world.pbr_render,
                                self.pg_world.physics_world)

    @classmethod
    def get_vehicle_config(cls, new_config):
        default = copy.deepcopy(cls.default_vehicle_config)
        default.update(new_config)
        return default

    def prepare_step(self, action):
        """
        Save info and make decision before action
        """
        self.last_position = self.position
        self.last_heading_dir = self.heading
        self.last_current_action.append(
            action
        )  # the real step of physics world is implemented in taskMgr.step()
        if self.increment_steering:
            self.set_incremental_action(action)
        else:
            self.set_act(action)
        if self.vehicle_panel is not None:
            self.vehicle_panel.renew_2d_car_para_visualization(self)

    def update_state(self, pg_world=None):
        if self.lidar is not None:
            self.lidar.perceive(self.position, self.heading_theta,
                                self.pg_world.physics_world)
        if self.routing_localization is not None:
            self.lane, self.lane_index = self.routing_localization.update_navigation_localization(
                self)
        return self._state_check()

    def reset(self, map: Map, pos: np.ndarray, heading: float):
        """
        pos is a 2-d array, and heading is a float (unit degree)
        """
        heading = -np.deg2rad(heading) - np.pi / 2
        self.chassis_np.setPos(Vec3(*pos, 1))
        self.chassis_np.setQuat(
            LQuaternionf(np.cos(heading / 2), 0, 0, np.sin(heading / 2)))
        self.update_map_info(map)
        self.chassis_np.node().clearForces()
        self.chassis_np.node().setLinearVelocity(Vec3(0, 0, 0))
        self.chassis_np.node().setAngularVelocity(Vec3(0, 0, 0))
        self.system.resetSuspension()

        # done info
        self.crash = False
        self.out_of_road = False

        # other info
        self.throttle_brake = 0.0
        self.steering = 0
        self.last_current_action = deque([(0.0, 0.0), (0.0, 0.0)], maxlen=2)
        self.last_position = self.born_place
        self.last_heading_dir = self.heading

        if "depth_cam" in self.image_sensors and self.image_sensors[
                "depth_cam"].view_ground:
            for block in map.blocks:
                block.node_path.hide(CamMask.DepthCam)

    """------------------------------------------- act -------------------------------------------------"""

    def set_act(self, action):
        para = self.get_config()
        steering = action[0]
        self.throttle_brake = action[1]
        self.steering = steering
        self.system.setSteeringValue(
            self.steering * para[Parameter.steering_max], 0)
        self.system.setSteeringValue(
            self.steering * para[Parameter.steering_max], 1)
        self._apply_throttle_brake(action[1])

    def set_incremental_action(self, action: np.ndarray):
        self.throttle_brake = action[1]
        self.steering += action[0] * self.STEERING_INCREMENT
        self.steering = clip(self.steering, -1, 1)
        steering = self.steering * self.max_steering
        self.system.setSteeringValue(steering, 0)
        self.system.setSteeringValue(steering, 1)
        self._apply_throttle_brake(action[1])

    def _apply_throttle_brake(self, throttle_brake):
        para = self.get_config()
        max_engine_force = para[Parameter.engine_force_max]
        max_brake_force = para[Parameter.brake_force_max]
        for wheel_index in range(4):
            if throttle_brake >= 0:
                self.system.setBrake(2.0, wheel_index)
                if self.speed > self.max_speed:
                    self.system.applyEngineForce(0.0, wheel_index)
                else:
                    self.system.applyEngineForce(
                        max_engine_force * throttle_brake, wheel_index)
            else:
                self.system.applyEngineForce(0.0, wheel_index)
                self.system.setBrake(
                    abs(throttle_brake) * max_brake_force, wheel_index)

    """---------------------------------------- vehicle info ----------------------------------------------"""

    @property
    def position(self):
        return pgdrive_position(self.chassis_np.getPos())

    @property
    def speed(self):
        """
        km/h
        """
        speed = self.chassis_np.node().get_linear_velocity().length() * 3.6
        return clip(speed, 0.0, 100000.0)

    @property
    def heading(self):
        real_heading = self.heading_theta
        heading = np.array([np.cos(real_heading), np.sin(real_heading)])
        return heading

    @property
    def heading_theta(self):
        """
        Get the heading theta of vehicle, unit [rad]
        :return:  heading in rad
        """
        return (pgdrive_heading(self.chassis_np.getH()) - 90) / 180 * math.pi

    @property
    def velocity(self) -> np.ndarray:
        return self.speed * self.velocity_direction

    @property
    def velocity_direction(self):
        direction = self.system.get_forward_vector()
        return np.asarray([direction[0], -direction[1]])

    @property
    def forward_direction(self):
        raise ValueError("This function id deprecated.")
        # print("This function id deprecated.")
        # direction = self.vehicle.get_forward_vector()
        # return np.array([direction[0], -direction[1]])

    @property
    def current_road(self):
        return self.lane_index[0:-1]

    """---------------------------------------- some math tool ----------------------------------------------"""

    def heading_diff(self, target_lane):
        lateral = None
        if isinstance(target_lane, StraightLane):
            lateral = np.asarray(
                get_vertical_vector(target_lane.end - target_lane.start)[1])
        elif isinstance(target_lane, CircularLane):
            if target_lane.direction == -1:
                lateral = self.position - target_lane.center
            else:
                lateral = target_lane.center - self.position
        else:
            raise ValueError("Unknown target lane type: {}".format(
                type(target_lane)))
        lateral_norm = norm(lateral[0], lateral[1])
        forward_direction = self.heading
        # print(f"Old forward direction: {self.forward_direction}, new heading {self.heading}")
        forward_direction_norm = norm(forward_direction[0],
                                      forward_direction[1])
        if not lateral_norm * forward_direction_norm:
            return 0
        # cos = self.forward_direction.dot(lateral) / (np.linalg.norm(lateral) * np.linalg.norm(self.forward_direction))
        cos = ((forward_direction[0] * lateral[0] +
                forward_direction[1] * lateral[1]) /
               (lateral_norm * forward_direction_norm))
        # return cos
        # Normalize to 0, 1
        return clip(cos, -1.0, 1.0) / 2 + 0.5

    def projection(self, vector):
        # Projected to the heading of vehicle
        # forward_vector = self.vehicle.get_forward_vector()
        # forward_old = (forward_vector[0], -forward_vector[1])

        forward = self.heading

        # print(f"[projection] Old forward {forward_old}, new heading {forward}")

        norm_velocity = norm(forward[0], forward[1]) + 1e-6
        project_on_heading = (vector[0] * forward[0] +
                              vector[1] * forward[1]) / norm_velocity

        side_direction = get_vertical_vector(forward)[1]
        side_norm = norm(side_direction[0], side_direction[1]) + 1e-6
        project_on_side = (vector[0] * side_direction[0] +
                           vector[1] * side_direction[1]) / side_norm
        return project_on_heading, project_on_side

    def lane_distance_to(self, vehicle, lane: AbstractLane = None) -> float:
        assert self.routing_localization is not None, "a routing and localization module should be added " \
                                                      "to interact with other vehicles"
        if not vehicle:
            return np.nan
        if not lane:
            lane = self.lane
        return lane.local_coordinates(
            vehicle.position)[0] - lane.local_coordinates(self.position)[0]

    """-------------------------------------- for vehicle making ------------------------------------------"""

    def _add_chassis(self, pg_physics_world: PGPhysicsWorld):
        para = self.get_config()
        chassis = BulletRigidBodyNode(BodyName.Ego_vehicle_top)
        chassis.setIntoCollideMask(BitMask32.bit(self.COLLISION_MASK))
        chassis_shape = BulletBoxShape(
            Vec3(para[Parameter.vehicle_width] / 2,
                 para[Parameter.vehicle_length] / 2,
                 para[Parameter.vehicle_height] / 2))
        ts = TransformState.makePos(
            Vec3(0, 0, para[Parameter.chassis_height] * 2))
        chassis.addShape(chassis_shape, ts)
        heading = np.deg2rad(-para[Parameter.heading] - 90)
        chassis.setMass(para[Parameter.mass])
        self.chassis_np = self.node_path.attachNewNode(chassis)
        # not random born now
        self.chassis_np.setPos(Vec3(*self.born_place, 1))
        self.chassis_np.setQuat(
            LQuaternionf(np.cos(heading / 2), 0, 0, np.sin(heading / 2)))
        chassis.setDeactivationEnabled(False)
        chassis.notifyCollisions(True)  # advance collision check
        self.pg_world.physics_world.dynamic_world.setContactAddedCallback(
            PythonCallbackObject(self._collision_check))
        self.dynamic_nodes.append(chassis)

        chassis_beneath = BulletGhostNode(BodyName.Ego_vehicle)
        chassis_beneath.setIntoCollideMask(BitMask32.bit(self.COLLISION_MASK))
        chassis_beneath.addShape(chassis_shape)
        self.chassis_beneath_np = self.chassis_np.attachNewNode(
            chassis_beneath)
        self.dynamic_nodes.append(chassis_beneath)

        self.system = BulletVehicle(pg_physics_world.dynamic_world, chassis)
        self.system.setCoordinateSystem(ZUp)
        self.dynamic_nodes.append(
            self.system
        )  # detach chassis will also detach system, so a waring will generate
        self.LENGTH = para[Parameter.vehicle_length]
        self.WIDTH = para[Parameter.vehicle_width]

        if self.render:
            model_path = 'models/ferra/scene.gltf'
            self.chassis_vis = self.loader.loadModel(
                AssetLoader.file_path(model_path))
            self.chassis_vis.setZ(para[Parameter.vehicle_vis_z])
            self.chassis_vis.setY(para[Parameter.vehicle_vis_y])
            self.chassis_vis.setH(para[Parameter.vehicle_vis_h])
            self.chassis_vis.set_scale(para[Parameter.vehicle_vis_scale])
            self.chassis_vis.reparentTo(self.chassis_np)

    def _create_wheel(self):
        para = self.get_config()
        f_l = para[Parameter.front_tire_longitude]
        r_l = -para[Parameter.rear_tire_longitude]
        lateral = para[Parameter.tire_lateral]
        axis_height = para[Parameter.tire_radius] + 0.05
        radius = para[Parameter.tire_radius]
        wheels = []
        for k, pos in enumerate([
                Vec3(lateral, f_l, axis_height),
                Vec3(-lateral, f_l, axis_height),
                Vec3(lateral, r_l, axis_height),
                Vec3(-lateral, r_l, axis_height)
        ]):
            wheel = self._add_wheel(pos, radius, True if k < 2 else False,
                                    True if k == 0 or k == 2 else False)
            wheels.append(wheel)
        return wheels

    def _add_wheel(self, pos: Vec3, radius: float, front: bool, left):
        wheel_np = self.node_path.attachNewNode("wheel")
        if self.render:
            model_path = 'models/yugo/yugotireR.egg' if left else 'models/yugo/yugotireL.egg'
            wheel_model = self.loader.loadModel(
                AssetLoader.file_path(model_path))
            wheel_model.reparentTo(wheel_np)
            wheel_model.set_scale(1.4, radius / 0.25, radius / 0.25)
        wheel = self.system.create_wheel()
        wheel.setNode(wheel_np.node())
        wheel.setChassisConnectionPointCs(pos)
        wheel.setFrontWheel(front)
        wheel.setWheelDirectionCs(Vec3(0, 0, -1))
        wheel.setWheelAxleCs(Vec3(1, 0, 0))

        # TODO add them to PGConfig in the future
        wheel.setWheelRadius(radius)
        wheel.setMaxSuspensionTravelCm(40)
        wheel.setSuspensionStiffness(30)
        wheel.setWheelsDampingRelaxation(4.8)
        wheel.setWheelsDampingCompression(1.2)
        wheel.setFrictionSlip(self.vehicle_config["wheel_friction"])
        wheel.setRollInfluence(1.5)
        return wheel

    def add_image_sensor(self, name: str, sensor: ImageBuffer):
        self.image_sensors[name] = sensor

    def add_lidar(self, laser_num=240, distance=50):
        self.lidar = Lidar(self.pg_world.render, laser_num, distance)

    def add_routing_localization(self, show_navi_point: bool):
        self.routing_localization = RoutingLocalizationModule(
            self.pg_world, show_navi_point)

    def update_map_info(self, map):
        """
        Update map info after reset()
        :param map: new map
        :return: None
        """
        self.routing_localization.update(map)
        lane, new_l_index = ray_localization((self.born_place), self.pg_world)
        assert lane is not None, "Born place is not on road!"
        self.lane_index = new_l_index
        self.lane = lane

    def _state_check(self):
        """
        Check States and filter to update info
        """
        result = self.pg_world.physics_world.dynamic_world.contactTest(
            self.chassis_beneath_np.node(), True)
        contacts = set()
        for contact in result.getContacts():
            node0 = contact.getNode0()
            node1 = contact.getNode1()
            name = [node0.getName(), node1.getName()]
            name.remove(BodyName.Ego_vehicle)
            if name[0] == "Ground" or name[0] == BodyName.Lane:
                continue
            elif name[0] == BodyName.Side_walk:
                self.out_of_road = True
            contacts.add(name[0])
        if self.render:
            self.render_collision_info(contacts)

        return contacts

    def _collision_check(self, contact):
        """
        It may lower the performance if overdone
        """
        node0 = contact.getNode0().getName()
        node1 = contact.getNode1().getName()
        name = [node0, node1]
        name.remove(BodyName.Ego_vehicle_top)
        if name[0] == BodyName.Traffic_vehicle:
            self.crash = True
            logging.debug("Crash with {}".format(name[0]))

    def _init_collision_info_render(self, pg_world):
        if pg_world.mode == "onscreen":
            info_np = NodePath("Collision info nodepath")
            info_np.reparentTo(pg_world.aspect2d)
        else:
            info_np = None
        return info_np

    def render_collision_info(self, contacts):
        contacts = sorted(list(contacts),
                          key=lambda c: COLLISION_INFO_COLOR[COLOR[c]][0])
        text = contacts[0] if len(contacts) != 0 else None
        if text is None:
            text = "Normal" if time.time(
            ) - self.pg_world._episode_start_time > 10 else "Press H to see help message"
            self.render_banner(text, COLLISION_INFO_COLOR["green"][1])
        else:
            self.render_banner(text, COLLISION_INFO_COLOR[COLOR[text]][1])

    def render_banner(self, text, color=COLLISION_INFO_COLOR["green"][1]):
        """
        Render the banner in the left bottom corner.
        """
        if self.collision_info_np is None:
            return
        if self.current_banner is not None:
            self.current_banner.detachNode()
        if text in self.collision_banners:
            self.collision_banners[text].reparentTo(self.collision_info_np)
            self.current_banner = self.collision_banners[text]
        else:
            new_banner = NodePath(TextNode("collision_info:{}".format(text)))
            self.collision_banners[text] = new_banner
            text_node = new_banner.node()
            text_node.setCardColor(color)
            text_node.setText(text)
            text_node.setCardActual(-5 * self.pg_world.w_scale,
                                    5.1 * self.pg_world.w_scale, -0.3, 1)
            text_node.setCardDecal(True)
            text_node.setTextColor(1, 1, 1, 1)
            text_node.setAlign(TextNode.A_center)
            new_banner.setScale(0.05)
            new_banner.setPos(-0.75 * self.pg_world.w_scale, 0,
                              -0.8 * self.pg_world.h_scale)
            new_banner.reparentTo(self.collision_info_np)
            self.current_banner = new_banner

    def destroy(self, _=None):
        self.dynamic_nodes.remove(self.chassis_np.node())
        super(BaseVehicle, self).destroy(self.pg_world)
        self.pg_world.physics_world.dynamic_world.clearContactAddedCallback()
        self.routing_localization.destroy()
        self.routing_localization = None
        if self.lidar is not None:
            self.lidar.destroy()
            self.lidar = None
        if len(self.image_sensors) != 0:
            for sensor in self.image_sensors.values():
                sensor.destroy(self.pg_world)
        self.image_sensors = None
        if self.vehicle_panel is not None:
            self.vehicle_panel.destroy(self.pg_world)
        self.pg_world = None

    def set_position(self, position):
        """
        Should only be called when restore traffic from episode data
        :param position: 2d array or list
        :return: None
        """
        self.chassis_np.setPos(panda_position(position, 0.4))

    def set_heading(self, heading_theta) -> None:
        """
        Should only be called when restore traffic from episode data
        :param heading_theta: float in rad
        :return: None
        """
        self.chassis_np.setH((panda_heading(heading_theta) * 180 / np.pi) - 90)

    def get_state(self):
        return {
            "heading": self.heading_theta,
            "position": self.position.tolist(),
            "done": self.crash or self.out_of_road
        }

    def set_state(self, state: dict):
        self.set_heading(state["heading"])
        self.set_position(state["position"])

    def __del__(self):
        super(BaseVehicle, self).__del__()
        self.pg_world = None
        self.lidar = None
        self.mini_map = None
        self.rgb_cam = None
        self.routing_localization = None
        self.wheels = None
Exemplo n.º 5
0
class Game(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)
        base.set_background_color(0.1, 0.1, 0.8, 1)
        base.set_frame_rate_meter(True)

        base.cam.set_pos(0, -20, 4)
        base.cam.look_at(0, 0, 0)

        # Light
        alight = AmbientLight('ambientLight')
        alight.set_color(LVector4(0.5, 0.5, 0.5, 1))
        alightNP = render.attach_new_node(alight)

        dlight = DirectionalLight('directionalLight')
        dlight.set_direction(LVector3(1, 1, -1))
        dlight.set_color(LVector4(0.7, 0.7, 0.7, 1))
        dlightNP = render.attach_new_node(dlight)

        render.clear_light()
        render.set_light(alightNP)
        render.set_light(dlightNP)

        # Input
        self.accept('escape', self.do_exit)
        self.accept('r', self.do_reset)
        self.accept('f1', base.toggle_wireframe)
        self.accept('f2', base.toggle_texture)
        self.accept('f3', self.toggle_debug)
        self.accept('f5', self.do_screenshot)

        inputState.watchWithModifiers('forward', 'w')
        inputState.watchWithModifiers('reverse', 's')
        inputState.watchWithModifiers('turnLeft', 'a')
        inputState.watchWithModifiers('turnRight', 'd')

        # Task
        taskMgr.add(self.update, 'updateWorld')

        # Physics
        self.setup()

    # _____HANDLER_____

    def do_exit(self):
        self.cleanup()
        sys.exit(1)

    def do_reset(self):
        self.cleanup()
        self.setup()

    def toggle_debug(self):
        if self.debugNP.is_hidden():
            self.debugNP.show()
        else:
            self.debugNP.hide()

    def do_screenshot(self):
        base.screenshot('Bullet')

    # ____TASK___

    def process_input(self, dt):
        engineForce = 0.0
        brakeForce = 0.0

        if inputState.isSet('forward'):
            engineForce = 1000.0
            brakeForce = 0.0

        if inputState.isSet('reverse'):
            engineForce = 0.0
            brakeForce = 100.0

        if inputState.isSet('turnLeft'):
            self.steering += dt * self.steeringIncrement
            self.steering = min(self.steering, self.steeringClamp)

        if inputState.isSet('turnRight'):
            self.steering -= dt * self.steeringIncrement
            self.steering = max(self.steering, -self.steeringClamp)

        # Apply steering to front wheels
        self.vehicle.setSteeringValue(self.steering, 0);
        self.vehicle.setSteeringValue(self.steering, 1);

        # Apply engine and brake to rear wheels
        self.vehicle.applyEngineForce(engineForce, 2);
        self.vehicle.applyEngineForce(engineForce, 3);
        self.vehicle.setBrake(brakeForce, 2);
        self.vehicle.setBrake(brakeForce, 3);

    def update(self, task):
        dt = globalClock.get_dt()
        self.process_input(dt)
        self.world.do_physics(dt, 10, 0.008)
        #print self.vehicle.getWheel(0).getRaycastInfo().isInContact()
        #print self.vehicle.getWheel(0).getRaycastInfo().getContactPointWs()
        #print self.vehicle.getChassis().isKinematic()
        return task.cont

    def cleanup(self):
        self.world = None
        self.worldNP.remove_node()

    def setup(self):
        self.worldNP = render.attach_new_node('World')

        # World
        self.debugNP = self.worldNP.attach_new_node(BulletDebugNode('Debug'))
        self.debugNP.show()

        self.world = BulletWorld()
        self.world.set_gravity(LVector3(0, 0, -9.81))
        self.world.set_debug_node(self.debugNP.node())

        # Plane
        shape = BulletPlaneShape(LVector3(0, 0, 1), 0)

        np = self.worldNP.attach_new_node(BulletRigidBodyNode('Ground'))
        np.node().add_shape(shape)
        np.set_pos(0, 0, -1)
        np.set_collide_mask(BitMask32.all_on())

        self.world.attach(np.node())

        # Chassis
        shape = BulletBoxShape(LVector3(0.6, 1.4, 0.5))
        ts = TransformState.make_pos(LPoint3(0, 0, 0.5))

        np = self.worldNP.attach_new_node(BulletRigidBodyNode('Vehicle'))
        np.node().add_shape(shape, ts)
        np.set_pos(0, 0, 1)
        np.node().set_mass(800.0)
        np.node().set_deactivation_enabled(False)

        self.world.attach(np.node())

        #np.node().set_ccd_swept_sphere_radius(1.0)
        #np.node().set_ccd_motion_threshold(1e-7) 

        # Vehicle
        self.vehicle = BulletVehicle(self.world, np.node())
        self.vehicle.set_coordinate_system(ZUp)
        self.world.attach(self.vehicle)

        self.yugoNP = loader.load_model('models/yugo/yugo.egg')
        self.yugoNP.reparent_to(np)

        # Right front wheel
        np = loader.load_model('models/yugo/yugotireR.egg')
        np.reparent_to(self.worldNP)
        self.add_wheel(LPoint3( 0.70,  1.05, 0.3), True, np)

        # Left front wheel
        np = loader.load_model('models/yugo/yugotireL.egg')
        np.reparent_to(self.worldNP)
        self.add_wheel(LPoint3(-0.70,  1.05, 0.3), True, np)

        # Right rear wheel
        np = loader.load_model('models/yugo/yugotireR.egg')
        np.reparent_to(self.worldNP)
        self.add_wheel(LPoint3( 0.70, -1.05, 0.3), False, np)

        # Left rear wheel
        np = loader.load_model('models/yugo/yugotireL.egg')
        np.reparent_to(self.worldNP)
        self.add_wheel(LPoint3(-0.70, -1.05, 0.3), False, np)

        # Steering info
        self.steering = 0.0            # degree
        self.steeringClamp = 45.0      # degree
        self.steeringIncrement = 120.0 # degree per second

    def add_wheel(self, pos, front, np):
        wheel = self.vehicle.create_wheel()

        wheel.set_node(np.node())
        wheel.set_chassis_connection_point_cs(pos)
        wheel.set_front_wheel(front)

        wheel.set_wheel_direction_cs(LVector3(0, 0, -1))
        wheel.set_wheel_axle_cs(LVector3(1, 0, 0))
        wheel.set_wheel_radius(0.25)
        wheel.set_max_suspension_travel_cm(40.0)

        wheel.set_suspension_stiffness(40.0)
        wheel.set_wheels_damping_relaxation(2.3)
        wheel.set_wheels_damping_compression(4.4)
        wheel.set_friction_slip(100.0);
        wheel.set_roll_influence(0.1)