Draw 3D lines problem

Hey fellows, I’ve managed to draw lines using the mouse in the direction of plane xy,xz,yz with angular steps. However, when I try to move the origin of the line in the using the yz plane the visual aid (a circle and a line) rotates wrongly. Any ideas why this happens? Here follows my code and images regarding the issue. This only happens when I change to rotate the line around y-axis.

import math

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText

# Function to put instructions on the screen.
def addInstructions(pos, msg):
    return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), scale=.05,
                        shadow=(0, 0, 0, 1), parent=base.a2dTopLeft,
                        pos=(0.08, -pos - 0.04), align=TextNode.ALeft)

class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        #disable mouse, we need the left click working
        base.disableMouse()
        #set sensible view
        base.cam.set_pos(20, -20, 20)
        base.cam.look_at(0,0,0)
        #help info
        self.inst1 = addInstructions(0.06, "MOUSE-LEFT: click and hold / MOUSE RIGHT : Draw line")
        self.inst2 = addInstructions(0.12, "X, Y, Z: change rotation axis")
        self.inst3 = addInstructions(0.18, "Vector")
        self.inst4 = addInstructions(0.24, "Last vector")
        self.inst5 = addInstructions(0.30, "Position")
        #origin
        self.origin = Point3(0,0,0)
        #visual aid
        self.circle=self.make_circle()
        #mouse left or right click control
        self.mouse_left_is_down = False
        self.mouse_right_is_down = False

        #temporary line between the mouse and the origin
        self.aux_line = None
        #the line you want to draw
        self.last_line = None
        #a line showing the current rotation axis
        self.line=None
        #the axis node, we rotate around this node
        self.axis=render.attach_new_node('axis')
        #list of current axis, overkill here, but used for moving
        self.active_axis='z'
        self.update_axis(Point3(0,0,0))
        self.mouse_is_down=False

        #bind keys
        self.accept('mouse1', self.on_mouse_left_down)
        self.accept('mouse1-up', self.on_mouse_left_up)
        self.accept('mouse3', self.on_mouse_right_down)
        self.accept('mouse3-up', self.on_mouse_right_up)
        self.accept('x', self.toggle_axis, ['x'])
        self.accept('y', self.toggle_axis, ['y'])
        self.accept('z', self.toggle_axis, ['z'])
        #run task
        taskMgr.add(self.mouse_task, 'mouse_tsk')

    def make_circle(self, segments = 360, thickness=2.0, radius=2.0):
        l=LineSegs()
        l.set_thickness(thickness)
        l.move_to(self.origin)
        l.draw_to((0, radius, 0))
        temp = NodePath('temp')
        for i in range(segments + 1):
            temp.set_h(i * 360.0 / segments)
            p = render.get_relative_point(temp, (0, radius/2, 0))
            l.draw_to(p)
        temp.remove_node()
        return render.attach_new_node(l.create())

    def draw_node(self, position):
        sphere = loader.loadModel("misc/sphere.egg")
        sphere.reparentTo(render)
        sphere.setScale(0.1)
        sphere.setPos(position)
        sphere.setColor(0, 0, 0)
        return sphere

    def on_mouse_left_down(self):
        self.mouse_left_is_down = True
        print('mouse left down')

    def on_mouse_right_down(self):
        self.mouse_right_is_down = True
        print('mouse right down')
        self.last_vec = None
        self.mouse_is_down = True

    def on_mouse_left_up(self):
        self.mouse_left_is_down = False
        print('mouse left up')

    def on_mouse_right_up(self):
        self.mouse_right_is_down = False
        print('mouse right up')
        self.inst4.setText(str(self.last_vec))

        l = LineSegs()
        self.draw_node(self.last_line[0])
        l.move_to(self.last_line[0])
        self.draw_node(self.last_line[1])
        l.draw_to(self.last_line[1])
        render.attach_new_node(l.create())

    def toggle_axis(self, axis):
        self.active_axis = axis
        self.update_axis(self.origin)

    def update_axis(self, point):
        '''Creates a plane to capture mouse clicks,
        the pos of self.axis defines the point for the plane,
        the normal of the plane depends on what is
        the current axis in self.active_axis.
        Also draws a line/vector to show the axis.
        '''
        if 'x' in self.active_axis:
            vec=self.axis.get_quat().get_right()
        elif 'y' in self.active_axis:
            vec=self.axis.get_quat().get_forward()
        elif 'z' in self.active_axis:
            vec=self.axis.get_quat().get_up()
        #remove old line
        if self.line:
            self.line.remove_node()
        #draw new line
        if self.active_axis:
            self.plane=Plane(vec, point) # also make the plane, kind of important...
            l=LineSegs()
            l.set_thickness(2.0)
            l.move_to(point)
            l.draw_to((point+vec))
            self.line=render.attach_new_node(l.create())
            self.line.set_color((abs(vec.x), abs(vec.y), abs(vec.z), 1.0), 1)
            self.circle.set_color(self.line.get_color(), 1) #recolor the circle to make it fit

    def mouse_task(self, task):
        '''Rotates self.model around self.axis based on mouse movement '''
        if self.active_axis:
            if base.mouseWatcherNode.has_mouse():
                # get the mouse ray-plane intersection
                mpos = base.mouseWatcherNode.get_mouse()
                pos3d = Point3()
                near_point = Point3()
                far_point = Point3()
                base.camLens.extrude(mpos, near_point, far_point)
                if self.plane.intersects_line(pos3d,
                                              render.get_relative_point(base.cam, near_point),
                                              render.get_relative_point(base.cam, far_point)):

                    self.inst5.setText(str(pos3d))
                    if self.mouse_left_is_down:
                        self.update_axis(pos3d)
                        self.origin = pos3d
                        self.circle.set_pos(self.origin)

                    vec = pos3d - self.axis.get_pos()
                    angle_vector = pos3d - self.origin
                    # calculate the angle
                    angle = 0
                    if self.active_axis == 'x':
                        angle = math.atan2(angle_vector.y, angle_vector.z)
                    if self.active_axis == 'y':
                        angle = math.atan2(angle_vector.x, angle_vector.z)
                    if self.active_axis == 'z':
                        angle = math.atan2(angle_vector.x, angle_vector.y)

                    angle_in_degrees = round(math.degrees(angle))

                    if angle_in_degrees % 5 == 0:
                        if angle_in_degrees < 0:
                            angle_in_degrees += 360

                        self.inst3.setText(str(angle_in_degrees))
                        self.circle.look_at(pos3d, self.plane.get_normal())
                        self.circle.show()
                        # remove if exists previous auxiliar line
                        if self.aux_line : self.aux_line.removeAllGeoms()
                        #draw the auxiliar line
                        l = LineSegs()
                        l.move_to(self.circle.get_pos())
                        l.draw_to(vec)
                        #store the auxiliar line
                        self.aux_line = l.create()
                        render.attach_new_node(self.aux_line)
                        self.last_line = (self.circle.get_pos(),vec)

        return task.again

