Python: вращение трехмерных объектов в киве (нежелательный наклон) - PullRequest
0 голосов
/ 04 сентября 2018

Я хочу сделать что-то вроде простого средства просмотра 3D (возможно, редактор). Поэтому одна из моих целей - научиться вращать трехмерные объекты, например, с помощью мыши.

Я взял пример "3D Rotating Monkey Head" и изменил некоторый код в файле main.py.

Я использовал функции преобразования углов Эйлера в кватернионы и обратно - так что я достиг ближайшего результата.

Так что приложение работает почти так, как и должно (демонстрация gif на imgur)

Но есть одна досадная проблема - нежелательное вращение вдоль оси z (наклон?). Вы можете увидеть это здесь (демо GIF на imgur)

Очевидно, что это не должно быть так.

Есть ли способ избавиться от этого наклона?

гл и кватернионы - новые темы для меня. Может быть, я сделал что-то не так.

Мой код здесь (только 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 голосов
/ 06 сентября 2018

Решение

  • В 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 равны нулю. Может быть способ доказать это в более общем плане, но я думаю, что приведенного выше примера достаточно для иллюстрации проблемы.

(Это было и для «чистых» входных данных. Представьте себе реальный поток входных данных - человеческие руки не очень хороши в рисовании совершенно прямых линий без какой-либо помощи, поэтому конечный результат будет еще более непредсказуемым.)

...