ボクセルゲームでブロックを選択するためにレイをキャストします


22

ブロックで作られたMinecraftのような地形でゲームを開発しています。これで基本的なレンダリングとチャンクの読み込みが完了したので、ブロック選択を実装したいと思います。

したがって、私は一人称カメラが直面しているブロックを見つける必要があります。シーン全体を投影解除すると聞いたことがありますが、それはハッキリと聞こえ、正確ではないので、私はそれに反対しました。どういうわけか、視線方向に光線を投げることができましたが、ボクセルデータ内のブロックとの衝突をチェックする方法がわかりません。もちろん、ゲームロジック操作を実行するには結果が必要なので、この計算はCPUで実行する必要があります。

それでは、どのブロックがカメラの前にあるかをどのようにして見つけることができますか?それが望ましい場合、どのように光線を投じて衝突を確認できますか?


私は自分でやったことがありません。しかし、カメラ平面からの「光線」(この場合は線分)、特定の長さの(法線ベクトルのみが必要な)法線ベクトルを取得し、それがいずれかと交差するかどうかを確認できませんでしたブロック。部分的な間隔とクリッピングも実装されていると想定しています。だから、どのブロックでテストするかを知ることはそれほど問題ではないでしょう...私は思いますか?
シダー

回答:


21

キューブでの作業中にこの問題が発生したとき、John AmanatidesとAndrew Wooによる1987年の「レイトレーシング用の高速ボクセルトラバーサルアルゴリズム」という論文で、このタスクに適用できるアルゴリズムを説明しています。正確であり、交差するボクセルごとに1回のループ反復のみが必要です。

この論文のアルゴリズムの関連部分の実装をJavaScriptで記述しました。私の実装は2つの機能を追加します:レイキャストの距離の制限を指定することを可能にし(パフォーマンスの問題を回避し、制限された「リーチ」を定義するのに役立ちます)、また、レイが各ボクセルのどの面に入るかを計算します。

入力originベクトルは、ボクセルの辺の長さが1になるようにスケーリングする必要があります。directionベクトルの長さは重要ではありませんが、アルゴリズムの数値精度に影響する可能性があります。

アルゴリズムは、光線のパラメータ化された表現を使用して動作しますorigin + t * direction。各座標軸のために、我々は、を追跡t我々は、変数にその軸に沿ったボクセルの境界を横断するのに十分なステップ(すなわち座標の整数部分を変更し)た場合、我々が有することになる値tMaxXtMaxYおよびtMaxZ。次に、どの軸が最も小さいか、つまり、ボクセル境界が最も近い軸に沿って(stepand tDelta変数を使用して)ステップを実行します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
このアルゴリズムは負の数空間でも機能しますか?私はアルゴリズムを実装しただけで、一般的に感銘を受けました。正の座標に最適です。しかし、何らかの理由で、負の座標がときどき含まれていると、奇妙な結果が得られます。
ダニジャー

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が等しい必要があるため)、アンプロジェクションに問題があります(この回答のコードの一部ではありません)。カーソルの下に確実にある場合、問題はレイキャストにあります。それでも問題が解決しない場合は、このスレッドを拡張するのではなく、別の質問をしてください。
ケビンリード

1
エッジケースは、光線の原点の座標が整数値であり、光線の方向の対応する部分が負の場合です。原点はすでにセルの下端にあるため、その軸の初期tMax値はゼロである必要がありますが、代わり1/dsに他の軸の1つがインクリメントされます。修正方法はintfloor、両方dsが負でs整数値(modが0を返す)かどうかをチェックし、その場合は0.0を返すことです。
codewarrior 14

2
Unityへの私のポート:gist.github.com/dogfuntom/cc881c8fc86ad43d55d8です。ただし、いくつかの追加の変更があります。ウィルとコードウォーリアの貢献を統合し、無制限の世界でキャストできるようにしました。
マキシムカマロフ

1

おそらく、Bresenhamのラインアルゴリズムを調べてみてください(特に、ほとんどのMinecraftishゲームの傾向があるように)ユニットブロックを使用している場合は特にそうです。

基本的に、これは任意の2点を取り、それらの間の切れ目のない線をトレースします。プレーヤーから最大ピッキング距離までベクトルをキャストする場合、これを使用でき、プレーヤーはポイントとして配置されます。

私はここにPythonで3D実装を持っています:bresenham3d.py


6
ただし、ブレゼンハム型のアルゴリズムでは、いくつかのブロックが失われます。光線が通過するすべてのブロックを考慮するわけではありません。光線がブロックの中心に十分に近づかない部分はスキップします。これはウィキペディアの図からはっきりとわかります。左上隅から3番目下および3番目右のブロックは一例です。ラインは(ほとんど)通過しますが、ブレゼンハムのアルゴリズムはヒットしません。
ネイサンリード

0

カメラの前で最初のブロックを見つけるには、0から最大距離までループするforループを作成します。次に、カメラの前方ベクトルにカウンターを掛け、その位置のブロックがソリッドかどうかを確認します。ある場合は、後で使用するためにブロックの位置を保存し、ループを停止します。

ブロックを配置できるようにしたい場合は、顔を選ぶのは難しくありません。ブロックからループバックして、最初の空のブロックを見つけます。


角度付きの順方向ベクトルでは、ブロックの一部の前にポイントがあり、ブロックが欠落した後に後続のポイントがある可能性があります。これに関する唯一の解決策は、増分のサイズを小さくすることですが、他のアルゴリズムをより効果的にするためには、それを小さくする必要があります。
フィル

これは私のエンジンでかなりうまく機能します。0.1の間隔を使用します。
無題

@Philが指摘したように、アルゴリズムは小さなエッジのみが見られるブロックを見逃します。さらに、ブロックを配置するために逆方向にループすることは機能しません。同様にループフォワードし、結果を1減らす必要があります。
ダニジャー

0

私が作っ私の実装でRedditの上のポストをブレゼンハムの線アルゴリズムを使用しています、。使用方法の例を次に示します。

// 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
以下のように誰かに気づいたコメントで、あなたのコードが文書化されていません。コードは役立つかもしれませんが、質問にはまったく答えません。
アンコ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.