app = MyApp()
app.run()

Wrong movement


Right after a change in angle

Hmm… It looks like this behaviour might be related to “look_at” in some way. It may be that, to get the behaviour that you’re looking for, an explicit mathematical approach (something along the lines of what I described here) might be called for.

(What I have there won’t work for all axes, I don’t think; there may be some work yet to be done to get it quite working properly.)

The issue is caused by angle_vector having zero-length, which results from setting self.origin equal to pos3d. The way I fixed this is by pointing the circle at a different point that is guaranteed to be different from self.origin. This is also done in update_axis, to immediately update the orientation of the circle so it lies in the new plane.

Here is the new code:

import math

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText

# Function to put instructions on the screen.
def addInstructions(pos, msg):
    return OnscreenText(text=msg, style=1, fg=(1, 1, 1, 1), scale=.05,
                        shadow=(0, 0, 0, 1), parent=base.a2dTopLeft,
                        pos=(0.08, -pos - 0.04), align=TextNode.ALeft)

class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        #disable mouse, we need the left click working
        base.disableMouse()
        #set sensible view
        base.cam.set_pos(20, -20, 20)
        base.cam.look_at(0,0,0)
        #help info
        self.inst1 = addInstructions(0.06, "MOUSE-LEFT: click and hold / MOUSE RIGHT : Draw line")
        self.inst2 = addInstructions(0.12, "X, Y, Z: change rotation axis")
        self.inst3 = addInstructions(0.18, "Vector")
        self.inst4 = addInstructions(0.24, "Last vector")
        self.inst5 = addInstructions(0.30, "Position")
        #origin
        self.origin = Point3(0,0,0)
        #visual aid
        self.circle=self.make_circle()
        #mouse left or right click control
        self.mouse_left_is_down = False
        self.mouse_right_is_down = False

        #temporary line between the mouse and the origin
        self.aux_line = None
        #the line you want to draw
        self.last_line = None
        #a line showing the current rotation axis
        self.line=None
        #the axis node, we rotate around this node
        self.axis=render.attach_new_node('axis')
        #list of current axis, overkill here, but used for moving
        self.active_axis='z'
        self.update_axis(Point3(0,0,0))
        self.mouse_is_down=False

        #bind keys
        self.accept('mouse1', self.on_mouse_left_down)
        self.accept('mouse1-up', self.on_mouse_left_up)
        self.accept('mouse3', self.on_mouse_right_down)
        self.accept('mouse3-up', self.on_mouse_right_up)
        self.accept('x', self.toggle_axis, ['x'])
        self.accept('y', self.toggle_axis, ['y'])
        self.accept('z', self.toggle_axis, ['z'])
        #run task
        taskMgr.add(self.mouse_task, 'mouse_tsk')

    def make_circle(self, segments = 360, thickness=2.0, radius=2.0):
        l=LineSegs()
        l.set_thickness(thickness)
        l.move_to(self.origin)
        l.draw_to((0, radius, 0))
        temp = NodePath('temp')
        for i in range(segments + 1):
            temp.set_h(i * 360.0 / segments)
            p = render.get_relative_point(temp, (0, radius/2, 0))
            l.draw_to(p)
        temp.remove_node()
        return render.attach_new_node(l.create())

    def draw_node(self, position):
        sphere = loader.loadModel("misc/sphere.egg")
        sphere.reparentTo(render)
        sphere.setScale(0.1)
        sphere.setPos(position)
        sphere.setColor(0, 0, 0)
        return sphere

    def on_mouse_left_down(self):
        self.mouse_left_is_down = True
        print('mouse left down')

    def on_mouse_right_down(self):
        self.mouse_right_is_down = True
        print('mouse right down')
        self.last_vec = None
        self.mouse_is_down = True

    def on_mouse_left_up(self):
        self.mouse_left_is_down = False
        print('mouse left up')

    def on_mouse_right_up(self):
        self.mouse_right_is_down = False
        print('mouse right up')
        self.inst4.setText(str(self.last_vec))

        l = LineSegs()
        self.draw_node(self.last_line[0])
        l.move_to(self.last_line[0])
        self.draw_node(self.last_line[1])
        l.draw_to(self.last_line[1])
        render.attach_new_node(l.create())

    def toggle_axis(self, axis):
        self.active_axis = axis
        self.update_axis(self.origin)

    def update_axis(self, point):
        '''Creates a plane to capture mouse clicks,
        the pos of self.axis defines the point for the plane,
        the normal of the plane depends on what is
        the current axis in self.active_axis.
        Also draws a line/vector to show the axis.
        '''
        if 'x' in self.active_axis:
            vec=self.axis.get_quat().get_right()
            pos3d = self.origin + Vec3(0., 1., 0.)  # used to update circle
        elif 'y' in self.active_axis:
            vec=self.axis.get_quat().get_forward()
            pos3d = self.origin + Vec3(1., 0., 0.)  # used to update circle
        elif 'z' in self.active_axis:
            vec=self.axis.get_quat().get_up()
            pos3d = self.origin + Vec3(0., 1., 0.)  # used to update circle
        #remove old line
        if self.line:
            self.line.remove_node()
        #draw new line
        if self.active_axis:
            self.plane=Plane(vec, point) # also make the plane, kind of important...
            l=LineSegs()
            l.set_thickness(2.0)
            l.move_to(point)
            l.draw_to((point+vec))
            self.line=render.attach_new_node(l.create())
            self.line.set_color((abs(vec.x), abs(vec.y), abs(vec.z), 1.0), 1)
            self.circle.set_color(self.line.get_color(), 1) #recolor the circle to make it fit
            # make sure the circle is aimed at a point in the new plane
            self.circle.look_at(pos3d, self.plane.get_normal())

    def mouse_task(self, task):
        '''Rotates self.model around self.axis based on mouse movement '''
        if self.active_axis:
            if base.mouseWatcherNode.has_mouse():
                # get the mouse ray-plane intersection
                mpos = base.mouseWatcherNode.get_mouse()
                pos3d = Point3()
                near_point = Point3()
                far_point = Point3()
                base.camLens.extrude(mpos, near_point, far_point)
                if self.plane.intersects_line(pos3d,
                                              render.get_relative_point(base.cam, near_point),
                                              render.get_relative_point(base.cam, far_point)):

                    self.inst5.setText(str(pos3d))
                    if self.mouse_left_is_down:
