Как исправить мой алгоритм 3D Ray-Casting для получения блока, на который смотрит игрок - PullRequest
0 голосов
/ 03 января 2019

Мой алгоритм расчета, на какой блок смотрит игрок (мир на основе вокселей), работает неправильно. Я адаптировал его из этого урока из 2D в 3D. Иногда он показывает правильный блок, но иногда он либо ничего не возвращает, когда должен, либо что-то в совершенно другом направлении, почему это происходит?

public (Block, Box?) GetLookAtBlock(Vector3 pos, Vector3 look) {
    try {
        look = look.Normalized() * 4;

        float deltaX = Math.Abs(look.Normalized().X);
        float deltaY = Math.Abs(look.Normalized().Y);
        float deltaZ = Math.Abs(look.Normalized().Z);

        int stepX, stepY, stepZ;
        float distX, distY, distZ;

        if (look.X < 0) {
            distX = (pos.X - SandboxMath.RoundDown(pos.X)) * deltaX;
            stepX = -1;
        } else {
            distX = (SandboxMath.RoundDown(pos.X) + 1 - pos.X) * deltaX;
            stepX = 1;
        }

        if (look.Y < 0) {
            distY = (pos.Y - SandboxMath.RoundDown(pos.Y)) * deltaY;
            stepY = -1;
        } else {
            distY = (SandboxMath.RoundDown(pos.Y) + 1 - pos.Y) * deltaY;
            stepY = 1;
        }

        if (look.Z < 0) {
            distZ = (pos.Z - SandboxMath.RoundDown(pos.Z)) * deltaZ;
            stepZ = -1;
        } else {
            distZ = (SandboxMath.RoundDown(pos.Z) + 1 - pos.Z) * deltaZ;
            stepZ = 1;
        }

        int endX = SandboxMath.RoundDown(pos.X + look.X);
        int endY = SandboxMath.RoundDown(pos.Y + look.Y);
        int endZ = SandboxMath.RoundDown(pos.Z + look.Z);

        int x = (int)pos.X;
        int y = (int)pos.Y;
        int z = (int)pos.Z;

        Block start = GetBlock(x, y, z);

        if (start != 0) {
            return (start, new Box(new Vector3(x, y, z), new Vector3(x + 1, y + 1, z + 1)));
        }

        while (x != endX && y != endY && z != endZ) {
            if (distX < distY) {
                if (distX < distZ) {
                    distX += deltaX;
                    x += stepX;
                } else {
                    distZ += deltaZ;
                    z += stepZ;
                }
            } else {
                if (distY < distZ) {
                    distY += deltaY;
                    y += stepY;
                } else {
                    distZ += deltaZ;
                    z += stepZ;
                }
            }

            Block b = GetBlock(x, y, z);
            if (b != 0) {
                return (b, new Box(new Vector3(x, y, z), new Vector3(x + 1, y + 1, z + 1)));
            }
        }

        return (0, null);
    } catch (IndexOutOfRangeException) {
        return (0, null);
    }
}

1 Ответ

0 голосов
/ 03 января 2019

ваш DDA имеет две проблемы, которые я вижу по первому взгляду:

  1. работают, только если Z является главной осью

    , поэтому толькоесли вы находитесь в пространстве камеры или у вас стационарная камера, смотрящая в направлении Z

  2. ваши дельты странные

    почему:

    delta? = abs(1 / look.Normalized().?);
    

    Я бы ожидал:

    delta? = abs(look.Normalized().?);
    

Я не кодирую в C # , поэтому я не уверен, что чиню ваш код, но вот мой шаблон C ++ дляn -мерный DDA , так что просто сравните и восстановите ваш в соответствии с ним ...

template<const int n>class DDA
    {
public:
    int p0[n],p1[n],p[n];
    int d[n],s[n],c[n],ix;

    DDA(){};
    DDA(DDA& a) { *this=a; }
    ~DDA(){};
    DDA* operator = (const DDA *a) { *this=*a; return this; }
    //DDA* operator = (const DDA &a) { ..copy... return this; }

    void start()
        {
        int i;
        for (ix=0,i=0;i<n;i++)
            {
            p[i]=p0[i];  s[i]= 0; d[i]=p1[i]-p0[i];
            if (d[i]>0)  s[i]=+1;
            if (d[i]<0){ s[i]=-1; d[i]=-d[i]; }
            if (d[ix]<d[i]) ix=i;
            }
        for (i=0;i<n;i++) c[i]=d[ix];
        }
    void start(double *fp0) // this will add the subpixel offset according to first point as double
        {
        int i; start();
        for (i=0;i<n;i++)
            {
            if (s[i]<0) c[i]=double(double(d[ix])*(    fp0[i]-floor(fp0[i])));
            if (s[i]>0) c[i]=double(double(d[ix])*(1.0-fp0[i]+floor(fp0[i])));
            }
        }

    bool update()
        {
        int i;
        for (i=0;i<n;i++){ c[i]-=d[i]; if (c[i]<=0){ c[i]+=d[ix]; p[i]+=s[i]; }}
        return (p[ix]!=p1[ix]+s[ix]);
        }
    };

