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
我建议在视图空间中绕轴旋转
你必须知道视图矩阵(
1
|
V = glm.lookAt(self.eye, self.target, self.up)
|
计算视图空间中的
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) |
设置旋转矩阵
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 |
从新的视图矩阵
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]) |
方法的完整编码
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): |
或者,可以在模型的世界空间中执行旋转。解决方案非常相似。
旋转是在世界空间中完成的,因此枢轴不必转换到视图空间,并且旋转应用于视图矩阵(
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 。 -
水平鼠标移动应该围绕世界的向上矢量旋转视图
-
鼠标垂直移动应该围绕当前水平轴倾斜视图
我想出了以下方法:
计算当前视线(
通过将上向量投影到由原始上向量和当前视线给定的平面上,使上向量垂直。这是通过 Gram-Schmidt 正交化实现的。
围绕当前水平轴倾斜。这意味着
围绕向上矢量旋转。
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 |
以下是该线程中提供的所有答案的小总结:
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 语法(我真诚地希望如此)。让我们知道(或不)进展如何。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/268004.html