我正在开发一款具有类似于Minecraft的地形且由块组成的游戏。由于基本渲染和块加载现已完成,因此我想实现块选择。
因此,我需要找出第一人称相机所面对的障碍。我已经听说过要取消整个场景的投影,但是我决定反对,因为它听起来很hacky,而且不准确。也许我可以以某种方式向视图方向投射射线,但是我不知道如何用体素数据中的块检查碰撞。当然,此计算必须在CPU上完成,因为我需要结果才能执行游戏逻辑运算。
那么,如何找出相机前面的哪个块呢?如果可以的话,我该如何投射光线并检查碰撞?
我正在开发一款具有类似于Minecraft的地形且由块组成的游戏。由于基本渲染和块加载现已完成,因此我想实现块选择。
因此,我需要找出第一人称相机所面对的障碍。我已经听说过要取消整个场景的投影,但是我决定反对,因为它听起来很hacky,而且不准确。也许我可以以某种方式向视图方向投射射线,但是我不知道如何用体素数据中的块检查碰撞。当然,此计算必须在CPU上完成,因为我需要结果才能执行游戏逻辑运算。
那么,如何找出相机前面的哪个块呢?如果可以的话,我该如何投射光线并检查碰撞?
Answers:
当我在处理多维数据集时遇到此问题时,我发现了John Amanatides和Andrew Woo于1987年发表的论文“射线追踪的快速Voxel遍历算法”,其中描述了一种可用于此任务的算法。它是准确的,并且每个相交的体素只需要一个循环迭代。
我已经用JavaScript编写了该算法相关部分的实现。我的实现增加了两个功能:它允许指定射线投射距离的限制(用于避免性能问题以及定义有限的“范围”),还可以计算射线进入每个体素的面。
输入origin
向量必须按比例缩放,以使体素的边长为1。direction
向量的长度不重要,但可能会影响算法的数值精度。
该算法通过使用射线的参数化表示进行操作origin + t * direction
。对于各坐标轴,我们跟踪的t
其中如果我们采取了足够跨越沿着该轴的体素边界处的步骤中,我们将具有值在变量(即改变的坐标的整数部分)tMaxX
,tMaxY
和tMaxZ
。然后,我们沿着哪个轴最小的位置(即,哪个体素边界最靠近)采取步骤(使用step
和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;
}
function intbounds(s,ds) { return (ds > 0? Math.ceil(s)-s: s-Math.floor(s)) / Math.abs(ds); }
。因为Infinity
大于所有数字,所以我也不认为您也需要防止ds为0。
1/ds
导致其他轴之一增加。解决方法是编写intfloor
检查是否两者ds
均为负数并且s
是整数值(mod返回0),在这种情况下返回0.0。
也许研究一下Bresenham的line算法,尤其是在使用单位块的情况下(尤其是大多数《我的世界》游戏倾向于这样做)。
基本上,这需要两个点,并在它们之间画一条连续的线。如果将向量从玩家投射到他们的最大拾取距离,则可以使用此值,并将玩家位置作为点。
我在python中有3D实现:bresenham3d.py。
要找到相机前面的第一个块,请创建一个for循环,该循环从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;
}
}