Cast ray per selezionare il blocco nel gioco voxel


22

Sto sviluppando un gioco con un terreno simile a Minecraft fatto di blocchi. Dal momento che il rendering di base e il caricamento del blocco vengono eseguiti ora, voglio implementare la selezione dei blocchi.

Pertanto, devo scoprire a quale blocco si trova di fronte la fotocamera in prima persona. Ho già sentito di non aver proiettato l'intera scena, ma ho deciso di non farlo perché sembra confuso e non è preciso. Forse potrei in qualche modo lanciare un raggio nella direzione della vista ma non so come controllare la collisione con un blocco nei miei dati voxel. Naturalmente questi calcoli devono essere eseguiti sulla CPU poiché ho bisogno dei risultati per eseguire le operazioni di logica di gioco.

Quindi, come potrei scoprire quale blocco si trova di fronte alla telecamera? Se è preferibile, come potrei lanciare un raggio e controllare le collisioni?


Non l'ho mai fatto da solo. Ma non potresti semplicemente avere un "raggio" (segmento in questo caso) dal piano della telecamera, un vettore normale, con una certa lunghezza (vuoi solo che sia entro un raggio) e vedere se si interseca con uno dei blocchi. Presumo che sia implementata anche la spaziatura parziale e il ritaglio. Quindi sapere con quali blocchi testare non dovrebbe essere un grosso problema ... penso?
Sidar,

Risposte:


21

Quando ho avuto questo problema mentre lavoravo sui miei cubi , ho trovato l'articolo "A Fast Voxel Traversal Algorithm for Ray Tracing" di John Amanatides e Andrew Woo, 1987, che descrive un algoritmo che può essere applicato a questo compito; è accurato e necessita di una sola iterazione in loop per voxel intersecato.

Ho scritto un'implementazione delle parti rilevanti dell'algoritmo del documento in JavaScript. La mia implementazione aggiunge due funzionalità: consente di specificare un limite sulla distanza del raycast (utile per evitare problemi di prestazioni e per definire un "raggio d'azione" limitato) e calcola anche quale faccia di ciascun voxel è entrata nel raggio.

Il originvettore di input deve essere ridimensionato in modo tale che la lunghezza laterale di un voxel sia 1. La lunghezza del directionvettore non è significativa ma può influire sulla precisione numerica dell'algoritmo.

L'algoritmo opera utilizzando una rappresentazione parametrizzata del raggio, origin + t * direction. Per ciascuna coordinata asse, teniamo traccia del tvalore che avremmo se abbiamo fatto un passo sufficiente per attraversare un confine voxel lungo tale asse (cioè modificare la parte intera della coordinata) nelle variabili tMaxX, tMaxYe tMaxZ. Quindi, facciamo un passo (usando le variabili stepe tDelta) lungo l'asse che ha il minimo tMax- vale a dire il limite del voxel più vicino.

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

Collegamento permanente a questa versione del sorgente su GitHub .


1
Questo algoritmo funziona anche con spazio numerico negativo? Ho implementato l'algoritmo solo e generalmente sono impressionato. Funziona alla grande per coordinate positive. Ma per qualche motivo ottengo strani risultati se a volte sono coinvolte coordinate negative.
danijar,

2
@danijar non ho potuto ottenere la roba intbounds / mod al lavoro con lo spazio negativo, quindi io uso questo: function intbounds(s,ds) { return (ds > 0? Math.ceil(s)-s: s-Math.floor(s)) / Math.abs(ds); }. Dato che Infinityè maggiore di tutti i numeri, non penso che dovresti guardarti nemmeno da ds.
Sarà il

1
@BotskoNet Sembra che tu abbia un problema con la non proiezione per trovare il tuo raggio. Ho avuto problemi del genere all'inizio. Suggerimento: traccia una linea dall'origine all'origine + direzione, nello spazio del mondo. Se quella linea non si trova sotto il cursore, o se non appare come un punto (poiché la X e Y proiettate dovrebbero essere uguali) allora si ha un problema nella non proiezione ( non parte del codice di questa risposta). Se è affidabile un punto sotto il cursore, allora il problema è nel raycast. Se il problema persiste, fai una domanda separata invece di estendere questo thread.
Kevin Reid,

1
Il caso limite è dove una coordinata dell'origine del raggio è un valore intero e la parte corrispondente della direzione del raggio è negativa. Il valore tMax iniziale per quell'asse dovrebbe essere zero, poiché l'origine è già sul bordo inferiore della sua cella, ma sta invece 1/dscausando l'incremento di uno degli altri assi. La correzione è scrivere intfloorper verificare se entrambi dssono negativi ed sè un valore intero (mod restituisce 0) e restituisce 0,0 in quel caso.
codewarrior,

2
Ecco la mia porta su Unity: gist.github.com/dogfuntom/cc881c8fc86ad43d55d8 . Tuttavia, con alcune modifiche aggiuntive: i contributi di Will e codewarrior integrati e reso possibile il cast in un mondo illimitato.
Maxim Kamalov,

1

Forse guarda l'algoritmo di linea di Bresenham , in particolare se stai lavorando con blocchi di unità (come la maggior parte dei giochi di minecraft).

Fondamentalmente questo prende due punti qualsiasi e traccia una linea ininterrotta tra di loro. Se lanci un vettore dal giocatore alla sua massima distanza di raccolta, puoi usarlo e i giocatori si posizionano come punti.

Ho un'implementazione 3D in Python qui: bresenham3d.py .


6
Tuttavia, un algoritmo di tipo Bresenham mancherà alcuni blocchi. Non considera ogni blocco attraversato dal raggio; salterà un po 'in cui il raggio non si avvicina abbastanza al centro del blocco. Puoi vederlo chiaramente dal diagramma su Wikipedia . Il blocco 3 ° in basso e 3 ° a destra dall'angolo in alto a sinistra è un esempio: la linea lo attraversa (a malapena) ma l'algoritmo di Bresenham non lo colpisce.
Nathan Reed,

0

Per trovare il primo blocco di fronte alla videocamera, crea un ciclo for che passa da 0 a una distanza massima. Quindi, moltiplica il vettore in avanti della telecamera per il contatore e controlla se il blocco in quella posizione è solido. Se lo è, quindi memorizzare la posizione del blocco per un uso successivo e interrompere il looping.

Se vuoi anche essere in grado di posizionare i blocchi, la selezione del viso non è più difficile. Basta tornare indietro dal blocco e trovare il primo blocco vuoto.


Non funzionerebbe, con un vettore in avanti inclinato sarebbe molto possibile avere un punto prima di una parte di un blocco, e il punto successivo dopo, mancando il blocco. L'unica soluzione con questo sarebbe quella di ridurre la dimensione dell'incremento, ma dovresti farlo così piccolo da rendere altri algoritmi molto più efficaci.
Phil

Funziona abbastanza bene con il mio motore; Uso un intervallo di 0,1.
senza titolo

Come sottolineato da @Phil, l'algoritmo perderebbe i blocchi in cui si vede solo un piccolo bordo. Inoltre il looping all'indietro per posizionare i blocchi non funzionerebbe. Dovremmo anche fare un ciclo avanti e ridurre il risultato di uno.
danijar,

0

Ho pubblicato un post su Reddit con la mia implementazione , che utilizza l'algoritmo di linea di Bresenham. Ecco un esempio di come lo useresti:

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

Ecco l'implementazione stessa:

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
Come notato da qualcuno nei commenti, il tuo codice non è documentato. Sebbene il codice possa essere utile, non risponde esattamente alla domanda.
Anko,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.