Python:在 kivy 中旋转 3D 对象(不需要的倾斜)
Python: rotating 3D objects in kivy (unwanted tilt)
我想做一个简单的 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()
解决方案
- 在
on_touch_down
中:
- 初始化两个累加器变量
Dx, Dy = 0, 0
- 存储对象的当前四元数
- 在
on_touch_move
中:
- 使用
touch.dx, touch.dy
增加 Dx, Dy
- 从
Dx, Dy
计算四元数,不是 touch
增量
- 将对象的旋转设置为此四元数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)]
旋转矩阵一般是不可交换的,所以这些表达式是不同的。哪一个是正确的?
考虑这个简单的例子。假设您在屏幕上以完美正方形移动手指,返回起点:
每次旋转要么是水平的,要么是垂直的,并且(假设是)45 度。降低触摸屏采样率,使每条直线代表 一个 增量样本。人们会期望立方体之后看起来和以前一样,对吧?那么到底发生了什么?
哦,亲爱的。
相反,显然新代码给出了正确的结果,因为累加的Dx, Dy
为零。可能有一种方法可以更普遍地证明这一点,但我认为上面的例子足以说明这个问题。
(这也适用于 "clean" 输入。想象一下真实的输入流 - 如果没有某种形式的帮助,人的手并不擅长画出完美的直线,因此最终结果将更加难以预测。 )
我想做一个简单的 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()
解决方案
- 在
on_touch_down
中:- 初始化两个累加器变量
Dx, Dy = 0, 0
- 存储对象的当前四元数
- 初始化两个累加器变量
- 在
on_touch_move
中:- 使用
touch.dx, touch.dy
增加 - 从
Dx, Dy
计算四元数,不是touch
增量 - 将对象的旋转设置为此四元数x存储的四元数
Dx, Dy
- 使用
代码:
# 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)]
旋转矩阵一般是不可交换的,所以这些表达式是不同的。哪一个是正确的?
考虑这个简单的例子。假设您在屏幕上以完美正方形移动手指,返回起点:
每次旋转要么是水平的,要么是垂直的,并且(假设是)45 度。降低触摸屏采样率,使每条直线代表 一个 增量样本。人们会期望立方体之后看起来和以前一样,对吧?那么到底发生了什么?
哦,亲爱的。
相反,显然新代码给出了正确的结果,因为累加的Dx, Dy
为零。可能有一种方法可以更普遍地证明这一点,但我认为上面的例子足以说明这个问题。
(这也适用于 "clean" 输入。想象一下真实的输入流 - 如果没有某种形式的帮助,人的手并不擅长画出完美的直线,因此最终结果将更加难以预测。 )