Rzuć promień, aby wybrać blok w grze wokselowej

22

Rozwijam grę z terenem podobnym do Minecrafta zbudowanym z bloków. Ponieważ podstawowe renderowanie i ładowanie porcji jest już zakończone, chcę zaimplementować wybieranie bloków.

Dlatego muszę dowiedzieć się, jaki blok stoi aparat pierwszej osoby. Słyszałem już o nieprojektowaniu całej sceny, ale zdecydowałem się tego nie robić, ponieważ brzmi to hack i nie jest dokładne. Może mógłbym jakoś rzucić promień w kierunku widoku, ale nie wiem, jak sprawdzić kolizję z blokiem w moich danych wokselowych. Oczywiście obliczeń należy dokonać na CPU, ponieważ potrzebuję wyników do wykonania operacji logiki gry.

Jak więc dowiedzieć się, który blok znajduje się przed kamerą? Jeśli jest to preferowane, jak mogę rzucić promień i sprawdzić kolizje?

danijar
źródło
Nigdy tego nie zrobiłem. Ale czy nie możesz po prostu mieć „promienia” (w tym przypadku odcinka linii) z płaszczyzny kamery, normalnego wektora o określonej długości (chcesz tylko, aby znajdował się w promieniu) i sprawdzić, czy przecina się z jednym z Bloki. Zakładam, że zastosowano również częściowe odstępy i obcinanie. Więc wiedza, z którymi blokami do przetestowania nie powinna być aż takim problemem ... myślę?
Sidar

Odpowiedzi:

21

Kiedy miałem ten problem podczas pracy nad kostkami , znalazłem artykuł „A Fast Voxel Traversal Al Algorytm for Ray Tracing” autorstwa John Amanatides i Andrew Woo, 1987, który opisuje algorytm, który można zastosować do tego zadania; jest dokładny i wymaga tylko jednej iteracji pętli na przecięty woksel.

Napisałem implementację odpowiednich części algorytmu artykułu w JavaScript. Moja implementacja dodaje dwie funkcje: pozwala określić limit odległości raycasta (przydatne w celu uniknięcia problemów z wydajnością, a także zdefiniować ograniczony „zasięg”), a także oblicza, która twarz każdego woksela zawiera promień.

originWektor wejściowy musi być skalowany w taki sposób, aby długość boku woksela wynosiła 1. Długość directionwektora nie jest znacząca, ale może wpływać na dokładność numeryczną algorytmu.

Algorytm ten działa za pomocą sparametryzowanego reprezentację promieniem origin + t * direction. Dla każdej osi współrzędnych, możemy śledzić twartości, które mamy, jeśli zrobiliśmy krok wystarczający do przekroczenia granicy woksela wzdłuż tej osi (czyli zmienić część całkowita współrzędnych) w zmiennych tMaxX, tMaxYoraz tMaxZ. Następnie wykonujemy krok (używając zmiennych stepi tDelta) wzdłuż dowolnej osi, która ma najmniejszą tMax- tj. Która granica wokseli jest najbliższa.

