Truyền tia để chọn khối trong trò chơi voxel


22

Tôi đang phát triển một trò chơi với địa hình giống Minecraft được tạo thành từ các khối. Vì hiện tại cơ bản kết xuất và tải chunk đã được thực hiện, tôi muốn thực hiện chọn khối.

Vì vậy, tôi cần phải tìm ra khối máy ảnh người đầu tiên đang đối mặt. Tôi đã nghe nói về việc không dự đoán toàn bộ cảnh nhưng tôi đã quyết định chống lại điều đó bởi vì nó nghe có vẻ hack và không chính xác. Có lẽ bằng cách nào đó tôi có thể chiếu tia theo hướng nhìn nhưng tôi không biết cách kiểm tra va chạm với một khối trong dữ liệu voxel của mình. Tất nhiên tính toán này phải được thực hiện trên CPU vì tôi cần kết quả để thực hiện các hoạt động logic trò chơi.

Vậy làm thế nào tôi có thể tìm ra khối nào ở phía trước máy ảnh? Nếu thích hợp hơn, làm thế nào tôi có thể chiếu tia và kiểm tra va chạm?


Tôi chưa bao giờ tự làm điều đó. Nhưng bạn không thể chỉ có một "tia" (đường thẳng trong trường hợp này) từ mặt phẳng máy ảnh, một vectơ bình thường, với độ dài nhất định (bạn chỉ muốn nó nằm trong bán kính) và xem liệu nó có giao nhau với một trong các khối. Tôi giả sử khoảng cách một phần và cắt cũng được thực hiện. Vì vậy, biết những khối nào để kiểm tra không nên là vấn đề lớn ... tôi nghĩ sao?
Sidar

Câu trả lời:


21

Khi tôi gặp vấn đề này khi làm việc trên các khối của mình , tôi đã tìm thấy bài báo "Thuật toán truyền tải nhanh Voxel cho truy tìm tia" của John Amanatides và Andrew Woo, 1987 mô tả một thuật toán có thể áp dụng cho nhiệm vụ này; nó là chính xác và chỉ cần một vòng lặp cho mỗi voxel giao nhau.

Tôi đã viết một triển khai các phần có liên quan của thuật toán của bài báo bằng JavaScript. Việc triển khai của tôi bổ sung hai tính năng: nó cho phép chỉ định giới hạn về khoảng cách của raycast (hữu ích để tránh các vấn đề về hiệu suất cũng như xác định 'tầm với' giới hạn) và cũng tính toán mặt nào của mỗi voxel mà tia đi vào.

originVectơ đầu vào phải được chia tỷ lệ sao cho độ dài cạnh của voxel là 1. Độ dài của directionvectơ không đáng kể nhưng có thể ảnh hưởng đến độ chính xác của thuật toán.

Thuật toán hoạt động bằng cách sử dụng biểu diễn tham số của tia , origin + t * direction. Đối với mỗi phối hợp trục, chúng tôi tiếp tục theo dõi những tgiá trị mà chúng tôi sẽ có nếu chúng ta mất một bước đủ để vượt qua một ranh giới voxel dọc theo trục đó (tức là thay đổi một phần số nguyên của phối hợp) trong các biến tMaxX, tMaxYtMaxZ. Sau đó, chúng tôi thực hiện một bước (sử dụng các biến steptDeltabiến) dọc theo trục nào có ít nhất tMax- tức là bất kỳ ranh giới voxel nào là gần nhất.

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

Liên kết vĩnh viễn đến phiên bản nguồn này trên GitHub .


1
Liệu thuật toán này cũng hoạt động cho không gian số âm? Tôi chỉ thực hiện thuật toán và nói chung tôi rất ấn tượng. Nó hoạt động tuyệt vời cho tọa độ tích cực. Nhưng vì một số lý do, tôi nhận được kết quả lạ nếu đôi khi tọa độ âm.
danijar

2
@danijar Tôi không thể làm cho các nội dung / mod hoạt động với không gian âm, vì vậy tôi sử dụng điều này : function intbounds(s,ds) { return (ds > 0? Math.ceil(s)-s: s-Math.floor(s)) / Math.abs(ds); }. Như Infinitylà lớn hơn tất cả các số, tôi không nghĩ rằng bạn cần phải bảo vệ chống lại DS là 0 ở đó.
Sẽ

