Всем привет. Недавно я занялся нахождением возможности бросить луч не используя классический метод с перемножением обратных матриц. Меня эта идея зацепила и я стал исследовать, возможно ли как-то сделать то же самое, но без обратных матриц. И вот что получилось. Есть видео и также описание код приложу в туториал.

Начнём с того, что нам понадобятся нормали сторон камеры. Я приведу свой пример из кода. Вот как выглядит получение нормалей всех сторон камеры. Код на C.

void camera_commit (struct camera *cam)
{
    float p[3];
    float f[3];
    vec3_add (p, cam->pos, cam->front);
    vec3_cross (f, cam->front, cam->up);
    vec3_norm (cam->right, f);
    vec3_cross (cam->real_up, cam->front, cam->right);

    lookat(cam->view, cam->pos, p, cam->up);
}

Для вычислений позиции X на расстоянии нам понадобиться масштабирование экрана, которое вычисляется по формуле:

\frac{aspect + 1}{1 + \frac{aspect}{2}}

Идея очень простая и действенная. Мы берём фронт камеры и умножаем на нужную нам дальность.

vec3_mul_scalar (v0, cam->front, z_far);

Потом нам надо вычислять настоящие расстояния для ближней точки и для дальней.

    float near_x = (float) xx / (h / kh);
    float near_y = (float) yy / (v * (aspect / kh));

    float far_x = (near_x) / (2.f * kh);
    float far_y = near_y / 2.f;

    float x0 = ((far_x * ((z_far + 1.f) * kh)));
    float y0 = ((far_y * ((z_far + 1.f))));

z_far нам надо складывать с единицей. Честно сказать, я немного подзабыл некоторый замысел этих вычислений, я просто несколько дней не вылазил из расчетов. Но на единицу я прибавил, чтобы вроде как увеличить коэффициент для дальности и соответственно для правильного расположения координат.

Потом сдвигаем правую и верхнюю нормаль камеры в стороны:

    vec3_mul_scalar (v1, cam->right, x0);
    vec3_add (v2, v0, v1);

    vec3_mul_scalar (v1, cam->real_up, y0);
    vec3_sub (v0, v2, v1);

    vec3_copy (ray->dir, v0);

    vec3_mul_scalar (v1, cam->right, near_x);
    vec3_mul_scalar (v2, cam->real_up, near_y);

    vec3_add (v3, cam->front, v1);
    vec3_sub (v0, v3, v2);

    vec3_copy (ray->origin, v0);

ray->dir это направление. ray->origin это точка начала. Думаю по коду можно разобраться что здесь происходит.

И всё, луч готов.

Приведу полный код функции, которую конечно же надо немного подправить, так как я тестировал на ней другие лучи вместе.

void make_ray_from_cursor_to_far (struct game *game, struct camera *cam, struct ray *ray, int x, int y, float z_far)
{
	float v0[3];
	float v1[3];
	float v2[3];
	float v3[3];
	float d0[3];
	float d1[3];

	int xx = x - game->screen->w / 2;
	int yy = game->screen->h / 2 - y;

	float aspect = game->screen->aspect;
	float h = game->screen->fw * 0.5f;
	float v = game->screen->fh * 0.5f;

	float null_vector[3] = {0.f, 0.f, 0.f};

	float kh = (aspect + 1.f) / (1.f + aspect / 2.f);

	float near_x = (float) xx / (h / kh);
	float near_y = (float) yy / (v * (aspect / kh));

	float far_x = near_x / (2.f * kh);
	float far_y = near_y / 2.f;

	float x0 = ((far_x * ((z_far + 1.f) * kh)));
	float y0 = ((far_y * ((z_far + 1.f))));

	vec3_mul_scalar (v0, cam->front, z_far);

	vec3_mul_scalar (v1, cam->right, x0);
	vec3_add (v2, v0, v1); 

	vec3_mul_scalar (v1, cam->real_up, y0);
	vec3_sub (v0, v2, v1);

	vec3_copy (ray->dir, v0);

	vec3_mul_scalar (v1, cam->right, near_x);
	vec3_mul_scalar (v2, cam->real_up, near_y);

	vec3_add (v3, cam->front, v1);
	vec3_sub (v0, v3, v2);

	vec3_copy (ray->origin, v0);

	float e0[3];
	vec3_add (e0, cam->pos, v0);
	vec3_copy (ray->pos, e0);

	opengl_ray_setup (ray);

	translate (ray->transform, cam->pos[0], cam->pos[1], cam->pos[2]);

	float view[16];

	mat4x4_mul (view, ray->transform, cam->view);
	mat4x4_mul (ray->model, view, ray->projection);
}

Таким образом мы теперь знаем ещё один способ бросить луч и мне кажется, что это будет работать быстрее, чем мой способ нахождения двух обратных матриц, а потом и их перемножение.

