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


22

Я розробляю гру з місцевістю Minecraft, що складається з блоків. Оскільки базове візуалізація та завантаження фрагментів виконано зараз, я хочу здійснити вибір блоку.

Тому мені потрібно з’ясувати, з яким блоком стоїть камера першої особи. Я вже чув про зняття проекту з усієї сцени, але я вирішив проти цього, оскільки це звучить хитро і не точно. Можливо, я міг би якось кинути промінь у напрямку зору, але я не знаю, як перевірити зіткнення з блоком у своїх даних про вокселі. Звичайно, ці розрахунки потрібно робити на процесорі, оскільки мені потрібні результати для виконання логічних ігор.

Тож як я міг дізнатися, який блок знаходиться перед камерою? Якщо бажано, як я міг би кинути промінь і перевірити зіткнення?


Я ніколи цього не робив сам. Але чи не могли б у вас просто "промінь" (в даному випадку сегмент лінії) від площини камери, звичайний вектор, з певною довжиною (ви хочете, щоб він знаходився в радіусі) і побачите, чи перетинається він з одним із блоки. Я припускаю, що частковий пробіл і відсікання також реалізовані. Тож знання, з якими блоками слід перевірити, не повинно бути такою великою проблемою ... я думаю?
Сидар

Відповіді:


21

Коли у мене виникли ці проблеми під час роботи над моїми кубами , я знайшов документ "Швидкий алгоритм обходу вокселів для відстеження променя" Джона Аманатідеса та Ендрю Ву, 1987 року, в якому описаний алгоритм, який можна застосувати до цього завдання; він точний і потребує лише однієї ітерації циклу на один пересечений воксель.

Я написав реалізацію відповідних частин алгоритму роботи в JavaScript. Моя реалізація додає дві особливості: вона дозволяє задати обмеження на відстань радіомовлення (корисно для уникнення проблем із продуктивністю, а також визначення обмеженого "досяжності"), а також обчислює, на якій стороні кожного вокселя входить промінь.

Вхідний originвектор повинен бути розміщений таким чином, щоб довжина сторони вокселя дорівнювала 1. Довжина directionвектора не мала, але може впливати на числову точність алгоритму.

Алгоритм працює за допомогою параметризрвані уявлення променя, origin + t * direction. Для кожної осі координат, ми продовжуємо відслідковувати tзначення , яке ми мали б , якби ми зробили крок достатнього , щоб перетнути кордон вокселей уздовж цієї осі (тобто зміна цілої частини координат) в змінних tMaxX, tMaxYі tMaxZ. Потім ми робимо крок (використовуючи stepі tDeltaзмінні) уздовж тієї осі, яка має найменше tMax- тобто, яка межа вокселя є найближчою.

/**
 * Call the callback with (x,y,z,value,face) of all blocks along the line
 * segment from point 'origin' in vector direction 'direction' of length
 * 'radius'. 'radius' may be infinite.
 * 
 * 'face' is the normal vector of the face of that block that was entered.
 * It should not be used after the callback returns.
 * 
 * If the callback returns a true value, the traversal will be stopped.
 */