1
@BotskoNet Nghe có vẻ như bạn có vấn đề với việc không thể tìm thấy tia của mình. Tôi đã có vấn đề như vậy từ sớm. Gợi ý: Vẽ một đường thẳng từ gốc đến gốc + hướng, trong không gian thế giới. Nếu dòng đó không nằm dưới con trỏ hoặc nếu nó không xuất hiện dưới dạng một điểm (vì X và Y được chiếu bằng nhau) thì bạn có một vấn đề trong phần không được đưa ra ( không phải là một phần của mã câu trả lời này). Nếu nó đáng tin cậy là một điểm dưới con trỏ thì vấn đề nằm ở raycast. Nếu bạn vẫn gặp sự cố, vui lòng đặt câu hỏi riêng thay vì mở rộng chủ đề này.
Kevin Reid

1
Trường hợp cạnh là nơi tọa độ của gốc tia là một giá trị nguyên và phần tương ứng của hướng tia là âm. Giá trị tMax ban đầu cho trục đó phải bằng 0, vì gốc tọa độ đã nằm ở cạnh dưới cùng của ô của nó, nhưng thay vào đó, nó 1/dslàm cho một trong các trục khác được tăng lên thay thế. Cách khắc phục là viết intfloorđể kiểm tra xem cả hai dscó âm hay không và slà giá trị nguyên (mod trả về 0) và trả về 0,0 trong trường hợp đó.
CodeWarrior

2
Đây là cổng của tôi tới Unity: gist.github.com/dogfuntom/cc881c8fc86ad43d55d8 . Mặc dù, với một số thay đổi bổ sung: các đóng góp của Will và chiến binh tích hợp và có thể tham gia vào một thế giới không giới hạn.
Maxim Kamalov

1

Có lẽ nhìn vào thuật toán dòng của Bresenham , đặc biệt nếu bạn đang làm việc với các khối đơn vị (như hầu hết các trò chơi minecraft có xu hướng).

Về cơ bản, điều này mất hai điểm bất kỳ và theo dõi một đường thẳng không bị gián đoạn giữa chúng. Nếu bạn tạo một vectơ từ người chơi đến khoảng cách chọn tối đa của họ, bạn có thể sử dụng vị trí này và vị trí của người chơi làm điểm.

Tôi có một triển khai 3D trong python ở đây: bresenham3d.py .


6
Một thuật toán kiểu Bresenham sẽ bỏ lỡ một số khối, mặc dù. Nó không xem xét mọi khối mà tia đi qua; nó sẽ bỏ qua một số trong đó tia không đủ gần trung tâm khối. Bạn có thể thấy rõ điều này từ sơ đồ trên Wikipedia . Khối thứ 3 trở xuống và thứ 3 bên phải từ góc trên bên trái là một ví dụ: dòng đi qua nó (hầu như không) nhưng thuật toán của Bresenham không đánh vào nó.
Nathan Reed

0

Để tìm khối đầu tiên trước máy ảnh, hãy tạo một vòng lặp for vòng từ 0 đến một khoảng cách tối đa. Sau đó, nhân vectơ chuyển tiếp của máy ảnh với bộ đếm và kiểm tra xem khối ở vị trí đó có vững chắc không. Nếu có, sau đó lưu trữ vị trí của khối để sử dụng sau và dừng lặp.

Nếu bạn cũng muốn có thể đặt các khối, chọn mặt không khó hơn. Đơn giản chỉ cần lặp lại từ khối và tìm khối trống đầu tiên.


Sẽ không hoạt động, với một vectơ góc phía trước, rất có thể có một điểm trước một phần của khối và điểm tiếp theo sau, thiếu khối. Giải pháp duy nhất với điều này là giảm kích thước của gia số, nhưng bạn phải làm cho nó nhỏ đến mức làm cho các thuật toán khác hiệu quả hơn nhiều.
Phil

Điều này hoạt động khá tốt với động cơ của tôi; Tôi sử dụng một khoảng 0,1.
không có tiêu đề

Giống như @Phil đã chỉ ra, thuật toán sẽ bỏ lỡ các khối mà chỉ nhìn thấy một cạnh nhỏ. Hơn nữa, vòng lặp ngược để đặt các khối sẽ không hoạt động. Chúng ta sẽ phải lặp lại phía trước và giảm kết quả theo một.
danijar

0

Tôi đã tạo một bài đăng trên Reddit với triển khai của mình , sử dụng Thuật toán Dòng của Bresenham. Đây là một ví dụ về cách bạn sẽ sử dụng nó:

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

Đây là bản thực hiện:

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
Như ai đó trong các ý kiến ​​nhận thấy, mã của bạn không có giấy tờ. Mặc dù mã có thể hữu ích, nhưng nó không hoàn toàn trả lời câu hỏi.
Anko
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.