Нарисуйте сферу, используя 3D-пиксели (воксели)

Можете ли вы предложить алгоритм, который может рисовать сферу в трехмерном пространстве, используя только базовый примитив plot(x,y,z) (который будет рисовать один воксель)?

Я надеялся на что-то похожее на круговой алгоритм Брезенхема, но для 3D вместо 2D.

К вашему сведению, я работаю над аппаратным проектом, который представляет собой 3D-дисплей с низким разрешением, использующий 3-мерную матрицу светодиодов, поэтому мне нужно на самом деле нарисовать сферу, а не только 2D-проекцию (то есть круг).

Проект очень похож на этот:

3D светодиодный куб

... или увидеть его в действии здесь.

Я имею в виду одну возможность:

  • вычислить координаты Y полюсов (с учетом радиуса) (для сферы с центром в начале координат это будут -r и +r)
  • разрезать сферу: для каждой горизонтальной плоскости p i между этими координатами вычислить радиус круга, полученного пересечением указанной плоскости со сферой => r i.
  • нарисуйте фактическую окружность радиуса r i на плоскости p i, используя алгоритм Брезенхема.

FWIW, я использую микропроцессор на базе микросхемы .NET, поэтому программирование осуществляется на C #, но мне не нужны ответы на C #.


person Cristian Diaconescu    schedule 31.01.2012    source источник
comment
Судя по светодиодной сфере, следует ли предположить, что вам нужно как-то нарисовать внутреннюю часть сферы?   -  person NominSim    schedule 31.01.2012
comment
@NominSim Я так не думаю. В этом случае он не стал бы говорить о растеризации круга Брезенхэма и мог бы просто использовать решение Джапрайса методом грубой силы.   -  person Christian Rau    schedule 31.01.2012
comment
@Christian На самом деле, мне бы хотелось иметь оба варианта.   -  person Cristian Diaconescu    schedule 01.02.2012
comment
В этом случае я предлагаю использовать алгоритм грубой силы для создания твердотельной версии, а затем использовать морфологическую операцию для получения поверхности. Solid - Erode(Solid) должен помочь.   -  person japreiss    schedule 01.02.2012
comment
Я не уверен, но, возможно, я ответил на ваш вопрос с помощью алгоритма сферы Брезенхема здесь: stackoverflow.com/questions/9683965/   -  person Nick Udell    schedule 13.03.2012


Ответы (5)


Простой метод грубой силы - перебрать каждый воксель в сетке и вычислить его расстояние от центра сферы. Затем раскрасьте воксель, если расстояние до него меньше радиуса сферы. Вы можете сэкономить много инструкций, исключив квадратный корень и сравнив скалярное произведение с квадратом радиуса.

Довольно далеко от оптимального, конечно. Но на сетке 8x8x8, как показано, вам нужно будет проделать эту операцию 512 раз для каждой сферы. Если центр сферы находится на сетке, а ее радиус является целым числом, вам нужна только целочисленная математика. Скалярное произведение - это 3 умножения и 2 прибавления. Умножение происходит медленно; допустим берут по 4 инструкции. Плюс нужно сравнение. Добавьте сюда нагрузки и запоминания, скажем, это стоит 20 инструкций на воксель. Это 10240 инструкций на сферу.

Arduino, работающий на частоте 16 МГц, может выдавать 1562 сферы в секунду. Если вы не занимаетесь множеством других математических операций и операций ввода-вывода, этот алгоритм должен быть достаточно хорош.

person japreiss    schedule 31.01.2012
comment
Что ж, проблема в том, что ему, кажется, не нужна твердая сфера. В этом случае вы получили классическую задачу растеризации, заключающуюся в предотвращении дырок и вздутий на поверхности сферы. Хотя он все еще может использовать ваше решение и сделать второй проход, удалив все внутренние вокселы, проверив их 6-соседей. - person Christian Rau; 31.01.2012
comment
Хорошо, вам не нужен второй проход для удаления внутренних вокселей, поскольку объект определен неявно. Но в этом случае вам придется вычислять расстояние в среднем более 1 раза на воксель. - person Christian Rau; 31.01.2012
comment
Ага, сначала я подумал, что это залито в демо-видео, но при ближайшем рассмотрении похоже только на оболочку. Я согласен с вашим первым комментарием, проще всего было бы провести морфологическую операцию. - person japreiss; 31.01.2012