#                        self.update_axis(pos3d)  # call this after updating `self.origin`
                        self.origin = pos3d
                        self.circle.set_pos(self.origin)
                        self.update_axis(pos3d)

                    vec = pos3d - self.axis.get_pos()
                    angle_vector = pos3d - self.origin

                    if not angle_vector.normalize():
                        # if `angle_vector` is the null-vector (has zero length),
                        # use a new `pos3d` point that is guaranteed to differ
                        # from `self.origin`
                        pos3d = render.get_relative_point(self.circle, Point3(0., 1., 0.))
                        angle_vector = pos3d - self.origin

                    # calculate the angle
                    angle = 0
                    if self.active_axis == 'x':
                        angle = math.atan2(angle_vector.y, angle_vector.z)
                    if self.active_axis == 'y':
                        angle = math.atan2(angle_vector.x, angle_vector.z)
                    if self.active_axis == 'z':
                        angle = math.atan2(angle_vector.x, angle_vector.y)

                    angle_in_degrees = round(math.degrees(angle))

                    if angle_in_degrees % 5 == 0:
                        if angle_in_degrees < 0:
                            angle_in_degrees += 360

                        self.inst3.setText(str(angle_in_degrees))
                        self.circle.look_at(pos3d, self.plane.get_normal())
                        self.circle.show()
                        # remove if exists previous auxiliar line
                        if self.aux_line : self.aux_line.removeAllGeoms()
                        #draw the auxiliar line
                        l = LineSegs()
                        l.move_to(self.circle.get_pos())
                        l.draw_to(vec)
                        #store the auxiliar line
                        self.aux_line = l.create()
                        render.attach_new_node(self.aux_line)
                        self.last_line = (self.circle.get_pos(),vec)

        return task.again

app = MyApp()
app.run()

The new point is arbitrarily chosen and thus not part of the currently drawn line; hopefully that won’t be too much of a problem (only an issue when toggling axes).

3 Likes

I did something similar, thanks for the reply

I don’t know if this helps, but in NodePath — Panda3D Manual there is a method called headsUp, which behaves like lookAt while trying to maintain the “up” vector in an “up” direction. Hope it helps!