Ray-box Теория пересечений - PullRequest
       92

Ray-box Теория пересечений

20 голосов
/ 02 апреля 2010

Я хочу определить точку пересечения между лучом и коробкой. Поле определяется его минимальной трехмерной координатой и максимальной трехмерной координатой, а луч определяется его происхождением и направлением, на которое он указывает.

В настоящее время я формирую плоскость для каждой грани коробки и пересекаю луч с плоскостью. Если луч пересекает плоскость, то я проверяю, действительно ли точка пересечения находится на поверхности прямоугольника. Если это так, я проверяю, является ли это ближайшим пересечением для этого луча, и возвращаю самое близкое пересечение.

То, как я проверяю, находится ли точка пересечения плоскости на поверхности самого ящика, осуществляется с помощью функции

bool PointOnBoxFace(R3Point point, R3Point corner1, R3Point corner2)
{
  double min_x = min(corner1.X(), corner2.X());
  double max_x = max(corner1.X(), corner2.X());
  double min_y = min(corner1.Y(), corner2.Y());
  double max_y = max(corner1.Y(), corner2.Y());
  double min_z = min(corner1.Z(), corner2.Z());
  double max_z = max(corner1.Z(), corner2.Z());
  if(point.X() >= min_x && point.X() <= max_x && 
     point.Y() >= min_y && point.Y() <= max_y &&
     point.Z() >= min_z && point.Z() <= max_z)
     return true;

  return false;
}

, где corner1 - это один угол прямоугольника для этой грани коробки, а corner2 - противоположный угол. Моя реализация работает большую часть времени, но иногда она дает мне неправильное пересечение. Пожалуйста, смотрите изображение:

alt text

На изображении показаны лучи, исходящие от глаза камеры и падающие на поверхность коробки. Другие лучи являются нормалью к поверхности коробки. Можно видеть, что один отдельный луч (на самом деле это нормаль, которая видна) выходит из «тыльной стороны» коробки, тогда как нормаль должна исходить из верхней части окна. Это кажется странным, поскольку есть несколько других лучей, которые правильно попадают на верхнюю часть окна.

Мне было интересно, правильный ли способ проверки, находится ли точка пересечения на коробке, или мне следует использовать какой-то другой алгоритм.

Спасибо.

Ответы [ 5 ]

15 голосов
/ 02 апреля 2010

Увеличение количества эпсилонами на самом деле не является хорошим способом сделать это, так как теперь у вас есть граница размера эпсилон на краю вашей коробки, через которую могут проходить лучи. Таким образом, вы избавитесь от этого (относительно распространенного) странного набора ошибок и в итоге получите еще один (более редкий) набор странных ошибок.

Я предполагаю, что вы уже представляете, что ваш луч движется с некоторой скоростью вдоль своего вектора, и находите время пересечения с каждой плоскостью. Так, например, если вы пересекаете плоскость в x=x0, а ваш луч движется в направлении (rx,ry,rz) от (0,0,0), тогда время пересечения равно t = x0/rx. Если t отрицательно, игнорируйте его - вы идете другим путем. Если t равно нулю, вы должны решить, как обращаться с этим особым случаем - если вы уже находитесь в самолете, вы отскакиваете от него или проходите через него? Вы также можете обращаться с rx==0 как с особым случаем (чтобы вы могли поразить край коробки).

В любом случае, теперь у вас есть именно те координаты, где вы ударили по этой плоскости: они (t*rx , t*ry , t*rz). Теперь вы можете просто прочитать, находятся ли t*ry и t*rz внутри прямоугольника, в котором они должны находиться (то есть между минимальным и максимальным для куба вдоль этих осей). Вы не проверяете координату x, потому что вы уже знаете, что попали в нее Опять же, вы должны решить, следует ли / как обрабатывать удары по углам как особый случай. Кроме того, теперь вы можете упорядочить столкновения с различными поверхностями по времени и выбрать первую в качестве точки столкновения.

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

Таким образом, вам просто нужны три функции, такие как та, что у вас уже есть: одна для проверки, нажали ли вы в yz, если вы нажали x, и соответствующие функции для xz и xy, если вы нажмите y и z соответственно.


Редактировать: код, добавленный (многословно), показывает, как по-разному выполнять тесты для каждой оси:

#define X_FACE 0
#define Y_FACE 1
#define Z_FACE 2
#define MAX_FACE 4

// true if we hit a box face, false otherwise
bool hit_face(double uhit,double vhit,
                 double umin,double umax,double vmin,double vmax)
{
  return (umin <= uhit && uhit <= umax && vmin <= vhit && vhit <= vmax);
}