Я не верю, что запуск алгоритма окружности средней точки на каждом слое даст желаемые результаты, когда вы достигнете полюсов, так как у вас будут зазоры на поверхности, где светодиоды не горят. Тем не менее, это может дать желаемый результат, и это будет зависеть от эстетики. Этот пост основан на использовании алгоритма круга средней точки для определения радиуса слоев через два средних вертикальных октанта, а затем при рисовании каждого из этих кругов также устанавливает точки для полярных октантов.

Я думаю, что на основе комментария @Nick Udall и ответа здесь Использование кругового алгоритма для определения радиуса вашего горизонтального среза будет работать с модификацией, которую я предложил в комментарии к его ответу. Алгоритм круга должен быть изменен, чтобы принимать в качестве входных данных исходную ошибку, а также рисовать дополнительные точки для полярных октантов.

  • Нарисуйте стандартные точки алгоритма круга в точках y0 + y1 и y0 - y1: x0 +/- x, z0 +/- z, y0 +/- y1, x0 +/- z, z0 +/- x, y0 +/- y1, всего 16 точек. Это составляет основную часть вертикали сферы.
  • Дополнительно нарисуйте точки x0 +/- y1, z0 +/- x, y0 +/- z и x0 +/- x, z0 +/- y1, y0 +/- z, всего 16 точек, которые сформируют полярные шапки для сферы.

Путем передачи ошибки внешнего алгоритма в алгоритм круга он позволяет выполнять субвоксельную корректировку круга каждого слоя. Без передачи ошибки во внутренний алгоритм, экватор круга будет аппроксимирован цилиндром, а каждая аппроксимированная грань сферы по осям x, y и z будет образовывать квадрат. С учетом ошибки каждая грань с достаточно большим радиусом будет аппроксимирована как закрашенная окружность.


Следующий код является изменением алгоритма среднего круга Википедии. В алгоритме DrawCircle номенклатура изменена для работы в плоскости xz, добавлены третья начальная точка y0, смещение по оси y y1 и начальная ошибка error0. DrawSphere был изменен из той же функции, чтобы взять третью начальную точку y0 и вызывать DrawCircle, а не DrawPixel

public static void DrawCircle(int x0, int y0, int z0, int y1, int radius, int error0)
{
  int x = radius, z = 0;
  int radiusError = error0; // Initial error state passed in, NOT 1-x

  while(x >= z)
  {
    // draw the 32 points here.
    z++;
    if(radiusError<0)
    {
      radiusError+=2*z+1;
    }
    else
    {
      x--;
      radiusError+=2*(z-x+1);
    }
  }
}

public static void DrawSphere(int x0, int y0, int z0, int radius)
{
  int x = radius, y = 0;
  int radiusError = 1-x;

  while(x >= y)
  {
    // pass in base point (x0,y0,z0), this algorithm's y as y1,
    // this algorithm's x as the radius, and pass along radius error.
    DrawCircle(x0, y0, z0, y, x, radiusError);
    y++;
    if(radiusError<0)
    {
      radiusError+=2*y+1;
    }
    else
    {
      x--;
      radiusError+=2*(y-x+1);
    }
  }
}

Для сферы с радиусом 4 (что на самом деле требует 9x9x9), это запустит три итерации процедуры DrawCircle, при этом первая будет рисовать типичный круг радиуса 4 (три шага), вторая рисовать круг радиуса 4 с начальной ошибкой 0 ( также три шага), а затем третий рисунок круга радиуса 3 с начальной ошибкой 0 (также три шага). В итоге получается девять расчетных точек по 32 пикселя каждая. Это составляет 32 (точек на круг) x 3 (операций сложения или вычитания на точку) + 6 (операций сложения, вычитания, сдвига на итерацию) = 102 операций сложения, вычитания или сдвига на вычисленную точку. В этом примере это 3 точки для каждого круга = 306 операций на слой. Алгоритм радиуса также добавляет 6 операций на слой и повторяется 3 раза, поэтому 306 + 6 * 3 = 936 базовых арифметических операций для примера с радиусом 4. Стоимость здесь состоит в том, что вы будете повторно устанавливать некоторые пиксели без дополнительных проверок условий (т.е. x = 0, y = 0 , или z = 0), поэтому, если ваш ввод-вывод медленный, вам может быть лучше добавить проверки условий. Предполагая, что все светодиоды были сброшены в начале, в круге примера будет установлено 288 светодиодов, в то время как количество светодиодов, которые на самом деле будут гореть, будет намного меньше из-за повторяющихся наборов.