start() инициализируйте переменные и положение для DDA (от p0,p1 контрольных точек) и update() - это всего лишь один шаг DDA ... Результирующая повторная точка находится в p

s - это шаг, d - это дельта, c - счетчик и ix - индекс главной оси.

Использование выглядит следующим образом:

DDA<3> A; // 3D
A.p0[...]=...; // set start point
A.p1[...]=...; // set end point

for (A.start();A.update();)
 {
 A.p[...]; // here use the iterated point
 }

DDA проходит через

хорошо DDA просто интерпоРасстановка (растеризация) целочисленных позиций на некоторой линии между двумя конечными точками (p0, p1).Уравнение строки выглядит следующим образом:

p(t) = p0 + t*(p1-p0);
t = <0.0,1.0>

однако это включает в себя плавающую математику, и нам нужно целое число, чтобы мы могли переписать что-то вроде этого:

dp = p1-p0
D  = max (|dp.x|,|dp.y|,|dp.z|,...)
p.x(i) = p0.x + (dp.x*i)/D
p.y(i) = p0.y + (dp.y*i)/D
p.z(i) = p0.z + (dp.z*i)/D
...
i = { 0,1,...D }

где i,D соответствуетбольшая ось (та, где больше всего изменений)Если вы посмотрите поближе, мы используем *i/D, что является медленной операцией, и нам обычно нужна скорость, поэтому мы можем переписать значение therm (используя тот факт, что i переходит от 0 к D по порядку) в нечто подобное (только для оси xостальные будут одинаковыми с разными индексами ...):

p.x=p0.x;                          // start position
s.x=0; d.x=p1.x-p0.x;              // step and abs delta
if (d.x>0) s.x=+1;
if (d.x<0){ s.x=-1; d.x=-d.x; }
D = max(d.x,d.y,d.z,...);          // major axis abs delta
c.x=D;                             // counter for the iteration
for (i=0;i<D;i++)
 {
 c.x-=d.x;                         // update counter with axis abs delta
 if (c.x<=0)                       // counter overflowed?
  {
  c.x+=D;                          // update counter with major axis abs delta
  p.x+=s.x;                        // update axis by step
  }
 }

Теперь взглянем поближе на счетчик c, к которому мы добавляем D и вычитаем d.x, который являетсяi/D переписано в D итераций.Все остальные оси рассчитываются таким же образом, вам просто нужно добавить счетчик c step s и abs delta d для каждой оси ...

btw, если это поможет взглянуть на это:

  • объемный трассировщик обратного луча GLSL

    , что (я предполагаю) то, что вы делаете, но реализовано в шейдере GLSL (см. Фрагмент кода)он не использует DDA , вместо этого он добавляет вектор направления единицы в начальную позицию до тех пор, пока не достигнет чего-либо или не достигнет конца воксельного пространства ...

кстати, исходя из:

точно так же, как ваша ссылка.

[Редактировать] неправильные попадания(предположил по вашим комментариям)

Скорее всего, это не имеет ничего общего с DDA .Это больше похоже на крайний случай, когда вы стоите прямо на пересечении ячеек, или у вас неправильно усеченная позиция, или вы неправильно отсортировали попадания.Я помню, у меня были проблемы с этим.Я получил очень странное решение в GLSL , см. Ссылку выше и посмотрите фрагмент кода.Ищите

// YZ plane voxels hits

непосредственно после кода приведения луча без "DDA".Он определяет, какая плоскость вокселя будет поражена. Я думаю, что вы должны сделать что-то подобное.Это было странно, так как в 2D DOOM (также по ссылкам выше) у меня не было таких проблем ... но это было связано с тем, что они решались с использованием другой математики (подходит только для 2D).

Код GLSLнезадолго до того, как приведение луча немного меняет положение, чтобы избежать краевых случаев.обратите внимание на floor и ceil, но моя работает на поплавках, так что для математики понадобится некоторая настройка.К счастью, я ремонтировал другой движок для литья лучей, основываясь на этом:

И решение состоит в том, чтобы компенсировать DDA по субпиксельной начальной позиции луча.Я обновил DDA код выше, новое использование:

DDA<3> A; // 3D
A.p0[...]=...; // set start point
A.p1[...]=...; // set end point

for (A.start(start_point_as_double[3]);A.update();)
 {
 A.p[...]; // here use the iterated point
 }

Также на втором уроке убедитесь, что в вашем DDA c,d,s являются целыми числами, если онивместо этого плавающие, это может вызвать проблемы, которые вы описываете тоже ...

...