Exemple #1
0
class LightBase(object):
    """ LightBase is a lightweight interface for rendering with
    Panda3d. It contains several key Panda3d objects that are required
    for rendering, like GraphicsEngine, GraphicsPipe, GraphicsOutput,
    a root NodePath (root), cameras and lights. It provides also
    methods for manipulating and reading them out them."""

    def __init__(self):
        self.init_graphics()
        self.setup_root()
        self.output_list = []
        self.gsg_list = []
        self.cameras = None
        atexit.register(self._exitfunc)

    def init_graphics(self):
        """Creates GraphicsEngine, GraphicsPipe, and loader."""
        # Get a handle to the graphics pipe selector
        selection = GraphicsPipeSelection.getGlobalPtr()
        # Check for DISPLAY
        if "DISPLAY" in os.environ:
            # Use the first option (should be glx)
            pipe_type = selection.getPipeTypes()[0]
        else:
            # Use the last option (should be some fallback module)
            pipe_type = selection.getPipeTypes()[-1]
        # Create the graphics pipe
        self.pipe = selection.makePipe(pipe_type)
        # Get the graphics engine
        self.engine = GraphicsEngine.getGlobalPtr()
        # Get the model loader object and assign it to the engine
        # self.loader = Loader.Loader(self)
        # self.engine.setDefaultLoader(self.loader.loader)
        self.loader = Loader()
        self.engine.setDefaultLoader(self.loader.panda_loader)

    @staticmethod
    def init_fbp():
        """Initial / default FrameBufferProperties.

        Return:
            (FrameBufferProperties): FrameBufferProperties object.

        """
        fbp = FrameBufferProperties()
        fbp.setRgbColor(1)
        fbp.setColorBits(1)
        fbp.setAlphaBits(1)
        fbp.setDepthBits(1)
        return fbp

    @staticmethod
    def init_wp(window_type, size):
        """Initial / default WindowProperties.

        Args:
            window_type (str): Window type.
            size (Iterable, 2): Width, height.

        Return:
            (WindowProperties): WindowProperties object.

        """
        if window_type == "onscreen":
            wp = WindowProperties.getDefault()
            wp.setSize(size[0], size[1])
        elif window_type == "offscreen":
            wp = WindowProperties.size(size[0], size[1])
        return wp

    def make_window(self, size=None, name=None, sort=0, xflags=0,
                    host_out=None):
        """Create an onscreen window. High-level interface for `make_output`

        Keyword Args:
            size (Iterable, 2): Width, height.
            name (str): Window name.
            sort (int): Sort order.
            xflags (int): GraphicsPipe bit flags.
            host_out (GraphicsOutput): Output object.

        Return:
            (GraphicsOutput): Output object.

        """
        # Set size.
        if size is None and host_out is not None:
            size = (host_out.getFbXSize(), host_out.getFbYSize())
        elif size is None:
            raise ValueError("Size not available.")
        # Initialize WindowProperties.
        wp = self.init_wp("onscreen", size)
        # Create window.
        output = self.make_output("onscreen", name, sort, wp=wp,
                                  xflags=xflags, host_out=host_out)
        # Handle failure to create window (returns None)
        if output is None:
            raise StandardError("Window creation failed, not sure why.")
        return output

    def make_buffer(self, size=None, name=None, sort=10, xflags=0,
                    host_out=None):
        """Create an offscreen buffer. High-level interface for
        `make_output`.

        Keyword Args:
            size (Iterable, 2): Width, height.
            name (str): Window name.
            sort (int): Sort order.
            xflags (int): GraphicsPipe bit flags.
            host_out (GraphicsOutput): Output object.

        Return:
            (GraphicsOutput): Output object.

        """
        # Set size.
        if size is None and host_out is not None:
            size = (host_out.getFbXSize(), host_out.getFbYSize())
        elif size is None:
            raise ValueError("Size not available.")
        # Initialize WindowProperties
        wp = self.init_wp("offscreen", size)
        # Create the buffer
        output = self.make_output("offscreen", name, sort, wp=wp,
                                  xflags=xflags, host_out=host_out)
        # Handle case when offscreen buffer cannot be directly created
        # (currently this is what happens)
        if output is None:
            print("Direct offscreen buffer creation failed...")
            print("... falling back to onscreen --> offscreen method.")
            # Open an onscreen window first, just to get a GraphicsOutput
            # object which is needed to open an offscreen buffer.
            dummywin = self.make_window(size, "dummy_onscreen_win", sort,
                                        xflags, host_out)
            # Now, make offscreen buffer through win
            output = self.make_output("offscreen", name, sort, xflags=xflags,
                                      host_out=dummywin)
            # Handle failure to create window (returns None)
            if output is None:
                raise StandardError("Failed to create offscreen buffer.")
        return output

    def make_texture_buffer(self, size=None, name=None, sort=10,
                            xflags=0, mode=None, bitplane=None, host_out=None):
        """Makes an offscreen buffer and adds a render texture.

        Keyword Args:
            size (Iterable, 2): Width, height.
            name (str): Window name.
            sort (int): Sort order.
            xflags (int): GraphicsPipe bit flags.
            mode (GraphicsOutput.RenderTextureMode): see
                :py:meth:`add_render_texture` for possible values.
            bitplane (DrawableRegion.RenderTexturePlane): see
                :py:meth:`add_render_texture` for possible values.
            host_out (GraphicsOutput): Output object.

        Return:
            (GraphicsOutput): Output object.
            (Texture): Texture object.

        """
        # Make the offscreen buffer
        output = self.make_buffer(size, name, sort, xflags, host_out)
        # Add the texture
        tex = self.add_render_texture(output, mode, bitplane)
        # Cause necessary stuff to be created (buffers, textures etc)
        self.render_frame()
        return output, tex

    def make_output(self, window_type, name=None, sort=10, fbp=None, wp=None,
                    xflags=0, host_out=None):
        """Create a GraphicsOutput object and store in self.output_list.
        This is the low-level interface.

        Args:
            window_type (str): Window type.

        Keyword Args:
            name (str): Window name.
            sort (int): Sort order.
            fbp (FrameBufferProperties): FrameBufferProperties object.
            wp (WindowProperties): WindowProperties object.
            xflags (int): GraphicsPipe bit flags.
            host_out (GraphicsOutput): Output object.

        Return:
            (GraphicsOutput): Output object.

        """
        # Input handling / defaults
        if name is None:
            name = window_type + "_win"
        if fbp is None:
            # Initialize FramebufferProperties
            fbp = self.init_fbp()
        if wp is None and host_out is not None:
            # Initialize WindowProperties
            wp = self.init_wp(window_type, (host_out.getFbXSize(),
                                            host_out.getFbYSize()))
        elif wp is None:
            raise ValueError("Size not available in either wp or win.")
        flags = xflags | GraphicsPipe.BFFbPropsOptional
        # flags' window_type switch
        if window_type == "onscreen":
            flags = flags | GraphicsPipe.BFRequireWindow
        elif window_type == "offscreen":
            flags = flags | GraphicsPipe.BFRefuseWindow
        # Make the window / buffer
        engine = self.engine
        pipe = self.pipe
        if host_out is None:
            output = engine.makeOutput(pipe, name, sort, fbp, wp, flags)
        else:
            output = engine.makeOutput(pipe, name, sort, fbp, wp, flags,
                                       host_out.getGsg(), host_out)
        # Add output to this instance's list
        if output is not None:
            self.add_to_output_gsg_lists(output)
            # Set background color to black by default
            output.setClearColor((0.0, 0.0, 0.0, 0.0))
            # Cause necessary stuff to be created (buffers, textures etc)
            self.render_frame()
        return output

    def add_to_output_gsg_lists(self, output):
        """Adds the `output` and GSG to the instance's running list.

        Args:
            output (GraphicsOutput): Graphics output.

        """
        self.output_list.append(output)
        self.gsg_list.append(output.getGsg())

    def remove_from_output_gsg_lists(self, output):
        """Removes the `output` and GSG from the instance's running list.

        Args:
            output (GraphicsOutput): Graphics output.

        """
        self.output_list.remove(output)
        self.gsg_list.remove(output.getGsg())

    @staticmethod
    def add_render_texture(output, mode=None, bitplane=None):
        """Add render texture to `output`.

        Args:
            output (GraphicsOutput): Graphics output.

        Keyword Args:
            mode (GraphicsOutput.RenderTextureMode):
                | RTMNode
                | RTMBindOrCopy
                | RTMCopyTexture
                | RTMCopyRam
                | RTMTriggeredCopyTexture
                | RTMTriggeredCopyRam
            bitplane (DrawableRegion.RenderTexturePlane):
                | RTPStencil
                | RTPDepthStencil
                | RTPColor
                | RTPAuxRgba0
                | RTPAuxRgba1
                | RTPAuxRgba2
                | RTPAuxRgba3
                | RTPAuxHrgba0
                | RTPAuxHrgba1
                | RTPAuxHrgba2
                | RTPAuxHrgba3
                | RTPAuxFloat0
                | RTPAuxFloat1
                | RTPAuxFloat2
                | RTPAuxFloat3
                | RTPDepth
                | RTPCOUNT

        Return:
            (Texture): Texture object.

        """
        # Mode.
        if mode is None:
            mode = GraphicsOutput.RTMBindOrCopy
        elif isinstance(mode, str):
            mode = getattr(GraphicsOutput, mode)
        if bitplane is None:
            bitplane = GraphicsOutput.RTPColor
        elif isinstance(bitplane, str):
            bitplane = getattr(GraphicsOutput, bitplane)
        # Bitplane.
        if bitplane is GraphicsOutput.RTPColor:
            fmt = Texture.FLuminance
        elif bitplane is GraphicsOutput.RTPDepth:
            fmt = Texture.FDepthComponent
        # Get a handle to the texture.
        tex = Texture()
        tex.setFormat(fmt)
        # Add the texture to the buffer.
        output.addRenderTexture(tex, mode, bitplane)
        tex.clearRamImage()
        return tex

    def close_output(self, output):
        """Closes the indicated `output` and removes it from the list.

        Args:
            output (GraphicsOutput): Graphics output.

        """
        output.setActive(False)
        # First, remove all of the cameras associated with display
        # regions on the window.
        num_regions = output.getNumDisplayRegions()
        for i in range(num_regions):
            dr = output.getDisplayRegion(i)
            dr.setCamera(NodePath())
        # Remove this output from the list.
        self.remove_from_output_gsg_lists(output)
        # Now we can actually close the window.
        engine = output.getEngine()
        engine.removeWindow(output)
        # Give the window a chance to actually close before continuing.
        engine.renderFrame()

    def close_all_outputs(self):
        """Closes all of this instance's outputs."""
        for output in self.output_list:
            self.close_output(output)
        # Clear the output list
        self.output_list = []

    def make_camera(self, output, sort=0, dr_dims=(0, 1, 0, 1),
                    aspect_ratio=None, clear_depth=False, clear_color=None,
                    lens=None, cam_name="camera0", mask=None):
        """
        Makes a new 3-d camera associated with the indicated window,
        and creates a display region in the indicated subrectangle.

        If stereo is True, then a stereo camera is created, with a
        pair of DisplayRegions.  If stereo is False, then a standard
        camera is created.  If stereo is None or omitted, a stereo
        camera is created if the window says it can render in stereo.

        If useCamera is not None, it is a NodePath to be used as the
        camera to apply to the window, rather than creating a new
        camera.

        Args:
            output (GraphicsOutput): Output object.

        Keyword Args:
            sort (int): Sort order.
            dr_dims (Iterable, 4): DisplayRegion dimensions.
            aspect_ratio (float): Aspect ratio.
            clear_depth (bool): Indicator to clear depth buffer.
            clear_color (bool): Indicator to clear color buffer.
            lens (Lens): Lens object.
            cam_name (str): Window name.
            mask (BitMask32): Bit mask that indicates which objects to render.

        Return:
            (NodePath): Camera nodepath.

        """

        # self.cameras is the parent node of all cameras: a node that
        # we can move around to move all cameras as a group.
        if self.cameras is None:
            # We make it a ModelNode with the PTLocal flag, so that a
            # wayward flatten operations won't attempt to mangle the
            # camera.
            self.cameras = self.root.attachNewNode(ModelNode("cameras"))
            self.cameras.node().setPreserveTransform(ModelNode.PTLocal)

        # Make a new Camera node.
        cam_node = Camera(cam_name)
        if lens is None:
            lens = PerspectiveLens()
            if aspect_ratio is None:
                aspect_ratio = self.get_aspect_ratio(output)
            lens.setAspectRatio(aspect_ratio)
            lens.setNear(0.1)
            lens.setFar(1000.0)
        if lens is not None:
            cam_node.setLens(lens)
        camera = self.cameras.attachNewNode(cam_node)
        # Masks out part of scene from camera
        if mask is not None:
            if (isinstance(mask, int)):
                mask = BitMask32(mask)
            cam_node.setCameraMask(mask)
        # Make a display region
        dr = output.makeDisplayRegion(*dr_dims)
        # By default, we do not clear 3-d display regions (the entire
        # window will be cleared, which is normally sufficient).  But
        # we will if clearDepth is specified.
        if clear_depth:
            dr.setClearDepthActive(1)
        if clear_color:
            dr.setClearColorActive(1)
            dr.setClearColor(clear_color)
        dr.setSort(sort)
        dr.setCamera(camera)
        dr.setActive(True)
        return camera

    @staticmethod
    def get_aspect_ratio(output):
        """Returns aspect ratio of `output`'s window, or default aspect
        ratio if it has no window.

        Args:
            output (GraphicsOutput): Graphics output.

        Return:
            (float): Aspect ratio.

        """
        aspect_ratio = 1
        if output.hasSize():
            aspect_ratio = (float(output.getSbsLeftXSize()) /
                            float(output.getSbsLeftYSize()))
        else:
            wp = output.getRequestedProperties()
            if not wp.hasSize():
                wp = WindowProperties.getDefault()
            aspect_ratio = float(wp.getXSize()) / float(wp.getYSize())
        return aspect_ratio

    @staticmethod
    def make_lights():
        """Create one point light and an ambient light.

        Return:
           (NodePath): Lights nodepath.

        """
        lights = NodePath("lights")
        # Create point lights.
        plight = PointLight("plight1")
        light = lights.attachNewNode(plight)
        light.setPos((3, -10, 2))
        light.lookAt(0, 0, 0)
        # Create ambient light.
        alight = AmbientLight("alight")
        alight.setColor((0.75, 0.75, 0.75, 1.0))
        lights.attachNewNode(alight)
        return lights

    def setup_root(self):
        """Set up the scene graph."""
        self.root = NodePath("root")
        self.root.setAttrib(RescaleNormalAttrib.makeDefault())
        self.root.setTwoSided(False)
        self.backface_culling_enabled = True
        self.texture_enabled = True
        self.wireframe = False

    @property
    def wireframe(self):
        """(Property) Get wireframe mode.

        Return:
            (bool): Indicates wireframe ON.

        """
        return self._wireframe

    @wireframe.setter
    def wireframe(self, val):
        """(Property) Set wireframe mode.

        Args:
            val (bool): Indicates wireframe ON.

        """
        if self.wireframe:
            self.root.clearRenderMode()
            self.root.setTwoSided(not self.backface_culling_enabled)
        else:
            self.root.setRenderModeWireframe(100)
            self.root.setTwoSided(True)

    @staticmethod
    def trigger_copy(output):
        """Signals the texture to be pushed to RAM after the next
        render_frame.

        Args:
            output (GraphicsOutput): Graphics output.

        """
        output.trigger_copy()

    def render_frame(self):
        """Render the frame."""
        # If you're trying to read the texture buffer, remember to
        # call self.trigger_copy()
        self.engine.renderFrame()

    @staticmethod
    def get_tex_array(tex, reshape=True):
        """Returns image (as ndarray) in `tex`.

        Args:
            tex (Texture): Texture handle.

        Keyword Args:
            reshape (bool): Indicator to reshape image array to 2D.

        Return:
            (ndarray): Image array.

        """
        # Remember to call self.trigger_copy() before
        # self.render_frame(), or the next frame won't be pushed to RAM
        if not tex.hasUncompressedRamImage():
            img = None
        else:
            texel_type = tex.getComponentType()
            if texel_type == Texture.TUnsignedByte:
                dtype = "u1"
            elif texel_type == Texture.TUnsignedShort:
                dtype = "u2"
            elif texel_type == Texture.TFloat:
                dtype = "f4"
            elif texel_type == Texture.TUnsignedInt248:
                dtype = "u4"
            # texel_width = tex.getComponentWidth()
            texdata = tex.getUncompressedRamImage().getData()
            img = fromstring(texdata, dtype=dtype)
            if reshape:
                shape = [tex.getYSize(), tex.getXSize(), -1]
                n_channels = tex.getNumComponents()
                if n_channels == 4:
                    channel_order = [2, 1, 0, 3]
                elif n_channels == 3:
                    channel_order = [2, 1, 0]
                else:
                    # from pdb import set_trace as BP; BP()
                    channel_order = range(n_channels)
                img = img.reshape(shape)[:, :, channel_order]
        return img

    @staticmethod
    def get_tex_image(tex):
        """Returns image (as PNMImage) in `tex`.

        Args:
            tex (Texture): Texture handle.

        Return:
            (PNMImage): Image object.

        """
        # Remember to call self.trigger_copy() before
        # self.render_frame(), or the next frame won't be pushed to RAM.
        if not tex.hasRamImage():
            img = None
        else:
            img = PNMImage()
            tex.store(img)
        return img

    @staticmethod
    def screenshot(output, pth=None):
        """Save screenshot of `output`.

        Args:
            output (GraphicsOutput): Graphics output.

        Keyword Args:
            pth (str): Path to save screenshot.

        Return:
            (bool): Indicates successful save.

        """
        if pth is None:
            filename = GraphicsOutput.makeScreenshotFilename()
        else:
            filename = Filename(pth)
        if isinstance(output, GraphicsOutput):
            success = output.saveScreenshot(filename)
        elif isinstance(output, Texture):
            if output.getZSize() > 1:
                success = output.write(filename, 0, 0, 1, 0)
            else:
                success = output.write(filename)
        else:
            raise TypeError("Unhandled output type: " + type(output))
        return success

    def destroy(self):
        """Close outputs associated with this instance."""
        self.close_all_outputs()
        if getattr(self, "engine", None):
            self.engine.removeAllWindows()
            self.engine = None
        if getattr(self, "pipe", None):
            self.pipe = None

    def _exitfunc(self):
        """atexit function."""
        self.destroy()

    @staticmethod
    def destroy_windows():
        """Destroy all graphics windows globally."""
        GraphicsEngine.getGlobalPtr().removeAllWindows()