Я до сих пор не уверен на 100%, понял ли я ваш вопрос из-за этого предложения:
Однако теперь при ортогональной проекции дальняя и ближняя плоскости имеют одинаковый размер, поэтому мы не можем вычислить направление курсора таким образом. Направление будет равно оси Z мира. (Я мало спал, поэтому надеюсь, что это имеет смысл).
Если я вас неправильно понял, дайте мне знать в комментариях, и я исправлю или удалю свой ответ.
Однако, если я правильно понял ваше намерение и вы хотите направить луч через усеченную пирамиду (например, чтобы подобрать объекты), то ваше утверждение неверно. Направление будет равно просматривать пространства' отрицательное направление z, а не мировые пространства». Итак, все, что вам нужно сделать, это преобразовать вектор направления или точки ближней и дальней плоскости в мировое пространство. Чтобы доказать, что это работает, я реализовал все в скрипте Python, который вы найдете в конце этого ответа. Если у вас есть интерпретатор Python с установленными MatPlotLib и NumPy, вы можете изменить параметры настройки и немного поэкспериментировать самостоятельно.
Итак, давайте посмотрим на соответствующую реализацию. Сначала мы вычисляем положение мыши в пространстве отсечения и соответствующие 2 точки на ближней и дальней плоскости.
import numpy as np
from mpl_toolkits.mplot3d import Axes3D # noqa: F401 unused import
import matplotlib.pyplot as plt
# setup --------------------------------------------------------------------------------
screen_height = 1080
screen_width = 1980
mouse_pos_x_screen = 500
mouse_pos_y_screen = 300
camera_position = [3, 0, 1]
camera_yaw = 20
camera_pitch = 30
# ----------------
# projection setup
# ----------------
perspective = False # set 'False' for orthogonal and 'True' for perspective projection
z_near_plane = 0.5
z_far_plane = 3
# only orthogonal
frustum_width = 3
frustum_height = 2
# only perspective
field_of_view = 70
aspect_ratio = screen_width / screen_height
# functions ----------------------------------------------------------------------------
def render_frustum(points, camera_pos, ax, right_handed=True):
line_indices = [
[0, 1],
[0, 2],
[0, 4],
[1, 3],
[1, 5],
[2, 3],
[2, 6],
[3, 7],
[4, 5],
[4, 6],
[5, 7],
[6, 7],
]
for idx_pair in line_indices:
line = np.transpose([points[idx_pair[0]], points[idx_pair[1]]])
ax.plot(line[2], line[0], line[1], "g")
if right_handed:
ax.set_xlim([-5, 5])
else:
ax.set_xlim([5, -5])
ax.set_ylim([-5, 5])
ax.set_zlim([-5, 5])
ax.set_xlabel("z")
ax.set_ylabel("x")
ax.set_zlabel("y")
ax.plot([-5, 5], [0, 0], [0, 0], "k")
ax.plot([0, 0], [-5, 5], [0, 0], "k")
ax.plot([0, 0], [0, 0], [-5, 5], "k")
if camera_pos is not None:
ax.scatter(
camera_pos[2], camera_pos[0], camera_pos[1], marker="o", color="b", s=30
)
def render_ray(p0,p1,ax):
ax.plot([p0[2], p1[2]], [p0[0], p1[0]], [p0[1], p1[1]], color="r")
ax.scatter(p0[2], p0[0], p0[1], marker="o", color="r")
def get_perspective_mat(fov_deg, z_near, z_far, aspect_ratio):
fov_rad = fov_deg * np.pi / 180
f = 1 / np.tan(fov_rad / 2)
return np.array(
[
[f / aspect_ratio, 0, 0, 0],
[0, f, 0, 0],
[
0,
0,
(z_far + z_near) / (z_near - z_far),
2 * z_far * z_near / (z_near - z_far),
],
[0, 0, -1, 0],
]
)
def get_orthogonal_mat(width, height, z_near, z_far):
r = width / 2
t = height / 2
return np.array(
[
[1 / r, 0, 0, 0],
[0, 1 / t, 0, 0],
[
0,
0,
-2 / (z_far - z_near),
-(z_far + z_near) / (z_far - z_near),
],
[0, 0, 0, 1],
]
)
def get_rotation_mat_x(angle_rad):
s = np.sin(angle_rad)
c = np.cos(angle_rad)
return np.array(
[[1, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, 1]], dtype=float
)
def get_rotation_mat_y(angle_rad):
s = np.sin(angle_rad)
c = np.cos(angle_rad)
return np.array(
[[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]], dtype=float
)
def get_translation_mat(position):
return np.array(
[
[1, 0, 0, position[0]],
[0, 1, 0, position[1]],
[0, 0, 1, position[2]],
[0, 0, 0, 1],
],
dtype=float,
)
def get_world_to_view_matrix(pitch_deg, yaw_deg, position):
pitch_rad = np.pi / 180 * pitch_deg
yaw_rad = np.pi / 180 * yaw_deg
orientation_mat = np.matmul(
get_rotation_mat_x(-pitch_rad), get_rotation_mat_y(-yaw_rad)
)
translation_mat = get_translation_mat(-1 * np.array(position, dtype=float))
return np.matmul(orientation_mat, translation_mat)
# script -------------------------------------------------------------------------------
mouse_pos_x_clip = mouse_pos_x_screen / screen_width * 2 - 1
mouse_pos_y_clip = mouse_pos_y_screen / screen_height * 2 - 1
mouse_pos_near_clip = np.array([mouse_pos_x_clip, mouse_pos_y_clip, -1, 1], dtype=float)
mouse_pos_far_clip = np.array([mouse_pos_x_clip, mouse_pos_y_clip, 1, 1], dtype=float)
M_wv = get_world_to_view_matrix(camera_pitch, camera_yaw, camera_position)
if perspective:
M_vc = get_perspective_mat(field_of_view, z_near_plane, z_far_plane, aspect_ratio)
else:
M_vc = get_orthogonal_mat(frustum_width, frustum_height, z_near_plane, z_far_plane)
M_vw = np.linalg.inv(M_wv)
M_cv = np.linalg.inv(M_vc)
mouse_pos_near_view = np.matmul(M_cv,mouse_pos_near_clip)
mouse_pos_far_view = np.matmul(M_cv,mouse_pos_far_clip)
if perspective:
mouse_pos_near_view= mouse_pos_near_view / mouse_pos_near_view[3]
mouse_pos_far_view= mouse_pos_far_view / mouse_pos_far_view[3]
mouse_pos_near_world = np.matmul(M_vw, mouse_pos_near_view)
mouse_pos_far_world = np.matmul(M_vw, mouse_pos_far_view)
# calculate view frustum ---------------------------------------------------------------
points_clip = np.array(
[
[-1, -1, -1, 1],
[ 1, -1, -1, 1],
[-1, 1, -1, 1],
[ 1, 1, -1, 1],
[-1, -1, 1, 1],
[ 1, -1, 1, 1],
[-1, 1, 1, 1],
[ 1, 1, 1, 1],
],
dtype=float,
)
points_view = []
points_world = []
for i in range(8):
points_view.append(np.matmul(M_cv, points_clip[i]))
points_view[i] = points_view[i] / points_view[i][3]
points_world.append(np.matmul(M_vw, points_view[i]))
# plot everything ----------------------------------------------------------------------
plt.figure()
plt.plot(mouse_pos_x_screen,mouse_pos_y_screen, marker="o", color="r")
plt.xlim([0, screen_width])
plt.ylim([0, screen_height])
plt.xlabel("x")
plt.ylabel("y")
plt.title("screen space")
plt.figure()
ax_clip_space = plt.gca(projection="3d")
render_ray(mouse_pos_near_clip, mouse_pos_far_clip, ax_clip_space)
render_frustum(points=points_clip, camera_pos=None, ax=ax_clip_space, right_handed=False)
ax_clip_space.set_title("clip space")
plt.figure()
ax_view = plt.gca(projection="3d")
render_ray(mouse_pos_near_view, mouse_pos_far_view, ax_view)
render_frustum(points=points_view, camera_pos=[0, 0, 0], ax=ax_view)
ax_view.set_title("view space")
plt.figure()
ax_world = plt.gca(projection="3d")
render_ray(mouse_pos_near_world, mouse_pos_far_world, ax_world)
render_frustum(points=points_world, camera_pos=camera_position, ax=ax_world)
ax_world.set_title("world space")
plt.show()
Теперь мы получили задействованные матрицы. Мои обозначения здесь следующие: я использую два символа после point_view_near = [x_view, y_view, -z_near, 1]
that are abbreviations of the involved spaces. The first character is the source and the second the target space. The characters are x_view = (x_screen / screen_width - 0.5) * frustum_width
y_view = (y_screen / screen_height - 0.5) * frustum_height
для клипового пространства, M_vw
for view space, and [0, 0, -1, 0]
для мирового пространства. Так cWorld = Vector4(cWorld.x, cWorld.y, 0, 0);
is the view space to clip space transformation a.k.a the projection matrix.
cView = Vector4(cView.x, cView.y, 0, 0);
Теперь я просто использую правильные матрицы преобразования для преобразования клипа в мировое пространство. Обратите внимание, что перспективная проекция требует деления на Vector4 cScreen = Vector4(cursorNormX, cursorNormY, -1, 1);
after the transformation to view space. This is not necessary for the orthographic projection, but performing it does not affect the result.
Vector4 cScreen = Vector4(cursorNormX, cursorNormY, 0, 0);
Vector4 cView = Inverse(projection)*cScreen;
cView = Vector4(cView.x, cView.y, 0, 0);
Vector4 cWorld = Inverse(View) * cView;
cWorld = Vector4(cWorld.x, cWorld.y, 0, 0);
Vector3 cursorPos = cWorld.xyz();
Насколько я вижу, это идентично вашему первому разделу кода. Теперь давайте посмотрим на результат для перспективной и ортогональной проекции со следующими параметрами настройки:
screen_height = 1080
screen_width = 1980
mouse_pos_x_screen = 500
mouse_pos_y_screen = 300
camera_position = [3, 0, 1]
camera_yaw = 20
camera_pitch = 30
z_near_plane = 0.5
z_far_plane = 3
# only orthogonal
frustum_width = 3
frustum_height = 2
# only perspective
field_of_view = 70
aspect_ratio = screen_width / screen_height
Значения пространства экрана и пространства отсечения идентичны для обеих проекций:
Красная линия соединяет две точки на ближней и дальней плоскости. Красная точка — это точка на ближней плоскости, которая является вашим «экраном» в трехмерном пространстве. Зеленые линии обозначают границы усеченной пирамиды. В пространстве клипа это, очевидно, просто куб. Важно понимать, что пространство отсечения определяется в левой системе координат, в то время как другие системы координат обычно являются правыми (взгляните на изображения в эта ссылка). Я упоминаю об этом, потому что у меня были некоторые проблемы с сюжетами, пока я это не осознал.
Теперь для перспективной проекции я получаю следующие графики:
Синяя точка — это положение камеры.
Если я просто заменю матрицу перспективы на матрицу ортогональной проекции, результаты будут выглядеть так:
Как видите, подход, который вы использовали в первом разделе кода, работает независимо от выбранной проекции. Я не знаю, почему ты думал, что это не так. Я предполагаю, что вы могли допустить небольшую ошибку при реализации матрицы ортогональной проекции. Например, если вы случайно перевернули строки и столбцы (транспонировали) матрицы ортогональной проекции, вы получите такую ерунду:
Я знаю, что это выглядит как неправильная реализация перспективной проекции, но именно это я получаю, когда транспонирую матрицу ортогональной проекции перед умножением.Поэтому убедитесь, что вы используете правильную матрицу ортогональной проекции ():
источник
$$
\begin{bmatrix}
mouse_pos_near_view = np.matmul(M_cv, mouse_pos_near_clip)
mouse_pos_far_view = np.matmul(M_cv, mouse_pos_far_clip)
if perspective:
mouse_pos_near_view= mouse_pos_near_view / mouse_pos_near_view[3]
mouse_pos_far_view= mouse_pos_far_view / mouse_pos_far_view[3]
mouse_pos_near_world = np.matmul(M_vw, mouse_pos_near_view)
mouse_pos_far_world = np.matmul(M_vw, mouse_pos_far_view)
\frac{2}{w}&0&0&0\\ w
as initial vector.
0&\frac{2}{h}&0&0\\ M_wv = get_world_to_view_matrix(camera_pitch, camera_yaw, camera_position)
if perspective:
M_vc = get_perspective_mat(field_of_view, z_near_plane, z_far_plane, aspect_ratio)
else:
M_vc = get_orthogonal_mat(frustum_width, frustum_height, z_near_plane, z_far_plane)
M_vw = np.linalg.inv(M_wv)
M_cv = np.linalg.inv(M_vc)
is, that your z-component should be identical to your near plane value and not zero. You might get away with that since it would just shift your point a little bit in the camera viewing direction in world space, but more problematic is that you set w to 0. This makes it impossible to apply any translation to the vector by $4 \times 4$ matrix multiplication. So when you transform to world space, you will always end up with a point that treats the camera to be located at the coordinate system origin, regardless of its true position. So you need to set the w-component to 1. However, if the previous lines are correct, you should automatically get the correct z- and w-values which makes this line obsolete.
0&0&\frac{-2}{f-n}&-\frac{f+n}{f-n}\\ M_vc
doesn't make much sense either to me. Your camera is somewhere in 3d world space. Why do you remove the z-component you previously calculated? With this, you move the point into the XY-plane for no reason. Just remove this line.
0&0&0&1 w
with the view-to-world matrix ( v
\end{bmatrix}
$$
Здесь $w$ — ширина усеченной пирамиды, $h$ — высота усеченной пирамиды, $f$ — значение z в дальней плоскости и $n$ — значение z в ближней плоскости. Это представление, если вы используете векторы-столбцы и матрицы, умноженные слева. Для векторов-строк и матриц, умноженных справа, вам необходимо их транспонировать.
c
Ваш второй подход:
M_
имеет множество проблем, и все они связаны с z- и w-компонентами ваших векторов. По сути, вам нужно выполнить те же преобразования, что и в первом подходе. Так что используйте
Одна проблема на линии
mouse_pos_x_clip = mouse_pos_x_screen / screen_width * 2 - 1
mouse_pos_y_clip = mouse_pos_y_screen / screen_height * 2 - 1
mouse_pos_near_clip = np.array([mouse_pos_x_clip, mouse_pos_y_clip, -1, 1], dtype=float)
mouse_pos_far_clip = np.array([mouse_pos_x_clip, mouse_pos_y_clip, 1, 1], dtype=float)