Как быстро найти, если точка затенена в сложной сцене?

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

В настоящее время я использую встроенный raytracer от Three.js со следующим кодом:

// pos   = vector with (normalized) x, y coordinates on canvas
// dir   = vector from camera to target point

const raycaster = new THREE.Raycaster();
const d = dir.length(); // distance to point
let intersects = false;
raycaster.setFromCamera(pos, camera);
const intersections = raycaster.intersectObject(modelObject, true);
if (intersections.length > 0 && intersections[0].distance < d)
    intersects = true;

// if ray intersects at a point closer than d, then the target point is obscured
// otherwise it is visible

Однако это очень медленно (частота кадров падает с 50 кадров до 8 кадров в секунду) на этих сложных моделях. Я искал лучшие способы сделать это, но до сих пор я не нашел ничего хорошего в этом случае.

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

Ответ 1

Я не очень-то знаю об этом, но у вас есть несколько вариантов. Я не знаю достаточно о three.js, чтобы рассказать вам, как это сделать с этой библиотекой, но говоря о WebGL вообще...

Если вы можете использовать WebGL 2.0, вы можете использовать окклюзионные запросы. Это сводится к

var query = gl.createQuery();
gl.beginQuery(gl.ANY_SAMPLES_PASSED, query);
// ... draw a small quad at the specified 3d position ...
gl.endQuery(gl.ANY_SAMPLES_PASSED);
// some time later, typically a few frames later (after a fraction of a second)
if (gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE))
{
     gl.getQueryParameter(query, gl.QUERY_RESULT);
}

Обратите внимание, что результат запроса доступен только через несколько кадров.

Если WebGl 2.0 не является параметром, то вам, вероятно, следует нарисовать сцену в фреймбуфере, где вы присоедините свою собственную текстуру вместо обычного z-буфера. Существует расширение для использования правильных текстур глубины (подробнее здесь), но там, где это невозможно, вы всегда можете вернуться к рисованию своей сцены с помощью фрагментарный шейдер, который выводит глубину каждого пикселя.

Затем вы можете использовать gl.ReadPixels() в текстуре глубины. Опять же, помните о задержке для передачи GPU- > CPU, которая всегда будет значительной.

Сказав все это, в зависимости от того, как выглядят ваши объекты DOM, гораздо проще и быстрее сделать ваши объекты DOM текстурой и нарисовать эту текстуру, используя квад, как часть вашей 3D-сцены.

Ответ 2

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

Есть кулинарная книга для вычисления GPU. Повторяйте каждую смену сцены. Извините, я не могу сделать это для Three.js(не знаю движка).

Этап 1, построение изображения с видимостью тегов

Создайте буфер массива (и индекса), содержащий размер тегов html, идентификаторы тегов (int numbers from 0) и позиции тегов.

Создайте рендерингбуфер и новую программу WebGL, которая будет использоваться для рендеринга в нее. Шейдеры этой программы сделают упрощенную сцену, включая "тени тегов". Теперь упрощен алгоритм для фрагментарного шейдера: для любого объекта визуализируется белый цвет. За исключением тега, выведите цвет на основе идентификатора тега.

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

Результат может выглядеть так (но это не важно):

содержимое renderbuffer

Если цвет отличается от белого, есть метка. (Предполагая, что у меня есть только 3 метки, тогда мои цвета: # 000000, # 010000, # 020000, которые все выглядят как черные, но это не так.)

Этап 2, сбор данных прозрачности об тэгах в изображении

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

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

attribute vec3 tagPosition;
attribute float tagId;

float calculateTransparency(vec2 tagSize, vec2 tagPosition) {
    float transparency = 0;
    for(0-tagSize.x+tagPosition.x) {
        for(0-tagSize.y+tagPosition.y) {
            if(textureLookup == tagId) transparency++; // notice that texture lookup is used only for area where tag could be
        }
    }
    return transparency/totalSize;
}

vec2 tagSize2d = calculateSize(tagPosition);
float transparency = calculateTransparency(tagSize2d, tagPosition.xy);

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

Результатом этого этапа является изображение, в котором каждый пиксель представляет один тег и цвет пикселя - прозрачность тега. В зависимости от того, сколько тэгов у вас есть, некоторые пиксели могут ничего не значить и иметь значение clearColor. Позиция пикселя соответствует идентификатору тега.

Этап 3, чтение значений с помощью javascript

Для чтения данных используйте readPixels (или можете использовать texImage2D?). Простой способ сделать это.

Затем вы используете forloop на основе идентификаторов тегов и, например, записываете данные из типизированного массива в ваш конечный автомат javascript. Теперь у вас есть значения прозрачности в javascript, и вы можете изменять значения CSS.

Идеи

На этапе 1 уменьшение размера renderbuffer приведет к значительному повышению производительности (оно снижает также поиск текстур на этапе 2) с почти нулевой стоимостью.

Если вы используете readPixels сразу после этапа 1 и пытаетесь прочитать данные с экрана с помощью javascript, даже если вы используете renderbuffer только размером 320 * 200 пикселей, js должен делать столько итераций, сколько разрешение. Поэтому в случае, если сцена будет меняться каждый момент, а затем просто пустой forloop:

var time = Date.now();
for(var i=0;i<320*200*60;i++) { // 64000*60 times per second
}
console.log(Date.now() - time);

занял ~ 4100 мс на моей машине. Но со стадией 2 вы должны выполнять только столько итераций, что и теги в видимой области. (Для 50 меток это может быть 3000 * 60).

Самая большая проблема, которую я вижу, - сложность реализации.

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

Ответ 3

Предполагая, что ваше позиционирование Div синхронизировано с основной 3D-сценой, вы должны иметь возможность запросить только один пиксель с readPixels "под" вашего div.

Снова предполагая, что вы контролируете геометрию, не было бы целесообразным взломать просто добавить "вне контекста" цвет (или альфа-значение) в текстуре, где div будет покрывать и проверять его?

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

Ответ 4

Здесь в этом ответе вы найдете хороший пример, используя THREE.Frustum, чтобы определить, видны ли объекты:

var frustum = new THREE.Frustum();
var cameraViewProjectionMatrix = new THREE.Matrix4();
camera.updateMatrixWorld();
camera.matrixWorldInverse.getInverse( camera.matrixWorld );
cameraViewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
frustum.setFromMatrix( cameraViewProjectionMatrix );

visible = frustum.intersectsObject( object );

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