简体   繁体   中英

Python: rotating 3D objects in kivy (unwanted tilt)

I want to make something like a simple 3D viewer (maybe editor). So one of my goals is to learn how to rotate 3D objects for example using mouse.

I took "3D Rotating Monkey Head" example and changed some code in the main.py file.

I used functions of converting the Euler angles into quaternions and back - so I achieved the closest result.

So the app works almost as it should (demo gif on imgur)

But there is one annoying problem - an unwanted rotation along the z axis (tilt?). You can see this here (demo gif on imgur)

it is obvious that this should not be so.

Is there a way to get rid of this tilt?

gl and quaternions are new topics for me. Maybe I did something wrong.

My code is here (only main.py)

from kivy.app import App
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.resources import resource_find
from kivy.graphics.transformation import Matrix
from kivy.graphics.opengl import *
from kivy.graphics import *
from objloader import ObjFile



#============== quat =========================================================================

import numpy as np
from math import atan2, asin, pi, cos, sin, radians, degrees


def q2e(qua):
    L = (qua[0]**2 + qua[1]**2 + qua[2]**2 + qua[3]**2)**0.5
    w = qua[0] / L
    x = qua[1] / L
    y = qua[2] / L
    z = qua[3] / L
    Roll = atan2(2 * (w * x + y * z), 1 - 2 * (x**2 + y**2))
    if Roll < 0:
        Roll += 2 * pi


    temp = w * y - z * x
    if temp >= 0.5:
        temp = 0.5
    elif temp <= -0.5:
        temp = -0.5

    Pitch = asin(2 * temp)
    Yaw = atan2(2 * (w * z + x * y), 1 - 2 * (y**2 + z**2))
    if Yaw < 0:
        Yaw += 2 * pi
    return [Yaw,Pitch,Roll]



def e2q(ypr):
    y,p,r = ypr
    roll = r / 2
    pitch = p / 2
    yaw = y / 2

    w = cos(roll) * cos(pitch) * cos(yaw) + \
        sin(roll) * sin(pitch) * sin(yaw)
    x = sin(roll) * cos(pitch) * cos(yaw) - \
        cos(roll) * sin(pitch) * sin(yaw)
    y = cos(roll) * sin(pitch) * cos(yaw) + \
        sin(roll) * cos(pitch) * sin(yaw)
    z = cos(roll) * cos(pitch) * sin(yaw) + \
        sin(roll) * sin(pitch) * cos(yaw)
    qua = [w, x, y, z]
    return qua




def mult(q1, q2):

    w1, x1, y1, z1 = q1
    w2, x2, y2, z2 = q2
    w = w1*w2 - x1*x2 - y1*y2 - z1*z2
    x = w1*x2 + x1*w2 + y1*z2 - z1*y2
    y = w1*y2 + y1*w2 + z1*x2 - x1*z2
    z = w1*z2 + z1*w2 + x1*y2 - y1*x2
    return np.array([w, x, y, z])


def list2deg(l):
    return [degrees(i) for i in l]

#=====================================================================================================



