복셀 게임에서 블록을 선택하기 위해 캐스트 레이


22

블록으로 만든 Minecraft와 같은 지형으로 게임을 개발 중입니다. 기본 렌더링 및 청크 로딩이 완료되었으므로 블록 선택을 구현하고 싶습니다.

따라서 1 인칭 카메라가 어떤 블록을 향하고 있는지 찾아야합니다. 나는 이미 전체 장면을 투사하지 않는다고 들었지만 해키처럼 들리고 정확하지 않기 때문에 반대했습니다. 어쩌면 어떻게 든 뷰 방향으로 광선을 캐스팅 할 수는 있지만 보셀 데이터의 블록과의 충돌을 확인하는 방법을 모르겠습니다. 물론 게임 로직 작업을 수행하려면 결과가 필요하기 때문에 CPU에서이 계산을 수행해야합니다.

카메라 앞에 어떤 블록이 있는지 어떻게 알 수 있습니까? 바람직한 경우 어떻게 광선을 투사하고 충돌을 확인할 수 있습니까?


나는 결코 그것을 한 적이 없다. 그러나 카메라 평면에서 "레이"(이 경우 선분), 특정 길이 (반지름 범위 내에서만 원함)를 가진 법선 벡터를 가질 수 없었으며 광선이 블록. 부분 간격과 클리핑도 구현되어 있다고 가정합니다. 테스트 할 블록을 아는 것이 그렇게 큰 문제가되지는 않습니다.
Sidar

답변:


21

큐브 작업 중에이 문제가 발생했을 때 , 이 작업에 적용 할 수있는 알고리즘을 설명하는 John Amanatides와 Andrew Woo (1987) 의 논문 "Ray Tracing을위한 Fast Voxel Traversal Algorithm"을 발견했습니다 . 정확하고 교차 된 복셀 당 하나의 루프 반복 만 필요합니다.

필자는 논문 알고리즘의 관련 부분을 JavaScript로 구현했습니다. 내 구현은 두 가지 기능을 추가합니다. 레이 캐스트 거리에 제한을 지정하고 (성능 문제를 피하고 제한된 '범위'를 정의하는 데 유용함) 광선이 입력 한 각 복셀의면을 계산합니다.

origin복셀의 측면 길이가 1이되도록 입력 벡터의 크기를 조정해야합니다. 벡터의 길이는 direction중요하지 않지만 알고리즘의 수치 정확도에 영향을 줄 수 있습니다.

알고리즘은 광선의 매개 변수화 된 표현을 사용하여 작동합니다 origin + t * direction. 각 축 좌표 위해, 우리는 추적 유지 t변수에 (좌표의 정수 부분 변경 예)를 우리가 그 축을 따라 복셀의 경계를 통과하기에 충분한 조치를 취했다 경우 우리가 가진 것 값을 tMaxX, tMaxY하고 tMaxZ. 그런 다음 축이 가장 적은 축, 즉 복셀 경계가 가장 가까운 축을 따라 단계 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 인을 방지 할 필요가 있다고 생각하지 않습니다.

1
@BotskoNet 광선을 찾기 위해 투영을 해제하는 데 문제가있는 것 같습니다. 나는 일찍 그런 문제가 있었다. 제안 : 월드 공간에서 원점에서 원점 + 방향으로 선을 그립니다. 해당 줄이 커서 아래에 없거나 점으로 표시되지 않으면 (투영 된 X와 Y가 같아야 함) 투영 되지 않은 문제가있는 것 입니다 (이 답변의 코드의 일부가 아님 ). 커서 아래에있는 점이라면 레이 캐스트에 문제가있는 것입니다. 여전히 문제가 발생하면이 스레드를 확장하는 대신 별도의 질문을하십시오.
Kevin Reid

1
가장자리 경우는 광선 원점의 좌표가 정수 값이고 광선 방향의 해당 부분이 음수입니다. 원점이 이미 셀의 아래쪽 가장자리에 있기 때문에 해당 축의 초기 tMax 값은 0이어야하지만 대신 1/ds다른 축 중 하나 가 대신 증가합니다. 수정은 intfloor모두 ds음수이고 s정수 값 인지 확인 하기 위해 작성 하고 (mod는 0을 반환),이 경우 0.0을 반환합니다.
codewarrior

2
여기 내 Unity 포트가 있습니다 : gist.github.com/dogfuntom/cc881c8fc86ad43d55d8 . 그러나 몇 가지 추가 변경 사항이 포함되어 있습니다. Will 's 및 codewarrior의 기여가 통합되어 무제한 세계에서 캐스팅이 가능해졌습니다.
Maxim Kamalov

1

아마도 Bresenham의 라인 알고리즘을 살펴보십시오 . 특히 단위 블록으로 작업하는 경우 (대부분의 마인 크래프트 게임처럼).

기본적으로 이것은 두 가지 점을 취하고 그 사이에 끊어지지 않은 선을 추적합니다. 플레이어에서 벡터를 최대 피킹 거리까지 캐스팅하면이를 사용하여 플레이어가 포인트로 배치됩니다.

나는 파이썬에서 3D 구현을 가지고 있습니다 : bresenham3d.py .


6
그러나 Bresenham 형 알고리즘은 일부 블록을 놓칠 것입니다. 광선이 통과하는 모든 블록을 고려하지는 않습니다. 광선이 블록 중심에 충분히 가까워지지 않는 부분은 건너 뜁니다. Wikipedia의 다이어그램에서 이를 명확하게 볼 수 있습니다 . 왼쪽 위 모서리에서 3 번째 아래로, 3 번째 오른쪽에서 블록을 예로들 수 있습니다.
Nathan Reed

0

카메라 앞에서 첫 번째 블록 을 찾으 려면 0에서 최대 거리까지 반복되는 for 루프를 만듭니다. 그런 다음 카메라의 순방향 벡터에 카운터를 곱하고 해당 위치의 블록이 단색인지 확인하십시오. 만약 그렇다면, 나중에 사용하기 위해 블록의 위치를 ​​저장하고 루핑을 중지하십시오.

블록을 배치하려면 얼굴 선택이 어렵지 않습니다. 블록에서 간단히 루프를 돌려 첫 번째 빈 블록을 찾으십시오.


각진 순방향 벡터를 사용하면 작동하지 않을 것입니다. 블록의 한 부분 앞에 포인트가 있고 그 뒤에 포인트가 있으면 블록을 놓칠 수 있습니다. 이것의 유일한 해결책은 증분의 크기를 줄이는 것이지만 다른 알고리즘을 훨씬 더 효과적으로 만들려면 크기를 작게해야합니다.
Phil

이것은 내 엔진과 잘 작동합니다. 0.1 간격을 사용합니다.
제목 없음

@Phil이 지적했듯이 알고리즘은 작은 가장자리 만 보이는 블록을 놓칠 것입니다. 또한 블록을 배치하기 위해 뒤로 루프하면 작동하지 않습니다. 우리는 또한 앞으로 나아가서 결과를 하나씩 줄여야 할 것입니다.
danijar

0

내가 만든 내 구현 레딧에 게시물을 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.