// 0.0 if we missed, the time of impact otherwise
double hit_box(double rx,double ry, double rz,
                double min_x,double min_y,double min_z,
                double max_x,double max_y,double max_z)
{
  double times[6];
  bool hits[6];
  int faces[6];
  double t;
  if (rx==0) { times[0] = times[1] = 0.0; }
  else {
    t = min_x/rx;
    times[0] = t; faces[0] = X_FACE; 
    hits[0] = hit_box(t*ry , t*rz , min_y , max_y , min_z , max_z);
    t = max_x/rx;
    times[1] = t; faces[1] = X_FACE + MAX_FACE;
    hits[1] = hit_box(t*ry , t*rz , min_y , max_y , min_z , max_z);
  }
  if (ry==0) { times[2] = times[3] = 0.0; }
  else {
    t = min_y/ry;
    times[2] = t; faces[2] = Y_FACE;
    hits[2] = hit_box(t*rx , t*rz , min_x , max_x , min_z , max_z);
    t = max_y/ry;
    times[3] = t; faces[3] = Y_FACE + MAX_FACE;
    hits[3] = hit_box(t*rx , t*rz , min_x , max_x , min_z , max_z);
  }
  if (rz==0) { times[4] = times[5] = 0.0; }
  else {
    t = min_z/rz;
    times[4] = t; faces[4] = Z_FACE;
    hits[4] = hit_box(t*rx , t*ry , min_x , max_x , min_y , max_y);
    t = max_z/rz;
    times[5] = t; faces[5] = Z_FACE + MAX_FACE;
    hits[5] = hit_box(t*rx , t*ry , min_x , max_x , min_y , max_y);
  }
  int first = 6;
  t = 0.0;
  for (int i=0 ; i<6 ; i++) {
    if (times[i] > 0.0 && (times[i]<t || t==0.0)) {
      first = i;
      t = times[i];
    }
  }
  if (first>5) return 0.0;  // Found nothing
  else return times[first];  // Probably want hits[first] and faces[first] also....
}

(Я просто набрал это, не скомпилировал, так что остерегайтесь ошибок.) (Изменить: только что исправил i -> first.)

В любом случае, дело в том, что вы обрабатываете три направления по отдельности и проверяете, произошло ли воздействие в пределах правого поля в координатах (u, v), где (u, v) равны (x, y) , (x, z) или (y, z) в зависимости от того, в какую плоскость вы попали.

2 голосов
/ 02 апреля 2010

PointOnBoxFace должна быть двухмерной проверкой, а не трехмерной. Например, если вы проводите тестирование на плоскости z = z_min, вам нужно только сравнить x и y с их соответствующими границами. Вы уже поняли, что координата z верна. Точность с плавающей точкой, вероятно, сбивает вас с толку, когда вы «перепроверяете» третью координату.

Например, если z_min равно 1,0, вы сначала проверяете эту плоскость. Вы найдете точку пересечения (x, y, 0.999999999). Теперь, даже если x и y находятся в пределах, z не совсем подходит.

0 голосов
/ 02 апреля 2010

РЕДАКТИРОВАТЬ: игнорировать этот ответ (см. Комментарии ниже, где мне совершенно убедительно показали ошибку моих путей).

Вы проверяете, находится ли точка внутри тома, но точка находится на периферии этого тома, поэтому вы можете обнаружить, что это «бесконечно малое» расстояние вне тома. Попробуйте вырастить коробку с помощью небольшого эпсилона, например ::1003

double epsilon = 1e-10; // Depends the scale of things in your code.
double min_x = min(corner1.X(), corner2.X()) - epsilon;
double max_x = max(corner1.X(), corner2.X()) + epsilon;
double min_y = min(corner1.Y(), corner2.Y()) - epsilon;
...

Технически, правильный способ сравнения почти равных чисел состоит в том, чтобы привести их битовые представления к целым и сравнить целые числа для некоторого небольшого смещения, например, в C:

#define EPSILON 10 /* some small int; tune to suit */
int almostequal(double a, double b) {
    return llabs(*(long long*)&a - *(long long*)&b) < EPSILON;
}

Конечно, это не так просто в C #, но, возможно, небезопасная семантика может достичь того же эффекта. (Спасибо @Novox за его комментарии, которые привели меня к прекрасной странице , подробно объясняющей эту технику.)

0 голосов
/ 02 апреля 2010

Может ли быть так, что этот луч проходит точно через край коробки? Ошибки округления с плавающей запятой могут привести к тому, что он будет пропущен как правой, так и задней стороной.

0 голосов
/ 02 апреля 2010

Код выглядит нормально. Попробуйте найти этот конкретный луч и отладить его.

...