Для точности нужно дальность луча нужно устанавливать очень большую, к примеру 100 или 1000.

Вот видео как происходит сравнение луча с AABB.

Комментарии (16)


  1. xverizex Автор
    16.11.2025 04:59

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


    1. Sadler
      16.11.2025 04:59

      Народ на Хабре очень разношёрстный, и не всем очевидно, зачем это нужно, вряд ли многие вычитывали и верифицировали эту работу: насколько оно работает, и работает ли эффективно. Лично я стараюсь юзать готовые движки и утилиты, чтобы не уходить в дебри векторной геометрии, кватернионов и всего прочего без необходимости. Поэтому для меня raycast -- это либо GetWorld()->LineTraceSingleByChannel , либо StartShapeTestLosProbe, за исключением случаев, когда нужно что-то совсем уж нестандартное. Из кастома в последний раз рисовал расчёт ближайшего расстояния (и точки соприкосновения) от произвольной точки до bounding box в мировых координатах, это максимум, что мне не лениво было накодить :).

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


    1. OldFisher
      16.11.2025 04:59

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

      Насчёт самой решаемой проблемы тоже можно сказать пару вещей.
      Во-первых, неплохо бы дать оценку эффективности предложенного метода: сколько операций он позволяет сэкономить, как эта экономия соотносится с имеющимися затратами на вычисление проекции и как - со всеми затратами на отрисовку кадра.
      Во-вторых, трассировкой лучей занималось уже немало хороших грамотных специалистов и весьма возможно, что они уже придумали хорошие способы повысить эффективность и в этом вопросе. Может быть, стоило поискать эти решения. Возможно, они даже превосходят предлагаемое (а вопрос их сравнения тем временем возвращает нас к "во-первых").


      1. Jijiki
        16.11.2025 04:59

        в основном лучи пускают как я понял прямо в шейдере, лучше наверно пускать в компута шейдере, соотв всё в шейдере, на процессоре дороговато может быть, кто-то я видел и в картинку на SDL пускает, но там нюансов много

        у вулкана есть ускоряющий слой рей трейса так же

        ну из нюансов proj*view можно предподсчитывать, чтобы не повторять этот счет, но на лоу среднем оборудовании без трассировки не ощутимо с террейном, игроком на камере от 3 лица, и двумя движущимися анимированными модельками, с ускорением и деревьями там же помимо лучей, надо ускоряющее дерево будет еще(типо куб-сфера площадь)

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


        1. xverizex Автор
          16.11.2025 04:59

          Как в шейдере кстати пускать луч я не знаю или не совсем понимаю что вы имеете ввиду, так как у меня сам луч рисуется в шейдере, но вычисляется начальная точка и конечная на CPU. Без матриц.


          1. Jijiki
            16.11.2025 04:59

            так да смотрите, есть 2 матрицы так? эти матрицы состоят из векторов, тоесть 1 из них должна иметь обратную компоненту, про рейтрейс в шейдере я не делал но видел обзоры, ну пускаем точки в случае геометрического шейдера, или если компута шейдер настраиваем вывод картинки из шейдера, и в шейдере вся математика )

            потом в условиях симд преемножений они перемножаться будут как не крути векторами

            а там в обоих матрицах нули есть и будут

            детерминант в обратной матрице можно делать с картой коеффициентов поидее и считать 1 детерминант поидее

            тоесть уходить в вектор спорно, матрица призвана наоборот комплексовать это, на математике чтобы не запутаться лучше реализовать комплекс, отрисовать сцену этим комплексом, и там оптимизировать симды и прочее

            но по итогу я не знаю какой прирост получиться, потомучто с деревом ускоряющим, ситуация столкновений и поиска обьекта значительно быстрее, потомучто обработка не линейная или не совсем линейная


  1. Abdulin
    16.11.2025 04:59

    Дима ты молодец! От проекта rtn


  1. BenGunn
    16.11.2025 04:59

    Жду продолжения.


  1. Jijiki
    16.11.2025 04:59

    вы молодец, но просто надо протестировать на 3д сцене, в разных положениях камеры, потом надо быть уверенным правильно ли вы считаете обратную матрицу.

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

    ну мы вытаскиваем матрицу наружу, да, и считаем векторами тоже самое, так и в какойто библиотеке обратная матрица частично может быть посчитана векторами

    и ведь еще есть sse4.2 avx2 если поддерживается можно с этим компилировать

    Скрытый текст
        // 1. Нормализованные координаты
        float xNDC = (mouseX / win->width) * 2.0f - 1.0f;
        float yNDC = 1.0f - (mouseY / win->height) * 2.0f;
    
        // 2. точка в NDC у Near plane
        glm::vec4 clipCoords = glm::vec4(xNDC, yNDC, -1.0f, 1.0f);
        glm::mat4 invVP = glm::inverse(projection * view);
        glm::vec4 worldCoords = invVP * clipCoords;
        worldCoords /= worldCoords.w;
    
        // 3. луч
        glm::vec3 rayOrigin = worldCoords;
        // glm::vec3 rayDir = glm::normalize(glm::vec3(worldCoords) - rayOrigin)*200.0f;
        glm::vec3 temp = camera->cameraFront * 2000000.f;
        Ray ray = CreateRay(worldCoords, temp);

    но у меня выбор центральным дескриптором экрана модельку

    кстати, а если игра от третьего лица, камера прикреплена к модельке


    1. xverizex Автор
      16.11.2025 04:59

      Спасибо. Мой вариант вообще без использования матриц. Я нашел способ, чтобы работать только с нормалями камеры.


    1. xverizex Автор
      16.11.2025 04:59

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

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


      1. Jijiki
        16.11.2025 04:59

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

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

        мы двигаем камеру, и она обновляется в комплексе матриц, получается камера всегда известна

        а если орто проекция, то её надо превратить в 2д координаты окна поидее подогнать можно, кароче получается, да вы правы

        тоесть другой путь, это гнать в орто, центр, оттуда в мир, вобщем как не крути это поидее камера


        1. Jijiki
          16.11.2025 04:59

              glm::vec3 temp = camera->cameraFront * 2000000.f;
              Ray ray = CreateRay(camera->cameraPos, temp);
          

          вот тест

          Скрытый текст


          1. xverizex Автор
            16.11.2025 04:59

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


            1. Jijiki
              16.11.2025 04:59

              так есть центр окна, луч по дефолту это точка, предположим мы захватили точку, ну можно узнать минмин максмакс точки, тоесть пользователь создаёт аабб считаем центр аабб и пускаем в сцену(ну может там можно с фрустумом подумать, но вроде просто выбранного обьёма по аабб достаточно поидее(тоесть точки в аабб, тоесть либо по аабб проверять либо пускаемый аабб луч с координатами )), все обьекты какие попадают в аабб выделяются поидее, тоесть тут зависит от комплекса или от нюансов, у меня просто канонический способ не работает, и я не знаю почему, поэтому у меня работает со стартовой точкой с матрицей/без, а дальней точкой не работает, и оно логично, потомучто тут надо внимательно смотреть lookat, и обработку обновления фронта, и тогда если оно так работает и обновляется, дальнюю точку поидее можно не искать, а как-то невилировать это

              кстати, тогда кватернионы могут быть оправданы, там тоже lookatquat есть ну и фишки кватера, тоесть пускать по кватерниону перпендикулярно стартовой точке что-то типо такого

              потом лево верх все эти нормали ориентированы должны быть, поэтому проще наверно брать ориентацию камеры, а точку из центра, и тут 2 сценария либо центр либо перпендикуляры, в мир от ориентации камеры что-то типо такого наверно тоже логично

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

              ну это сложно оттестировать надо 2 камеры, переключаемые, чтобы на тестовой запустить квадраты по окну, а со второй камеры наблюдать будут типо там перпендикуляры и прочее и куда летят типо, надо будет заморочится посмотреть, вам тоже спасибо


              1. Jijiki
                16.11.2025 04:59

                    glm::vec3 temp = camera->cameraFront * 2000000.f;
                    Ray ray = CreateRay(camera->cameraPos, camera->cameraFront*2000000.0f);//aabb должен точно покрывать обьект
                
                    //debug info теневой дебаг на кнопке тоесть точка типо летит
                    start=camera->cameraPos+glm::vec3(-1.0f,0.0f,0.0f);// аля смещение к правой руке
                    end=camera->cameraFront;//аля идём по направлению для дебага

                сразу скажу я не эксперт, вобще если верить в обновление камеры, то смещения работают, точка в мире известна, тоесть камера спейс смотрит в мир(тоесть front - это плоскость просто центр смещения от центра плоскости обьекта в мире позиции как я понимаю), значит векторка работает между обьектами в мире как на самом деле не знаю

                еще вспомнил, посмотрите, если поможет, есть в ютубе, thecplusplusguy make game..., и может помочь, thebennybox, у синкМатрикса еще что-то есть, но там по масштабу - практикам в целом

                основной обзор комплекса -камера-спейс-связка с взаимодействием у сиплюсплюсгая есть, там можно уловать суть камера спейса, тоесть все эти ААББ, это просто усложнение тех принципов потомучто на гл1 это всё спорно уже, и свет пропадёт

                есть еще более полный туториал по сурс с камера спейс и сценой на 3.3, но уже забыл автора )