Nested Scenes with Linked Rotation

This is one that is easier to show with a screen capture. I’m trying to figure out how to recreate the functionality of the origin indicator down in the lower right corner.

I created that app (Electron.js based) and there are two separate canvases: one for the CAD models being displayed, and one for the origin indicator. The rotation from the CAD canvas is mirrored to the origin indicator’s canvas. It ends up being a nested canvas that gives a picture-in-picture effect. I’m probably over-complicating it in my mind, but I’m not sure how to accomplish this correctly in Panda3D. I’m planing to create the origin indicator model in Blender, so that part should be covered at least. Any ideas on how to do this would be appreciated.

I think that the linked rotation might be done with a CompassEffect (For more detail, see the API) applied to the indicator. Having it appear over the rest might be done simply by rendering it last (via assigning it to an appropriate culling-bin), with depth-testing disabled, if I’m not much mistaken.

1 Like

Here’s how I do this in my own project:

from direct.showbase.ShowBase import ShowBase, DirectObject
from panda3d.core import *


class WorldAxesTripod:

    def __init__(self, showbase):

        self.win = showbase.win
        self.dr_pixel_size = 68  # size of display region in pixels
        dr = showbase.win.make_display_region(0., 1., 0., 1.)
        dr.sort = 2
        lens = OrthographicLens()
        lens.film_size = .235

        self._cam_target = cam_target = NodePath("tripod_cam_target")
        cam_target.set_compass(showbase.cam)
        cam_node = Camera("tripod_cam")
        camera = cam_target.attach_new_node(cam_node)

        camera.node().set_lens(lens)
        dr.camera = camera
        dr.set_clear_color_active(False)
        dr.set_clear_depth_active(True)
        self._display_region = dr

        self._root = camera.attach_new_node("world_axes")
        self._root.set_y(10.)
        self._axis_tripod = self.__create_axis_tripod()

        node = self._root.node()
        node.set_bounds(OmniBoundingVolume())
        node.final = True

        self.listener = DirectObject.DirectObject()
        self.listener.accept("aspectRatioChanged", self.__update_region_size)

    def __create_axis_tripod(self):

        vertex_format = GeomVertexFormat.get_v3c4()

        vertex_data = GeomVertexData("axis_tripod_data", vertex_format, Geom.UH_static)
        pos_writer = GeomVertexWriter(vertex_data, "vertex")
        col_writer = GeomVertexWriter(vertex_data, "color")

        tripod = GeomLines(Geom.UH_static)

        for i in range(3):
            v_pos = VBase3()
            pos_writer.add_data3(v_pos)
            v_pos[i] = .1
            pos_writer.add_data3(v_pos)
            color = VBase4(0., 0., 0., 1.)
            color[i] = 1.
            col_writer.add_data4(color)
            col_writer.add_data4(color)
            tripod.add_vertices(i * 2, i * 2 + 1)

        tripod_geom = Geom(vertex_data)
        tripod_geom.add_primitive(tripod)
        tripod_node = GeomNode("axis_tripod")
        tripod_node.add_geom(tripod_geom)
        axis_tripod = self._root.attach_new_node(tripod_node)
        axis_tripod.set_compass()

        return axis_tripod

    def __update_region_size(self):

        win_w, win_h = self.win.properties.size
        aspect_ratio = win_w / win_h
        size_h = self.dr_pixel_size / win_w
        size_v = size_h * aspect_ratio
        self._display_region.dimensions = (0., size_h, 0., size_v)


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        terrain = loader.loadModel('../samples/Roaming-Ralph/models/world')
        terrain.reparentTo(render)

        self._tripod = WorldAxesTripod(self)


app = MyApp()
app.run()

Note that two compass effects are needed to give the axis tripod the correct orientation. Also, this method does not require disabling depth-testing, so you can use whatever 3D model you wish for the tripod (otherwise, visually overlapping triangles in the geometry might not be rendered correctly).

As a bonus, the display region frame is updated each time the window size is changed, so the tripod will retain its apparent size, regardless of window size.

