Voksel oyunda blok seçmek için ışın Cast


22

Ben bloklardan yapılmış Minecraft benzeri bir arazi ile bir oyun geliştiriyorum. Temel işleme ve yığın yüklemesi şimdi yapıldığı için, blok seçmeyi uygulamak istiyorum.

Bu nedenle, ilk kişi kameranın hangi bloğa baktığını bulmam gerekiyor. Tüm sahneyi kaldırmayı çoktan duydum ama buna karşı karar verdim çünkü kulağa kaba geliyor ve doğru değil. Belki bir şekilde bir bakış açısı ile ışın yapabilirim ama voksel verilerimdeki bir blokla çarpışmayı nasıl kontrol edeceğimi bilmiyorum. Tabii ki bu hesaplamalar CPU üzerinde yapılmalı çünkü oyun mantığı işlemlerini yapmak için sonuçlara ihtiyacım var.

Kamera önünde hangi bloğun olduğunu nasıl bulabilirim? Tercih edilirse, nasıl ışın yapabilirim ve çarpışmaları nasıl kontrol edebilirim?


Asla kendim yapmadım. Ancak kamera düzleminden bir "ışın" (bu durumda çizgiler), belirli bir uzunlukta (sadece bir yarıçap içinde olmasını istersiniz) normal bir vektörünüz olamaz mı ve bir tanesiyle kesişip kesişmediğini göremez miydiniz? blokları. Kısmi boşluk bırakmanın ve kırpma işleminin de uygulandığını varsayıyorum. Hangi blokları test edeceğinizi bilmek o kadar da önemli bir sorun olmamalıydı.
Sidar

Yanıtlar:


21

Küplerim üzerinde çalışırken bu sorunu yaşadığımda , John Amanatides ve Andrew Woo, 1987'den bu göreve uygulanabilecek bir algoritmayı tanımlayan “Ray İzleme için Hızlı Voksel Geçiş Algoritması” adlı bir yazı buldum ; doğrudur ve kesişen her voksel için yalnızca bir döngü yinelemesi gerekir.

Makalenin algoritmasının ilgili bölümlerinin JavaScript’te uygulanmasını yazdım. Uygulamam iki özellik ekliyor: Bu, raycast'ın mesafesi için bir sınır belirlemeye izin veriyor (performans sorunlarından kaçınmak ve sınırlı bir 'erişim' tanımlamak için kullanışlıdır) ve ayrıca ışının girdiği her vokselin hangi yüzünü hesaplar.

Giriş originvektörü, bir vokselin yan uzunluğu 1 olacak şekilde ölçeklendirilmelidir. directionVektörün uzunluğu önemli değildir ancak algoritmanın sayısal doğruluğunu etkileyebilir.

Algoritma, ışının parametreli bir gösterimini kullanarak çalışır origin + t * direction. Her bir eksen koordinat sağlamak için, takip tdeğişkenleri (koordinat tamsayı kısmını değiştirmek örneğin) o eksen boyunca bir voksel sınırın geçilmesi için yeterli bir adım aldı eğer sahip olduğu değere tMaxX, tMaxYve tMaxZ. Sonra, hangi eksenin en az olduğu - yani hangi voksel-sınırının en yakın olduğu boyunca ( stepve tDeltadeğişkenlerini kullanarak) bir adım atıyoruz 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'daki kaynağın bu sürümüne kalıcı bağlantı .


1
Bu algoritma negatif sayı alanı için de çalışıyor mu? Algoritmayı sadece ve genellikle etkilendim. Pozitif koordinatlar için harika çalışıyor. Ancak bazı nedenlerden dolayı bazen olumsuz koordinatlar karışırsa garip sonuçlar alıyorum.
danijar

2
Bunu kullanmak yüzden @danijar Ben negatif boşluk ile çalışmalarına intbounds / mod şeyler alamadı: function intbounds(s,ds) { return (ds > 0? Math.ceil(s)-s: s-Math.floor(s)) / Math.abs(ds); }. InfinityTüm sayılardan daha büyük olduğu gibi , ds'nin 0 olması durumunda da korunmanız gerekmiyor.
olacak

