投射射线以选择体素游戏中的方块


22

我正在开发一款具有类似于Minecraft的地形且由块组成的游戏。由于基本渲染和块加载现已完成,因此我想实现块选择。

因此,我需要找出第一人称相机所面对的障碍。我已经听说过要取消整个场景的投影,但是我决定反对,因为它听起来很hacky,而且不准确。也许我可以以某种方式向视图方向投射射线,但是我不知道如何用体素数据中的块检查碰撞。当然,此计算必须在CPU上完成,因为我需要结果才能执行游戏逻辑运算。

那么,如何找出相机前面的哪个块呢?如果可以的话,我该如何投射光线并检查碰撞?


我从来没有自己做过。但是,难道您不能只是从相机平面获得具有特定长度(您只希望它在半径之内)的法线向量的“射线”(在这种情况下为线段),然后查看它是否与其中一个相交块。我假设也实现了部分间距和裁剪。因此,知道要测试哪些块不是一个大问题……我认为吗?
2013年

Answers:


21

当我在处理多维数据集时遇到此问题时,我发现了John Amanatides和Andrew Woo于1987年发表的论文“射线追踪的快速Voxel遍历算法”,其中描述了一种可用于此任务的算法。它是准确的,并且每个相交的体素只需要一个循环迭代。

我已经用JavaScript编写了该算法相关部分的实现。我的实现增加了两个功能:它允许指定射线投射距离的限制(用于避免性能问题以及定义有限的“范围”),还可以计算射线进入每个体素的面。

输入origin向量必须按比例缩放,以使体素的边长为1。direction向量的长度不重要,但可能会影响算法的数值精度。

该算法通过使用射线的参数化表示进行操作origin + t * direction。对于各坐标轴,我们跟踪的t其中如果我们采取了足够跨越沿着该轴的体素边界处的步骤中,我们将具有值在变量(即改变的坐标的整数部分)tMaxXtMaxYtMaxZ。然后,我们沿着哪个轴最小的位置(即,哪个体素边界最靠近)采取步骤(使用steptDelta变量)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上此版本的源代码


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。
2013年

1
@BotskoNet听起来好像您无法投影找到射线有问题。我很早就遇到过这样的问题。建议:在世界空间中从起点到起点+方向画一条线。如果该行不在光标下方,或者它没有显示为点(因为投影的X和Y应该相等),那么您在投影中就有问题(不是此答案的代码的一部分)。如果它可靠地位于光标下方,那么问题就出在光线投射上。如果仍然有问题,请问一个单独的问题,而不是扩展此线程。
凯文·里德

1
边缘情况是射线原点的坐标是整数值,并且射线方向的相应部分为负。该轴的初始tMax值应为零,因为原点已位于其单元格的底部边缘,但相反1/ds导致其他轴之一增加。解决方法是编写intfloor检查是否两者ds均为负数并且s是整数值(mod返回0),在这种情况下返回0.0。
codewarrior 2014年

2
这是我连接Unity的端口:gist.github.com/dogfuntom/cc881c8fc86ad43d55d8。但是,进行了一些其他更改:整合了Will和Codewarrior的贡献,并有可能在无限的世界中进行投射。
Maxim Kamalov

1

也许研究一下Bresenham的line算法,尤其是在使用单位块的情况下(尤其是大多数《我的世界》游戏倾向于这样做)。

基本上,这需要两个点,并在它们之间画一条连续的线。如果将向量从玩家投射到他们的最大拾取距离,则可以使用此值,并将玩家位置作为点。

我在python中有3D实现:bresenham3d.py


6
不过,Bresenham型算法将丢失某些块。它不会考虑射线穿过的每个障碍;它会跳过一些光线距离块中心不够近的区域。您可以从Wikipedia上的图中清楚地看到这一点。从左上角的第3个向下和第3个右侧的块是一个示例:直线(几乎)通过了该块,但是Bresenham的算法没有实现。
内森·里德

0

要找到相机前面的第一个块,请创建一个for循环,该循环从0循环到某个最大距离。然后,将相机的前向矢量乘以计数器,然后检查该位置处的块是否牢固。如果是,则存储该块的位置以备后用并停止循环。

如果您还希望能够放置障碍物,那么挑脸就不难了。只需从块循环返回并找到第一个空块。


用成角度的前向矢量将不起作用,那么很有可能在块的一部分之前有一个点,而在块的一部分之后有后一个点,则缺少该块。唯一的解决方案是减小增量的大小,但是您必须将其减小得很小,以使其他算法更有效。
Phil

这对我的引擎非常有效;我使用的间隔为0.1。
无题

就像@Phil指出的那样,该算法会丢失仅看到很小边缘的块。此外,向后循环以放置块将不起作用。我们也必须向前循环,并将结果减一。
danijar

0

通过实现在Reddit上发表了一篇文章,该文章使用了Bresenham的Line算法。这是一个如何使用它的示例:

// 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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.