ส่งเรย์เพื่อเลือกบล็อกในเกม voxel


22

ฉันกำลังพัฒนาเกมที่มีภูมิประเทศคล้าย Minecraft ที่ทำจากบล็อก เนื่องจากการเรนเดอร์พื้นฐานและการโหลดชิ้นข้อมูลเสร็จสิ้นแล้วฉันต้องการใช้การเลือกบล็อก

ดังนั้นฉันจึงต้องค้นหาว่าบล็อกใดที่กล้องตัวแรกกำลังเผชิญอยู่ ฉันได้ยินมาแล้วว่าไม่ได้คาดเดาฉากทั้งหมด แต่ฉันตัดสินใจว่าเพราะมันฟังดูแฮ็คและไม่ถูกต้อง บางทีฉันอาจโยนรังสีไปในทิศทางที่มองเห็นได้ แต่ฉันไม่รู้วิธีตรวจสอบการชนด้วยบล็อกในข้อมูล voxel ของฉัน แน่นอนว่าการคำนวณนี้ต้องทำบน CPU เนื่องจากฉันต้องการผลลัพธ์เพื่อดำเนินการกับเกมลอจิก

แล้วฉันจะรู้ได้อย่างไรว่าบล็อกไหนอยู่หน้ากล้อง? ถ้าเป็นที่ต้องการฉันจะโยนรังสีและตรวจสอบการชนได้อย่างไร


ฉันไม่เคยทำเอง แต่คุณไม่สามารถมี "เรย์" (เส้นแบ่งส่วนในกรณีนี้) จากระนาบกล้องเวกเตอร์ปกติที่มีความยาวแน่นอน (คุณแค่อยากให้มันอยู่ในรัศมี) และดูว่ามันตัดกับหนึ่งใน บล็อก ฉันคิดว่ามีการใช้พื้นที่และการตัดบางส่วนเช่นกัน ดังนั้นการรู้ว่าการทดสอบบล็อกใดที่ไม่น่าจะเป็นปัญหามากนัก ... ฉันคิดว่างั้นเหรอ?
Sidar

คำตอบ:


21

เมื่อฉันมีปัญหานี้ในขณะที่ทำงานกับลูกบาศก์ของฉันฉันพบกระดาษ"อัลกอริธึม Voxel Traversal Algorithm สำหรับการติดตามเรย์" โดย John Amanatides และ Andrew Woo, 1987ซึ่งอธิบายอัลกอริทึมที่สามารถนำไปใช้กับงานนี้ มันถูกต้องและต้องการวนซ้ำเพียงครั้งเดียวต่อ voxel ที่ถูกตัด

ฉันได้เขียนส่วนที่เกี่ยวข้องของอัลกอริทึมของกระดาษใน JavaScript การติดตั้งของฉันเพิ่มคุณสมบัติสองอย่าง: ช่วยให้สามารถระบุขีด จำกัด ของระยะทางของรังสี (มีประโยชน์สำหรับการหลีกเลี่ยงปัญหาด้านประสิทธิภาพการทำงานรวมถึงการกำหนด 'การเข้าถึง' ที่ จำกัด ) และคำนวณใบหน้าของ voxel แต่ละรังสีที่ป้อน

originเวกเตอร์การป้อนข้อมูลจะต้องปรับขนาดดังกล่าวว่าความยาวด้านของ voxel คือ 1 ความยาวของdirectionเวกเตอร์ไม่สำคัญ แต่อาจส่งผลต่อความแม่นยำเชิงตัวเลขของอัลกอริทึม

อัลกอริทึมทำงานโดยใช้การแทนค่าพารามิเตอร์ของรังสี, origin + t * direction. สำหรับแต่ละประสานงานแกนเราติดตามtค่าที่เราจะได้ถ้าเราเอาขั้นตอนเพียงพอที่จะข้ามเขตแดน voxel ตามแกนที่ (เช่นเปลี่ยนส่วนจำนวนเต็มของการประสานงาน) ในตัวแปรtMaxX, และtMaxY tMaxZจากนั้นเราใช้ขั้นตอน (โดยใช้stepและtDeltaตัวแปร) ตามแกนใดก็ตามที่มีค่าน้อยที่สุดtMax- เช่นใดก็ตามที่ voxel-boundary อยู่ใกล้ที่สุด