function raycast(origin, direction, radius, callback) {
  // From "A Fast Voxel Traversal Algorithm for Ray Tracing"
  // by John Amanatides and Andrew Woo, 1987
  // <http://www.cse.yorku.ca/~amana/research/grid.pdf>
  // <http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.42.3443>
  // Extensions to the described algorithm:
  //   • Imposed a distance limit.
  //   • The face passed through to reach the current cube is provided to
  //     the callback.

  // The foundation of this algorithm is a parameterized representation of
  // the provided ray,
  //                    origin + t * direction,
  // except that t is not actually stored; rather, at any given point in the
  // traversal, we keep track of the *greater* t values which we would have
  // if we took a step sufficient to cross a cube boundary along that axis
  // (i.e. change the integer part of the coordinate) in the variables
  // tMaxX, tMaxY, and tMaxZ.

  // Cube containing origin point.
  var x = Math.floor(origin[0]);
  var y = Math.floor(origin[1]);
  var z = Math.floor(origin[2]);
  // Break out direction vector.
  var dx = direction[0];
  var dy = direction[1];
  var dz = direction[2];
  // Direction to increment x,y,z when stepping.
  var stepX = signum(dx);
  var stepY = signum(dy);
  var stepZ = signum(dz);
  // See description above. The initial values depend on the fractional
  // part of the origin.
  var tMaxX = intbound(origin[0], dx);
  var tMaxY = intbound(origin[1], dy);
  var tMaxZ = intbound(origin[2], dz);
  // The change in t when taking a step (always positive).
  var tDeltaX = stepX/dx;
  var tDeltaY = stepY/dy;
  var tDeltaZ = stepZ/dz;
  // Buffer for reporting faces to the callback.
  var face = vec3.create();

  // Avoids an infinite loop.
  if (dx === 0 && dy === 0 && dz === 0)
    throw new RangeError("Raycast in zero direction!");

  // Rescale from units of 1 cube-edge to units of 'direction' so we can
  // compare with 't'.
  radius /= Math.sqrt(dx*dx+dy*dy+dz*dz);

  while (/* ray has not gone past bounds of world */
         (stepX > 0 ? x < wx : x >= 0) &&
         (stepY > 0 ? y < wy : y >= 0) &&
         (stepZ > 0 ? z < wz : z >= 0)) {

    // Invoke the callback, unless we are not *yet* within the bounds of the
    // world.
    if (!(x < 0 || y < 0 || z < 0 || x >= wx || y >= wy || z >= wz))
      if (callback(x, y, z, blocks[x*wy*wz + y*wz + z], face))
        break;

    // tMaxX stores the t-value at which we cross a cube boundary along the
    // X axis, and similarly for Y and Z. Therefore, choosing the least tMax
    // chooses the closest cube boundary. Only the first case of the four
    // has been commented in detail.
    if (tMaxX < tMaxY) {
      if (tMaxX < tMaxZ) {
        if (tMaxX > radius) break;
        // Update which cube we are now in.
        x += stepX;
        // Adjust tMaxX to the next X-oriented boundary crossing.
        tMaxX += tDeltaX;
        // Record the normal vector of the cube face we entered.
        face[0] = -stepX;
        face[1] = 0;
        face[2] = 0;
      } else {
        if (tMaxZ > radius) break;
        z += stepZ;
        tMaxZ += tDeltaZ;
        face[0] = 0;
        face[1] = 0;
        face[2] = -stepZ;
      }
    } else {
      if (tMaxY < tMaxZ) {
        if (tMaxY > radius) break;
        y += stepY;
        tMaxY += tDeltaY;
        face[0] = 0;
        face[1] = -stepY;
        face[2] = 0;
      } else {
        // Identical to the second case, repeated for simplicity in
        // the conditionals.
        if (tMaxZ > radius) break;
        z += stepZ;
        tMaxZ += tDeltaZ;
        face[0] = 0;
        face[1] = 0;
        face[2] = -stepZ;
      }
    }
  }
}

function intbound(s, ds) {
  // Find the smallest positive t such that s+t*ds is an integer.
  if (ds < 0) {
    return intbound(-s, -ds);
  } else {
    s = mod(s, 1);
    // problem is now s+t*ds = 1
    return (1-s)/ds;
  }
}

function signum(x) {
  return x > 0 ? 1 : x < 0 ? -1 : 0;
}

function mod(value, modulus) {
  return (value % modulus + modulus) % modulus;
}

Постійне посилання на цю версію джерела на GitHub .


1
Чи працює цей алгоритм і для від’ємного простору чисел? Я реалізував алгоритм лише просто і взагалі мене вразив. Це чудово підходить для позитивних координат. Але я чомусь отримую дивні результати, якщо іноді залучаються негативні координати.
danijar

