Python:以猕猴桃旋转3D对象(不需要的倾斜)

时间:2018-09-04 18:15:01

标签: python 3d rotation kivy euler-angles

我想制作一个简单的3D查看器(也许是编辑器)。 因此,我的目标之一就是学习如何使用鼠标旋转3D对象。

我拿了"3D Rotating Monkey Head" example并更改了main.py文件中的一些代码。

我使用了将欧拉角转换为四元数并返回的功能-因此获得了最接近的结果。

因此该应用可以正常运行almost as it should (demo gif on imgur)

但是有一个烦人的问题-沿z轴的不必要旋转(倾斜?)。 您可以看到此here (demo gif on imgur)

很明显,事实并非如此。

有没有办法消除这种倾斜?

gl和四元数对我来说是新话题。也许我做错了。

我的代码在这里(仅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()

1 个答案:

答案 0 :(得分:1)

解决方案

  • on_touch_down中:
    1. 初始化两个累加器变量Dx, Dy = 0, 0
    2. 存储对象的当前四元数
  • on_touch_move中:
    1. 使用Dx, Dy来增加touch.dx, touch.dy
    2. Dx, Dy计算四元数,而不是touch三角洲
    3. 将对象的旋转设置为该四元数 x 存储的四元数

代码:

# 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)

说明

上述更改似乎是不必要和违反直觉的。但是首先要从数学上看它。

考虑N更新对on_touch_move的调用,每个调用都有增量dx_i, dy_i。调用音高矩阵Rx(angle)和偏航矩阵Ry(angle)。最终的净旋转量更改为:

  • 您的方法:

    [Ry(dy_N) * Rx(dx_N)] * ... * [Ry(dy_2) * Rx(dx_2)] * [Ry(dy_1) * Rx(dx_1)]
    
  • 新方法:

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

旋转矩阵通常是不可交换的,因此这些表达式是不同的。哪个是正确的?

考虑这个简单的例子。假设您将手指移动到屏幕上的完美正方形,然后回到起点:

enter image description here

每个旋转都是水平旋转或垂直旋转(假定为45度)。降低触摸屏采样率,以使每条直线代表一个增量采样。人们会期望该多维数据集之后看起来和以前一样,对吗?那么到底发生了什么?

enter image description here

哦,亲爱的。

相反,很明显,新代码给出了正确的结果,因为累积的Dx, Dy为零。也许可以有一种更普遍的方法来证明这一点,但我认为以上示例足以说明问题。

(这也适用于“干净”的输入。想像一下真正的输入流-如果没有某种形式的帮助,人的手不会擅长绘制完美的直线,因此最终结果将更加不可预测。)