Proper way to handle camera rotations
让我们从考虑 2 种类型的摄像机旋转开始:
相机围绕一个点旋转(Rails):
1
2 3 4 5 6 7 8 9 10 11 |
def rotate_around_target(self, target, delta):
right = (self.target – self.eye).cross(self.up).normalize() amount = (right * delta.y + self.up * delta.x) self.target = target self.up = self.original_up self.eye = ( mat4.rotatez(amount.z) * mat4.rotatey(amount.y) * mat4.rotatex(amount.x) * vec3(self.eye) ) |
相机旋转目标(FPS)
1
2 3 4 5 6 7 8 9 |
def rotate_target(self, delta):
right = (self.target – self.eye).cross(self.up).normalize() self.target = ( mat4.translate(self.eye) * mat4().rotate(delta.y, right) * mat4().rotate(delta.x, self.up) * mat4.translate(–self.eye) * self.target ) |
然后只是一个更新函数,其中投影/视图矩阵是从眼睛/目标/向上相机向量中计算出来的:
1
2 3 4 5 |
def update(self, aspect):
self.view = mat4.lookat(self.eye, self.target, self.up) self.projection = mat4.perspective_fovx( self.fov, aspect, self.near, self.far ) |
当相机视图方向平行于上轴(此处为 z-up)时,这些旋转函数会出现问题……此时相机的行为非常糟糕,所以我会遇到诸如:
所以我的问题是,我怎样才能调整上面的代码,以便相机进行完整旋转,而最终结果在某些边缘点看起来很奇怪(相机轴翻转:/)?
我希望拥有与许多 DCC 软件包(3dsmax、maya、…)相同的行为,它们可以完全旋转而不会出现任何奇怪的行为。
编辑:
对于那些想试一试数学的人,我决定创建一个能够重现已解释问题的真正简约版本:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
import math
from ctypes import c_void_p import numpy as np import glm class Camera(): def __init__( def update(self, aspect): def rotate_target(self, delta): def rotate_around_target(self, target, delta): def rotate_around_origin(self, delta): class GlutController(): FPS = 0 def __init__(self, camera, velocity=100, velocity_wheel=100): def glut_mouse(self, button, state, x, y): if button == GLUT_LEFT_BUTTON: def glut_motion(self, x, y): if self.mode == self.FPS: class MyWindow: def __init__(self, w, h): glutInit() self.startup() glutReshapeFunc(self.reshape) def startup(self): aspect = self.width / self.height def run(self): def idle_func(self): def reshape(self, w, h): def display(self): glClearColor(0.2, 0.3, 0.3, 1.0) glMatrixMode(GL_PROJECTION) glBegin(GL_LINES) glutSwapBuffers()
if __name__ == ‘__main__’: |
为了运行它,你需要安装 pyopengl 和 pyglm
- 这就是为什么您需要基于四元数的旋转。您面临的问题是通过万向节锁来解释的。
- 我多次听说过云台锁,但我从未完全理解其背后的数学原理。使用双亲旋转时,您可能最终会得到 6 种可能的组合 {xyz, xzy, yxz, yzx, zxy, zyx},在所有这些组合中,都会出现某些情况下会发生云台锁定,在发布的代码中,您如何识别会发生云台锁吗?但最有趣的问题是,如何更新代码以使用四元数?您可以假设存在典型的四元数类
- 如果中间角为 90 度,则会发生 Gimabl 锁定。但这只是一个旁注。不需要四元数来解决这个问题。问题的根本原因是您无缘无故地使用了一些 \\’lookAt` 函数。您所需要的只是一个相机位置(通常是一个矢量)和它的 3d 方向(表示为旋转矩阵或四元数或任何您认为合适的)。因此,您可以轻松地从两者创建视图矩阵,并且可以单独调整这两个组件。
- 这里的问题是,修复 lookAt 方法函数的唯一解决方案意味着不仅要旋转 target 向量,还要旋转 up 向量 – 为此,您需要一些理智的表示相机方向(mat 或 quat),但如果有,则根本不需要 lookAt。
- ammount = (right * delta.y + self.up * delta.x) – 尝试组合围绕两个轴的旋转的奇怪方式,当然是不正确的。 FPS 相机中的矩阵代码产生更好的结果并且可以进行调整。
我建议在视图空间中绕轴旋转
你必须知道视图矩阵(V)。由于视图矩阵在 self.eye、self.target 和 self.up 中编码,因此必须通过 lookAt 计算:
1
|
V = glm.lookAt(self.eye, self.target, self.up)
|
计算视图空间中的pivot,旋转angular和旋转轴。在这种情况下,轴是右旋转方向,其中 y 轴必须翻转:
1
2 3 |
pivot = glm.vec3(V * glm.vec4(target.x, target.y, target.z, 1))
axis = glm.vec3(-delta.y, -delta.x, 0) angle = glm.length(delta) |
设置旋转矩阵R并计算围绕枢轴RP的比率矩阵。最后通过旋转矩阵变换视图矩阵(V)。结果是新的视图矩阵 NV:
1
2 3 |
R = glm.rotate( glm.mat4(1), angle, axis )
RP = glm.translate(glm.mat4(1), pivot) * R * glm.translate(glm.mat4(1), -pivot) NV = RP * V |
从新的视图矩阵NV中解码self.eye、self.target和self.up:
1
2 3 4 5 |
C = glm.inverse(NV)
targetDist = glm.length(self.target – self.eye) self.eye = glm.vec3(C[3]) self.target = self.eye – glm.vec3(C[2]) * targetDist self.up = glm.vec3(C[1]) |
方法的完整编码 rotate_around_target_view:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def rotate_around_target_view(self, target, delta):
V = glm.lookAt(self.eye, self.target, self.up) pivot = glm.vec3(V * glm.vec4(target.x, target.y, target.z, 1)) R = glm.rotate( glm.mat4(1), angle, axis ) C = glm.inverse(NV) |
最后它可以围绕世界的原点和眼睛的位置甚至任何其他点进行旋转。
1
2 3 4 5 |
def rotate_around_origin(self, delta):
return self.rotate_around_target_view(glm.vec3(0), delta) def rotate_target(self, delta): |
或者,可以在模型的世界空间中执行旋转。解决方案非常相似。
旋转是在世界空间中完成的,因此枢轴不必转换到视图空间,并且旋转应用于视图矩阵(NV = V * RP)之前:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
def rotate_around_target_world(self, target, delta):
V = glm.lookAt(self.eye, self.target, self.up) pivot = target R = glm.rotate( glm.mat4(1), angle, axis ) C = glm.inverse(NV) def rotate_around_origin(self, delta): |
1
2 3 4 5 |
def rotate_around_target(self, target, delta):
if abs(delta.x) > 0: self.rotate_around_target_world(target, glm.vec3(delta.x, 0.0, 0.0)) if abs(delta.y) > 0: self.rotate_around_target_view(target, glm.vec3(0.0, delta.y, 0.0)) |
我为了实现微创的方法,考虑到问题的原始代码,我会提出以下建议:
-
操作后视图的目标应该是函数rotate_around_target的输入参数target。
-
水平鼠标移动应该围绕世界的向上矢量旋转视图
-
鼠标垂直移动应该围绕当前水平轴倾斜视图
我想出了以下方法:
计算当前视线(los)、上向量(up)和水平轴(right)
通过将上向量投影到由原始上向量和当前视线给定的平面上,使上向量垂直。这是通过 Gram-Schmidt 正交化实现的。
围绕当前水平轴倾斜。这意味着 los 和 up 围绕 right 轴旋转。
围绕向上矢量旋转。 los 和 right 围绕 up 旋转。
Calculate 设置并计算眼睛和目标位置,其中目标由输入参数target设置:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
def rotate_around_target(self, target, delta):
# get directions # upright up vector (Gram–Schmidt orthogonalization) # tilt around horizontal axis # rotate around up vector # set eye, target and up |
- 首先非常感谢您的回答!我很高兴您尝试修复我的代码而不使用评论中建议的任何替代相机表示,我发现在许多情况下,在不解码视图矩阵的情况下明确存储眼睛/目标/向上向量非常方便。也就是说,我已经在发布的代码段和我的引擎中测试了你的代码,我得到的结果很奇怪……请看看这个视频。如果您将代码插入 mcve,您应该得到相同的结果…… :(
- 再次感谢…但是您的方法对我来说仍然不合适(可能是因为向上向量)?无论如何,我邀请您进行下一个测试,首先运行这个小脚本设置行#167 到 mode=0,玩弄(这是正确的),然后将那行 #167 切换到 mode=1(你的方法)。 ..比较两者,然后请让我知道我是否能够正确解释我的担忧…同时,我将测试@skatic的答案给出的解决方案
- 最后一个问题,你能解释一下这两行 axis = vec3(-delta.y, -delta.x, 0) 和 angle = delta.length() 吗?在我的引擎中问你,我通常处理两种类型的相机,有时我有 y-up 相机,但在其他情况下,z-up 相机。对于最后一种情况,我需要稍微调整您的代码。
- @BPL 1. delta 是”拖动”方向。 A (x, y) 可以通过 (y, -x) 向右旋转 90 度。 Y 必须翻转,因为”OpenGL”原点在左下角,但窗口(鼠标)原点在左上角。所以方向 drag 的旋转轴是 flip = -1 vec3(flip * delta.y, -delta.x, 0)。 2.我认为旋转angular编码为drag的长度。 angle = delta.length() 是我的一个假设。
- 很抱歉回到这个,但是……无论相机向上是 [0,1,0] 还是 [0,0,1],你将如何完成这项工作。您的方法适用于 [0,1,0] 但在使用 [0,0,1] 时表现非常奇怪:/
- 当我导出 3ds max 几何体(这是一个带有 z world up 的 RHS 系统)时,我不交换
以下是该线程中提供的所有答案的小总结:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 |
from OpenGL.GL import *
from OpenGL.GLU import * from OpenGL.GLUT import * import glm class Camera(): def __init__( def update(self, aspect): def zoom(self, *args): def load_projection(self): glMatrixMode(GL_PROJECTION) def load_modelview(self): glMatrixMode(GL_MODELVIEW) class CameraSkatic(Camera): def rotate_around_target(self, target, delta): self.target = target def rotate_around_origin(self, delta): class CameraBPL(Camera): def rotate_target(self, delta): def rotate_around_target(self, target, delta): def rotate_around_origin(self, delta): class CameraRabbid76_v1(Camera): def rotate_around_target_world(self, target, delta): pivot = target R = glm.rotate(glm.mat4(1), angle, axis) C = glm.inverse(NV) def rotate_around_target_view(self, target, delta): pivot = glm.vec3(V * glm.vec4(target.x, target.y, target.z, 1)) R = glm.rotate(glm.mat4(1), angle, axis) C = glm.inverse(NV) def rotate_around_target(self, target, delta): def rotate_around_origin(self, delta): def rotate_target(self, delta): class CameraRabbid76_v2(Camera): def rotate_around_target(self, target, delta): # get directions # upright up vector (Gram–Schmidt orthogonalization) # tilt around horizontal axis # rotate around up vector # set eye, target and up def rotate_around_origin(self, delta): def rotate_target(self, delta): class GlutController(): FPS = 0 def __init__(self, camera, velocity=100, velocity_wheel=100): def glut_mouse(self, button, state, x, y): if button == GLUT_LEFT_BUTTON: def glut_motion(self, x, y): if self.mode == self.FPS: def glut_mouse_wheel(self, *args):
def render_text(x, y, text):
def draw_plane_yup(): glBegin(GL_LINES) glColor3f(1, 0, 0)
def draw_plane_zup(): glBegin(GL_LINES) glColor3f(1, 0, 0)
def line(p0, p1, color=None):
def grid(segment_count=10, spacing=1, yup=True): data = [] glBegin(GL_LINES)
def axis(size=1.0, yup=True): class MyWindow: def __init__(self, w, h): glutInit() self.startup() glutReshapeFunc(self.reshape) def keyboard_func(self, *args): if key ==“\\x1b”: if key in [‘1’, ‘2’, ‘3’, ‘4’]: self.camera = self.cameras[self.index_camera] if key in [‘o’, ‘p’]: if key == ‘o’: self.camera.target = glm.vec3(0, 0, 0) except Exception as e: def startup(self): aspect = self.width / self.height def run(self): def idle_func(self): def reshape(self, w, h): def display(self): glClearColor(0.2, 0.3, 0.3, 1.0) self.camera.load_projection() glLineWidth(5) glMatrixMode(GL_PROJECTION) info =“\ glutSwapBuffers()
if __name__ == ‘__main__’: |
有这么多重新发明轮子的方法不是吗?这是一个简洁的选项(改编自 Opengl Development Cookbook,M.M.Movania,第 2 章中的目标相机概念):
首先创建新的方向(旋转)矩阵(更新为使用累积的鼠标增量)
1
2 3 4 5 6 7 8 9 10 11 12 |
# global variables somewhere appropriate (or class variables)
mouseX = 0.0 mouseY = 0.0 def rotate_around_target(self, target, delta): global mouseX global mouseY mouseX += delta.x/5.0 mouseY += delta.y/5.0 glm::mat4 M = glm::mat4(1) M = glm::rotate(M, delta.z, glm::vec3(0, 0, 1)) M = glm::rotate(M, mouseX , glm::vec3(0, 1, 0)) M = glm::rotate(M, mouseY, glm::vec3(1, 0, 0)) |
利用距离得到一个向量,然后通过当前的旋转矩阵平移这个向量
1
2 3 4 |
self.target = target
float distance = glm::distance(self.target, self.eye) glm::vec3 T = glm::vec3(0, 0, distance) T = glm::vec3(M*glm::vec4(T, 0.0f)) |
通过将平移向量添加到目标位置来获取新的相机眼睛位置
1
|
self.eye = self.target + T
|
重新计算正交基(你只需要做 UP 向量)
1
2 3 4 |
# assuming self.original_up = glm::vec3(0, 1, 0)
self.up = glm::vec3(M*glm::vec4(self.original_up, 0.0f)) # or self.up = glm::vec3(M*glm::vec4(glm::vec3(0, 1, 0), 0.0f)) |
5…然后您可以通过使用lookAt函数更新视图矩阵来尝试一下
1
|
self.view = glm.lookAt( self.eye, self.target, self.up)
|
这是迄今为止我发现的这类转换问题/解决方案中最简单的概念。我在 C/C 中对其进行了测试,并为您将其修改为 pyopengl 语法(我真诚地希望如此)。让我们知道(或不)进展如何。
来源:https://www.codenong.com/54027740/