/**
 * 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 / mod 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 ควรเท่ากัน) แสดงว่าคุณมีปัญหาในการคลายออก ( ไม่ใช่ส่วนหนึ่งของรหัสคำตอบนี้) หากเป็นจุดที่น่าเชื่อถือภายใต้เคอร์เซอร์ปัญหาจะอยู่ใน raycast หากคุณยังคงมีปัญหาโปรดถามคำถามแยกต่างหากแทนที่จะขยายหัวข้อนี้
Kevin Reid

1
กรณีขอบคือจุดที่พิกัดของจุดกำเนิดรังสีเป็นค่าจำนวนเต็มและส่วนที่สอดคล้องกันของทิศทางรังสีนั้นเป็นลบ ค่า tMax เริ่มต้นสำหรับแกนนั้นควรเป็นศูนย์เนื่องจากจุดเริ่มต้นอยู่ที่ขอบด้านล่างของเซลล์แล้ว แต่จะ1/dsทำให้แกนอื่นแกนหนึ่งเพิ่มขึ้นแทน การแก้ไขคือการเขียนintfloorเพื่อตรวจสอบว่าทั้งสองdsเป็นลบและsเป็นค่าจำนวนเต็ม (mod ส่งคืน 0) และส่งคืน 0.0 ในกรณีนั้น
codewarrior

2
นี่คือพอร์ตของฉันเพื่อความสามัคคี: gist.github.com/dogfuntom/cc881c8fc86ad43d55d8 แม้ว่าด้วยการเปลี่ยนแปลงเพิ่มเติมบางอย่าง: การรวมของ Will และ codewarrior และทำให้เป็นไปได้ในโลกที่ไร้ขีด จำกัด
Maxim Kamalov

1

บางทีมองเข้าไปในอัลกอริทึมของ Bresenhamโดยเฉพาะอย่างยิ่งถ้าคุณทำงานกับหน่วยบล็อก (เนื่องจากเกม minecraftish ส่วนใหญ่มักจะ)

โดยพื้นฐานแล้วจะมีจุดสองจุดใด ๆ และลากเส้นระหว่างแถว หากคุณโยนเวกเตอร์จากผู้เล่นไปยังระยะทางเลือกสูงสุดของพวกเขาคุณสามารถใช้สิ่งนี้และตำแหน่งของผู้เล่นเป็นจุด

ฉันมีการดำเนินงาน 3 มิติในหลามที่นี่: bresenham3d.py


6
แม้ว่าอัลกอริทึมประเภท Bresenham จะพลาดบางช่วงตึก มันไม่ได้พิจารณาทุกช่วงเวลาที่รังสีทะลุผ่าน มันจะข้ามบางส่วนที่รังสีไม่ได้เข้าใกล้ศูนย์บล็อก คุณสามารถดูนี้อย่างชัดเจนจากแผนภาพในวิกิพีเดีย บล็อกที่ 3 ลงมาและมุมขวาที่ 3 จากมุมบนซ้ายคือตัวอย่าง: เส้นผ่านมัน (แทบจะไม่) แต่อัลกอริทึมของ Bresenham ไม่ได้กระทบ
นาธานรีด

0

ในการค้นหาบล็อกแรกที่ด้านหน้าของกล้องให้สร้างห่วงสำหรับวนรอบจาก 0 ถึงระยะทางสูงสุด จากนั้นคูณเวกเตอร์ไปข้างหน้าของกล้องโดยตัวนับและตรวจสอบว่าบล็อกที่ตำแหน่งนั้นเป็นของแข็งหรือไม่ ถ้าเป็นเช่นนั้นให้เก็บตำแหน่งของบล็อกไว้เพื่อใช้ในภายหลังและหยุดการวนซ้ำ

หากคุณต้องการวางบล็อกการเลือกหน้าไม่ยาก เพียงวนกลับจากบล็อกและค้นหาบล็อกว่างเปล่าอันแรก


จะไม่ทำงานถ้าหากเวกเตอร์ไปข้างหน้าทำมุมมันจะเป็นไปได้มากที่จะมีจุดต่อส่วนหนึ่งของบล็อกและจุดต่อมาหลังจากนั้นหายไปจากบล็อก ทางออกเดียวของสิ่งนี้คือการลดขนาดของการเพิ่มขึ้น แต่คุณต้องทำให้เล็กลงเพื่อให้อัลกอริธึมอื่นมีประสิทธิภาพมากขึ้น
Phil

มันใช้งานได้ดีกับเครื่องยนต์ของฉัน ฉันใช้ช่วงเวลา 0.1
ไม่มีชื่อ

เช่นเดียวกับ @Phil ชี้ให้เห็นว่าอัลกอริทึมจะพลาดบล็อกที่เห็นเพียงขอบเล็ก ๆ นอกจากนี้การวนลูปย้อนหลังสำหรับการวางบล็อกจะไม่ทำงาน เราจะต้องวนไปข้างหน้าเช่นกันและลดผลลัพธ์ทีละรายการ
danijar

0

ฉันสร้างโพสต์บน Reddit ด้วยการติดตั้งซึ่งใช้อัลกอริทึม Line ของ Bresenham นี่คือตัวอย่างของวิธีที่คุณจะใช้:

// 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
เมื่อมีคนในความคิดเห็นสังเกตเห็นรหัสของคุณจะไม่มีเอกสาร แม้ว่ารหัสอาจมีประโยชน์ แต่ก็ไม่ได้ตอบคำถาม
Anko
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.