/**
 * 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;
}

Stały link do tej wersji źródła na GitHub .

Kevin Reid
źródło
1
Czy ten algorytm działa również w przypadku ujemnej przestrzeni liczb? Zaimplementowałem algorytm tylko i ogólnie jestem pod wrażeniem. Działa świetnie dla dodatnich współrzędnych. Ale z jakiegoś powodu uzyskuję dziwne wyniki, jeśli czasami występują współrzędne ujemne.
danijar 11.03.13
2
@danijar nie mogłem dostać intbounds / mod rzeczy do pracy z negatywnej przestrzeni, więc używam to: function intbounds(s,ds) { return (ds > 0? Math.ceil(s)-s: s-Math.floor(s)) / Math.abs(ds); }. Ponieważ Infinityjest większa niż wszystkie liczby, nie sądzę, żebyś musiał się wystrzegać, czy ds będzie tam 0.
Czy
1
@ BotskoNet Brzmi to tak, jakbyś miał problem z nieprojektowaniem, aby znaleźć swój promień. Na początku miałem takie problemy. Sugestia: Narysuj linię od początku do początku + kierunek w przestrzeni świata. Jeśli ta linia nie znajduje się pod kursorem lub jeśli nie pojawia się jako punkt (ponieważ rzutowane X i Y powinny być równe), masz problem z nieprojektowaniem ( nie jest częścią kodu tej odpowiedzi). Jeśli niezawodnie jest to punkt pod kursorem, problem leży w raycast. Jeśli nadal masz problem, zadaj osobne pytanie zamiast przedłużać ten wątek.
Kevin Reid,
1
Przypadek krawędzi ma miejsce, gdy współrzędna początku promienia jest liczbą całkowitą, a odpowiadająca mu część promienia jest ujemna. Początkowa wartość tMax dla tej osi powinna wynosić zero, ponieważ początek znajduje się już na dolnej krawędzi komórki, ale zamiast tego 1/dspowoduje, że jedna z pozostałych osi jest zamiast tego zwiększana. Poprawka polega na napisaniu, intflooraby sprawdzić, czy oba dssą ujemne i czy ssą liczbą całkowitą (mod zwraca 0), i zwracają w tym przypadku 0,0.
codewarrior
2
Oto mój port do Unity: gist.github.com/dogfuntom/cc881c8fc86ad43d55d8 . Jednak z pewnymi dodatkowymi zmianami: zintegrowane wkłady Willa i twórcy kodu oraz umożliwiły rzucanie w nieograniczonym świecie.
Maxim Kamalov
1

Być może zajrzyj do algorytmu linii Bresenhama , szczególnie jeśli pracujesz z blokami jednostek (jak ma to miejsce w większości gier Minecraft).

Zasadniczo zajmuje to dwa dowolne punkty i kreśli nieprzerwaną linię między nimi. Jeśli rzucisz wektor na gracza na maksymalną odległość, możesz go użyć, a gracze ustawią się jako punkty.

Mam tutaj implementację 3D w python: bresenham3d.py .

łosoś łososiowy
źródło
6
Algorytm typu Bresenhama ominie jednak niektóre bloki. Nie uwzględnia każdego bloku, przez który przechodzi promień; pominie niektóre, w których promień nie zbliży się wystarczająco do centrum bloku. Widać to wyraźnie na schemacie na Wikipedii . Blok trzeci w dół i trzeci w prawo od lewego górnego rogu jest przykładem: linia przechodzi przez niego (ledwo), ale algorytm Bresenhama go nie trafia.
Nathan Reed,
0

Aby znaleźć pierwszy blok przed kamerą, utwórz pętlę for, która zapętla od 0 do pewnej maksymalnej odległości. Następnie pomnóż wektor do przodu kamery przez licznik i sprawdź, czy blok w tej pozycji jest pełny. Jeśli tak, zapisz pozycję bloku do późniejszego użycia i przestań zapętlać.

Jeśli chcesz także umieścić klocki, wybieranie twarzy nie jest trudniejsze. Po prostu zapętl się z powrotem od bloku i znajdź pierwszy pusty blok.

nieuprawny
źródło
Nie działałoby, z wektorem ustawionym pod kątem byłoby bardzo możliwe, aby mieć punkt przed jedną częścią bloku, a kolejny punkt po nim, nieobecny. Jedynym rozwiązaniem tego byłoby zmniejszenie wielkości przyrostu, ale trzeba by było go zmniejszyć tak, aby inne algorytmy były znacznie bardziej skuteczne.
Phil
Działa to całkiem dobrze z moim silnikiem; Używam interwału 0,1.
bez tytułu
Jak wskazał @Phil, algorytm pomija bloki, w których widać tylko niewielką krawędź. Ponadto pętla do tyłu w celu umieszczania bloków nie działałaby. Musielibyśmy również wykonać pętlę do przodu i zmniejszyć wynik o jeden.
danijar
0

Napisałem post na Reddicie z moją implementacją , która korzysta z algorytmu liniowego Bresenhama. Oto przykład, w jaki sposób chcesz go użyć:

// 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
   }
}

Oto sama implementacja:

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;
    }



}
ClickerMonkey
źródło
1
Jak zauważył ktoś w komentarzach, twój kod jest nieudokumentowany. Chociaż kod może być pomocny, nie do końca odpowiada na pytanie.
Anko,