1
@BotskoNet Görünüşe göre, ışını bulmak için projeyi çözmemekle ilgili bir sorun var. Daha önce de böyle sorunlar yaşadım. Öneri: Dünyada, orijinden orjinal + yöne doğru bir çizgi çizin. Bu satır imlecin altında değilse veya bir nokta olarak görünmüyorsa (yansıtılan X ve Y'nin eşit olması gerektiği için), o zaman önyargıda bir sorun yaşarsınız ( bu cevap kodunun bir parçası değil ). Eğer imlecin altında güvenilir bir nokta varsa, o zaman sorun raycast'tadır. Hala bir sorununuz varsa, lütfen bu konuyu genişletmek yerine ayrı bir soru sorun.
Kevin Reid

1
Kenar durumu, ışın orijininin bir koordinatının bir tamsayı değeri olduğu ve ışın yönünün karşılık gelen kısmının negatif olduğu durumdur. Bu eksen için başlangıçtaki tMax değeri sıfır olmalıdır, çünkü başlangıç ​​noktası hücresinin en alt kenarındadır, ancak bunun yerine 1/dsdiğer eksenlerden birinin artmasına neden olur. Düzeltme, intfloorher ikisinin dsde negatif olup olmadığını ve sbir tamsayı olup olmadığını kontrol etmek için yazılır (mod, 0 döndürür) ve bu durumda 0.0 döndürür.
codewarrior

2
İşte Benim Birlik benim liman: gist.github.com/dogfuntom/cc881c8fc86ad43d55d8 . Yine de, bazı ek değişikliklerle: Will'in ve codewarrior'ın katkılarını birleştirerek sınırsız bir dünyada rol almayı mümkün kıldı.
Maxim Kamalov

1

Belki de özellikle birim bloklarla çalışıyorsanız (çoğu minecraft oyununun eğiliminde olduğu gibi) Bresenham'ın çizgi algoritmasına bakın .

Temel olarak bu iki nokta alır ve aralarında kırılmayan bir çizgi izler. Bir vektörü oynatıcıdan maksimum toplama mesafesine atarsanız, bunu kullanabilirsiniz ve oyuncular puan olarak konumlandırır.

Burada python'da bir 3D uygulaması var: bresenham3d.py .


6
Bresenham tipi bir algoritma bazı blokları kaçıracak. Işın içinden geçen her bloğu dikkate almaz; ışının blok merkezine yeterince yaklaşamadığı bazılarını atlar. Bunu Vikipedi'deki diyagramdan açıkça görebilirsiniz . Sol üst köşeden 3. aşağı ve 3. sağa blok bir örnektir: çizgi içinden (zar zor) geçer ancak Bresenham'ın algoritması ona vurmaz.
Nathan Reed,

0

Kameranın önündeki ilk bloğu bulmak için 0'dan bazı maksimum mesafelere kadar uzanan bir for döngüsü oluşturun. Ardından, kameranın ileri vektörünü sayaç ile çarpın ve bu konumdaki bloğun sağlam olup olmadığını kontrol edin. Öyleyse, daha sonra kullanmak üzere bloğun konumunu saklayın ve döngüyü durdurun.

Ayrıca blok yerleştirmek de istiyorsanız, yüz toplama zor değildir. Basitçe bloktan geri döngü yapın ve ilk boş bloğu bulun.


İşe yaramaz, açılı bir ileri vektör ile bir bloğun bir kısmından önce bir noktaya sahip olmak ve daha sonraki bir noktadan sonra bloğu kaçırmak çok mümkün olurdu. Bununla ilgili tek çözüm, artışın boyutunu azaltmak olabilir, ancak diğer algoritmaları daha etkili hale getirecek kadar küçük olması gerekir.
Phil

Bu benim motorum ile oldukça iyi çalışıyor; 0.1 aralık kullanıyorum.
başlıksız

@Phil'in belirttiği gibi, algoritma sadece küçük bir kenarın görüldüğü blokları kaçıracaktı. Ayrıca blokları yerleştirmek için geriye doğru döngü çalışmak işe yaramaz. Ayrıca ileriye doğru döngü yapmalı ve sonucu bir azaltmalıyız.
danijar

0

Bresenham'ın Line Algoritmasını kullanan uygulamam ile Reddit'te bir yazı yaptım . İşte nasıl kullanacağınıza bir örnek:

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

İşte uygulamanın kendisi:

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
Gibi birisi yorumlarda fark kodunuzu belgelenmemiş. Kod faydalı olsa da, soruyu tam olarak cevaplamıyor.
Anko
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.