Although it’s probably not that hard to change the location of the display region (it’s on the left in my code), I’ll leave that as an exercise for the reader :wink: .

1 Like

@Epihaius I copied your example into a .py file to test it, and only changed the terrain model to load a model that I had. When I rotate the terrain model, the tripod stays stationary. It does not mirror the rotation of the model. Any ideas on what I might have done wrong?

Ah, it wasn’t clear to me that you wanted the tripod to follow the rotation of a specific object. Judging from your screenshot, I had the impression you wanted to create something like a level editor, where the tripod would indicate how the world (represented by a presumably stationary grid) was currently oriented relative to the camera. So I thought it would only need to take the rotation of the camera into account.

No worries, this is easily fixed by passing in the desired target object to the set_compass call on the tripod model.

Here’s an updated version of the code (the WorldAxesTripod class has been renamed to AxisTripod, which is a bit more general, since it’s no longer really intended to be aligned with the world/scene itself):

from direct.showbase.ShowBase import ShowBase, DirectObject
from panda3d.core import *


class AxisTripod:

    def __init__(self, showbase):

        self._win = showbase.win
        self._dr_pixel_size = 68  # size of display region in pixels
        dr = showbase.win.make_display_region(0., 1., 0., 1.)
        dr.sort = 2
        lens = OrthographicLens()
        lens.film_size = .235

        self._cam_target = cam_target = NodePath("tripod_cam_target")
        cam_target.set_compass(showbase.cam)
        cam_node = Camera("tripod_cam")
        camera = cam_target.attach_new_node(cam_node)

        camera.node().set_lens(lens)
        dr.camera = camera
        dr.set_clear_color_active(False)
        dr.set_clear_depth_active(True)
        self._display_region = dr

        self._root = camera.attach_new_node("world_axes")
        self._root.set_y(10.)
        self.model = self.__create_model()

        node = self._root.node()
        node.set_bounds(OmniBoundingVolume())
        node.final = True

        self._listener = DirectObject.DirectObject()
        self._listener.accept("aspectRatioChanged", self.__update_region_size)

    def __create_model(self):

        vertex_format = GeomVertexFormat.get_v3c4()

        vertex_data = GeomVertexData("axis_tripod_data", vertex_format, Geom.UH_static)
        pos_writer = GeomVertexWriter(vertex_data, "vertex")
        col_writer = GeomVertexWriter(vertex_data, "color")

        lines = GeomLines(Geom.UH_static)

        for i in range(3):
            v_pos = VBase3()
            pos_writer.add_data3(v_pos)
            v_pos[i] = .1
            pos_writer.add_data3(v_pos)
            color = VBase4(0., 0., 0., 1.)
            color[i] = 1.
            col_writer.add_data4(color)
            col_writer.add_data4(color)
            lines.add_vertices(i * 2, i * 2 + 1)

        geom = Geom(vertex_data)
        geom.add_primitive(lines)
        node = GeomNode("axis_tripod")
        node.add_geom(geom)
        model = self._root.attach_new_node(node)

        return model

    def __update_region_size(self):

        win_w, win_h = self._win.properties.size
        aspect_ratio = win_w / win_h
        size_h = self._dr_pixel_size / win_w
        size_v = size_h * aspect_ratio
        self._display_region.dimensions = (0., size_h, 0., size_v)


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        self.terrain = loader.loadModel('../samples/Roaming-Ralph/models/world')
        self.terrain.reparentTo(render)

        self._tripod = AxisTripod(self)
        # make sure the tripod model follows the orientation of the terrain
        self._tripod.model.set_compass(self.terrain)
        self._angle = 0.
        self.taskMgr.add(self.__rotate, "rotate")

    def __rotate(self, task):

        self._angle += 0.1
        self.terrain.set_hpr(self._angle, self._angle, 0.)

        return task.cont


app = MyApp()
app.run()

A task has been added that continuously changes the orientation of the terrain; you should find that the tripod follows its rotation correctly, even while rotating the camera as well.

1 Like

@Epihaius Thank you for taking the time to post the modified example and explain it. I think I get what’s going on now.