Похоже, что это будет работать лучше, чем метод грубой силы для всех сфер, которые вписываются в сетку 8x8x8, но метод грубой силы будет иметь согласованную синхронизацию независимо от радиуса, в то время как этот метод будет замедляться при рисовании сфер большого радиуса, где будет только часть отображается. Однако по мере увеличения разрешения куба дисплея время этого алгоритма будет оставаться неизменным, в то время как брутфорс будет увеличиваться.

person rjp    schedule 04.12.2013
comment
Я не понимаю, где собственно часть для рисования сюжета - person John T; 12.11.2019

Предполагая, что у вас уже есть функция сюжета, как вы сказали:

    public static void DrawSphere(double r, int lats, int longs)
    {
        int i, j;
        for (i = 0; i <= lats; i++)
        {
            double lat0 = Math.PI * (-0.5 + (double)(i - 1) / lats);
            double z0 = Math.Sin(lat0) * r;
            double zr0 = Math.Cos(lat0) * r;

            double lat1 = Math.PI * (-0.5 + (double)i / lats);
            double z1 = Math.Sin(lat1) * r;
            double zr1 = Math.Cos(lat1) * r;

            for (j = 0; j <= longs; j++)
            {
                double lng = 2 * Math.PI * (double)(j - 1) / longs;
                double x = Math.Cos(lng);
                double y = Math.Sin(lng);

                plot(x * zr0, y * zr0, z0);
                plot(x * zr1, y * zr1, z1);
            }
        }
    }

Эта функция должна построить сферу в начале координат с заданным разрешением по широте и долготе (судя по вашему кубу, вы, вероятно, захотите что-то около 40 или 50 в качестве приблизительного предположения). Однако этот алгоритм не «заполняет» сферу, поэтому он обеспечивает только контур, но игра с радиусом должна позволить вам заполнить внутреннюю часть, возможно, с уменьшением разрешения широт и длин по ходу.

person NominSim    schedule 31.01.2012
comment
разве вам не нужно умножать координаты на r в вызовах сюжета? поскольку r не используется, это означает, что он всегда будет рисовать сферу радиуса 1. - person Cedric Mamo; 22.03.2013
comment
Почему 2 участка? Какая разница. - person John T; 12.11.2019

Только что нашел старый вопрос и ответ о создании Sphere Mesh, но главный ответ на самом деле дает вам небольшой фрагмент псевдокода для генерации ваших X, Y и Z:

(x, y, z) = (sin(Pi * m/M) cos(2Pi * n/N), sin(Pi * m/M) sin(2Pi * n/N), cos(Pi * m/M))

Подробности смотрите в этом разделе вопросов и ответов :) процедурно сгенерируйте сферическую сетку

person Kotch    schedule 31.01.2012
comment
Разве это не просто решение NominSim? - person Christian Rau; 31.01.2012

Мое решение использует математику с плавающей запятой вместо целочисленной математики, что не идеально, но работает.

private static void DrawSphere(float radius, int posX, int poxY, int posZ)
{
    // determines how far apart the pixels are
    float density = 1;

    for (float i = 0; i < 90; i += density)
    {
        float x1 = radius * Math.Cos(i * Math.PI / 180);
        float y1 = radius * Math.Sin(i * Math.PI / 180);

        for (float j = 0; j < 45; j += density)
        {
            float x2 = x1 * Math.Cos(j * Math.PI / 180);
            float y2 = x1 * Math.Sin(j * Math.PI / 180);
            
            int x = (int)Math.Round(x2) + posX;
            int y = (int)Math.Round(y1) + posY;
            int z = (int)Math.Round(y2) + posZ;

            DrawPixel(x, y, z);
            DrawPixel(x, y, -z);
            DrawPixel(-x, y, z);
            DrawPixel(-x, y, -z);

            DrawPixel(z, y, x);
            DrawPixel(z, y, -x);
            DrawPixel(-z, y, x);
            DrawPixel(-z, y, -x);

            DrawPixel(x, -y, z);
            DrawPixel(x, -y, -z);
            DrawPixel(-x, -y, z);
            DrawPixel(-x, -y, -z);

            DrawPixel(z, -y, x);
            DrawPixel(z, -y, -x);
            DrawPixel(-z, -y, x);
            DrawPixel(-z, -y, -x);
        }
    }
}
person Oogamar    schedule 19.03.2021