2
@danijar я не міг отримати intbounds / мод матеріалу для роботи з негативним простором, тому я використовую це: function intbounds(s,ds) { return (ds > 0? Math.ceil(s)-s: s-Math.floor(s)) / Math.abs(ds); }. Як Infinityбільше, ніж усі цифри, я не думаю, що вам також потрібно не захищати від того, щоб ds було 0.
Буде чи

1
@BotskoNet Це здається, що у вас виникають проблеми з відмовою від пошуку променя. У мене були подібні проблеми рано. Пропозиція: Намалюйте лінію від початку до початку + напрям у світовому просторі. Якщо цей рядок не знаходиться під курсором, або якщо він не відображається у вигляді точки (оскільки прогнозовані X і Y повинні бути рівними), у вас є проблема з непроекцією ( не є частиною коду цієї відповіді). Якщо це надійно крапка під курсором, тоді проблема полягає в радіації. Якщо у вас все ще виникає проблема, будь ласка, задайте окреме питання замість того, щоб продовжувати цю тему.
Кевін Рейд

1
Крайовий випадок, коли координата початку променя є цілим значенням, а відповідна частина напрямку променя є від’ємною. Початкове значення tMax для цієї осі повинно бути дорівнює нулю, оскільки джерело вже знаходиться в нижньому краї його комірки, але воно замість цього 1/dsпризводить до збільшення однієї з інших осей. Виправлення полягає в тому, щоб записати, intfloorщоб перевірити, чи обидва dsє негативними та sчи є ціле значення (мод повертає 0), а у такому випадку повернути 0,0.
кодове воїнство

2
Ось мій порт Unity: gist.github.com/dogfuntom/cc881c8fc86ad43d55d8 . Хоча, з деякими додатковими змінами: інтегрований внесок Вілла та Кодек-воїна та зробив можливим подати участь у необмеженому світі.
Максим Камалов

1

Можливо, вивчіть лінійний алгоритм Bresenham , особливо якщо ви працюєте з блок-блоками (як це прагне більшість minecraftish ігор).

В основному це займає будь-які дві точки, і простежується неперервана лінія між ними. Якщо ви відкидаєте вектор від гравця до максимальної відстані його вибору, ви можете використовувати це, а гравці позиціонують як очки.

У мене тут реалізація 3D в python: bresenham3d.py .


6
Але алгоритм типу Брезенама не вистачить деяких блоків. Він не враховує кожен блок, через який проходить промінь; це пропустить частину, в якій промінь не наблизиться до центру блоку. Це видно чітко з діаграми у Вікіпедії . Блок 3-го внизу та 3-го праворуч у верхньому лівому куті є прикладом: рядок проходить через нього (ледь), але алгоритм Брезена не вражає його.
Натан Рід

0

Щоб знайти перший блок перед камерою, створіть цикл, який обводиться від 0 до деякої максимальної відстані. Потім помножте передні вектори камери на лічильник і перевірте, чи блок у цьому положенні твердий. Якщо це так, то збережіть положення блоку для подальшого використання та припиніть циклічно.

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


Не буде працювати, при нахиленому векторі вперед прямо можна було б мати точку перед однією частиною блоку, а наступну точку після, пропустивши блок. Єдиним рішенням для цього було б зменшити приріст, але вам доведеться зробити його таким малим, щоб зробити інші алгоритми набагато ефективнішими.
Філ

Це досить добре працює з моїм двигуном; Я використовую інтервал 0,1.
без назви

Як і вказував @Phil, алгоритм буде пропускати блоки, де видно лише невеликий край. Більше того, цикління назад для розміщення блоків не буде працювати. Нам також доведеться провести цикл вперед і зменшити результат на один.
danijar

0

Я створив пост на Reddit зі своєю реалізацією , в якій використовується алгоритм лінії Брезенама. Ось приклад того, як ви його використовували:

// A plotter with 0, 0, 0 as the origin and blocks that are 1x1x1.
PlotCell3f plotter = new PlotCell3f(0, 0, 0, 1, 1, 1);
// From the center of the camera and its direction...
plotter.plot( camera.position, camera.direction, 100);
// Find the first non-air block
while ( plotter.next() ) {
   Vec3i v = plotter.get();
   Block b = map.getBlock(v);
   if (b != null && !b.isAir()) {
      plotter.end();
      // set selected block to v
   }
}

Ось сама реалізація:

public interface Plot<T> 
{
    public boolean next();
    public void reset();
    public void end();
    public T get();
}

public class PlotCell3f implements Plot<Vec3i>
{

    private final Vec3f size = new Vec3f();
    private final Vec3f off = new Vec3f();
    private final Vec3f pos = new Vec3f();
    private final Vec3f dir = new Vec3f();

    private final Vec3i index = new Vec3i();

    private final Vec3f delta = new Vec3f();
    private final Vec3i sign = new Vec3i();
    private final Vec3f max = new Vec3f();

    private int limit;
    private int plotted;

    public PlotCell3f(float offx, float offy, float offz, float width, float height, float depth)
    {
        off.set( offx, offy, offz );
        size.set( width, height, depth );
    }

    public void plot(Vec3f position, Vec3f direction, int cells) 
    {
        limit = cells;

        pos.set( position );
        dir.norm( direction );

        delta.set( size );
        delta.div( dir );

        sign.x = (dir.x > 0) ? 1 : (dir.x < 0 ? -1 : 0);
        sign.y = (dir.y > 0) ? 1 : (dir.y < 0 ? -1 : 0);
        sign.z = (dir.z > 0) ? 1 : (dir.z < 0 ? -1 : 0);

        reset();
    }

    @Override
    public boolean next() 
    {
        if (plotted++ > 0) 
        {
            float mx = sign.x * max.x;
            float my = sign.y * max.y;
            float mz = sign.z * max.z;

            if (mx < my && mx < mz) 
            {
                max.x += delta.x;
                index.x += sign.x;
            }
            else if (mz < my && mz < mx) 
            {
                max.z += delta.z;
                index.z += sign.z;
            }
            else 
            {
                max.y += delta.y;
                index.y += sign.y;
            }
        }
        return (plotted <= limit);
    }

    @Override
    public void reset() 
    {
        plotted = 0;

        index.x = (int)Math.floor((pos.x - off.x) / size.x);
        index.y = (int)Math.floor((pos.y - off.y) / size.y);
        index.z = (int)Math.floor((pos.z - off.z) / size.z);

        float ax = index.x * size.x + off.x;
        float ay = index.y * size.y + off.y;
        float az = index.z * size.z + off.z;

        max.x = (sign.x > 0) ? ax + size.x - pos.x : pos.x - ax;
        max.y = (sign.y > 0) ? ay + size.y - pos.y : pos.y - ay;
        max.z = (sign.z > 0) ? az + size.z - pos.z : pos.z - az;
        max.div( dir );
    }

    @Override
    public void end()
    {
        plotted = limit + 1;
    }

    @Override
    public Vec3i get() 
    {
        return index;
    }

    public Vec3f actual() {
        return new Vec3f(index.x * size.x + off.x,
                index.y * size.y + off.y,
                index.z * size.z + off.z);
    }

    public Vec3f size() {
        return size;
    }

    public void size(float w, float h, float d) {
        size.set(w, h, d);
    }

    public Vec3f offset() {
        return off;
    }

    public void offset(float x, float y, float z) {
        off.set(x, y, z);
    }

    public Vec3f position() {
        return pos;
    }

    public Vec3f direction() {
        return dir;
    }

    public Vec3i sign() {
        return sign;
    }

    public Vec3f delta() {
        return delta;
    }

    public Vec3f max() {
        return max;
    }

    public int limit() {
        return limit;
    }

    public int plotted() {
        return plotted;
    }



}

1
Як хтось у коментарях помітив, ваш код недокументований. Хоча код може бути корисним, він не зовсім відповідає на питання.
Анко
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.