class Renderer(Widget):
    def __init__(self, **kwargs):

        self.last = (0,0)

        self.canvas = RenderContext(compute_normal_mat=True)
        self.canvas.shader.source = resource_find('simple.glsl')
        self.scene = ObjFile(resource_find("monkey.obj"))
        super(Renderer, self).__init__(**kwargs)
        with self.canvas:
            self.cb = Callback(self.setup_gl_context)
            PushMatrix()
            self.setup_scene()
            PopMatrix()
            self.cb = Callback(self.reset_gl_context)
        Clock.schedule_interval(self.update_glsl, 1 / 60.)

    def setup_gl_context(self, *args):
        glEnable(GL_DEPTH_TEST)

    def reset_gl_context(self, *args):
        glDisable(GL_DEPTH_TEST)


    def on_touch_down(self, touch):
        super(Renderer, self).on_touch_down(touch)
        self.on_touch_move(touch)


    def on_touch_move(self, touch):

        new_quat = e2q([0.01*touch.dx,0.01*touch.dy,0])

        self.quat = mult(self.quat, new_quat)

        euler_radians = q2e(self.quat)

        self.roll.angle, self.pitch.angle, self.yaw.angle = list2deg(euler_radians)

        print self.roll.angle, self.pitch.angle, self.yaw.angle



    def update_glsl(self, delta):
        asp = self.width / float(self.height)
        proj = Matrix().view_clip(-asp, asp, -1, 1, 1, 100, 1)
        self.canvas['projection_mat'] = proj
        self.canvas['diffuse_light'] = (1.0, 1.0, 0.8)
        self.canvas['ambient_light'] = (0.1, 0.1, 0.1)


    def setup_scene(self):
        Color(1, 1, 1, 1)
        PushMatrix()
        Translate(0, 0, -3)

        self.yaw = Rotate(0, 0, 0, 1)
        self.pitch = Rotate(0, -1, 0, 0)
        self.roll = Rotate(0, 0, 1, 0)


        self.quat = e2q([0,0,0])

        m = list(self.scene.objects.values())[0]
        UpdateNormalMatrix()
        self.mesh = Mesh(
            vertices=m.vertices,
            indices=m.indices,
            fmt=m.vertex_format,
            mode='triangles',
        )
        PopMatrix()


class RendererApp(App):
    def build(self):
        return Renderer()

if __name__ == "__main__":
    RendererApp().run()

Solution

  • In on_touch_down :
    1. Initialize two accumulator variables Dx, Dy = 0, 0
    2. Store the object's current quaternion
  • In on_touch_move :
    1. Increment Dx, Dy using touch.dx, touch.dy
    2. Compute the quaternion from Dx, Dy , not the touch deltas
    3. Set the object's rotation to this quaternion x the stored quaternion

Code:

# only changes are shown here
class Renderer(Widget):
    def __init__(self, **kwargs):
        # as before ...

        self.store_quat = None
        self.Dx = 0
        self.Dy = 0

    def on_touch_down(self, touch):
        super(Renderer, self).on_touch_down(touch)
        self.Dx, self.Dy = 0, 0
        self.store_quat = self.quat

    def on_touch_move(self, touch):
        self.Dx += touch.dx
        self.Dy += touch.dy

        new_quat = e2q([0.01 * self.Dx, 0.01 * self.Dy, 0])
        self.quat = mult(self.store_quat, new_quat)

        euler_radians = q2e(self.quat)
        self.roll.angle, self.pitch.angle, self.yaw.angle = list2deg(euler_radians)

Explanation

The above change might seem unnecessary and counter-intuitive. But first look at it mathematically.

Consider N update calls to on_touch_move , each with delta dx_i, dy_i . Call pitch matrices Rx(angle) and yaw matrices Ry(angle) . The final net rotation change is given by:

  • Your method:

     [Ry(dy_N) * Rx(dx_N)] * ... * [Ry(dy_2) * Rx(dx_2)] * [Ry(dy_1) * Rx(dx_1)] 
  • New method:

     [Ry(dy_N + ... + dy_2 + dy_1)] * [Rx(dx_N + ... + dx_2 + dx_1)] 

Rotation matrices are non-commutative in general, so these expressions are different. Which one is correct?

Consider this simple example. Say you move your finger in a perfect square on the screen, returning to the starting point:

在此处输入图片说明

Each rotation is either horizontal or vertical, and (assumed to be) by 45 degrees. The touchscreen sampling rate is lowered such that each straight line represents one delta sample. One would expect the cube to look the same afterwards as before, right? So what really happens?

在此处输入图片说明

Oh dear.

On the contrary, it is obvious that the new code gives the correct result, since the accumulated Dx, Dy are zero. There may be a way to prove this more generally, but I think the above example suffices to illustrate the issue.

(This was for "clean" inputs too. Imagine a real stream of inputs - human hands are not great at drawing perfectly straight lines without some form of assistance, so the end result would be even more unpredictable.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM