Pull to refresh

Симплекс Серпинского

Reading time5 min
Views16K


Данная статья предназначена для ознакомления с базовой математической составляющей компьютерной многомерной графики. На примере симплекса Серпинского в статье рассматриваются вопросы построения, перемещения, проецирования и отображения сложных многомерных геометрических фигур.
Также имеются картинки, видео, исходники, и уверяю вас, что всё очень просто, читайте, вникайте.

Что такое треугольник Серпинского, и что такое пирамида Серпинского весьма понятно. Но что такое симплекс Серпинского? Для начала, для тех кто не прочитал в вики, что такое симплекс — скажу просто, что симплекс это n-мерный тетраэдр (см. видео). Ну а симлекс Серпинского — некий фрактал, построенный по аналогии с треугольником и тетраэдром Серпинского.

Построение равностороннего симплекса


Не поверите, но это самое сложное среди всего, что есть в этой статье (по моим оценкам). А ведь всё, что нам нужно — это n точек в n-1 мерном пространстве, равноудаленных друг от друга и от центра. Прежде, чем читать дальше, строго рекомендую подумать, как бы вы строили такие точки?

Конечно же нас интересуют случаи, где n >= 3, иначе просто не интересно. Каждую n-тую точку будем строить на основе всех предыдущих. Для начала, нам понадобится построить обычный равносторонний треугольник:


Для того, чтобы добавить новую точку — нужно новое измерение (обозначим W). Введя это измерение, для того, чтобы точка была равноудалена от всех других точек, установим для этой точки Pn значение q1 по оси W, а остальным точкам по оси W — -q2. Остальные координаты осей для точки Pn оставим нулевыми. Что получается? Что все точки, что были — остаются на равном расстоянии друг от друга и от центра, а новая точка равноудалена от всех остальных. Осталось лишь подобрать такие q1, q2, чтобы все точки были равноудалены друг от друга, и от центра.


l — расстояние между точками — неизменно, и вычисляется после построения треугольника.
d — расстояние от точек до центра — меняется, и вычисляется перед добавлением новой точки.
Первое уравнение указывает на то, что расстояния от принятых точек до новой должны быть одинаковыми.
Второе — что расстояния до центра должны быть одинаковыми.
Собственно, таким образом и получаем равносторонний симплекс.

Исходники:
    double c = r * sqrt(3) / 2;  
    double l = c * 2;    //distance between points
    points[0][0] = + 0; points[0][1] = + r;      //first point
    points[1][0] = + c; points[1][1] = - r / 2;  //second point
    points[2][0] = - c; points[2][1] = - r / 2;  //3th point    

    for (int i = 3; i <= dimensionalCount; i++)
    {
        double d =  points[0].distanceToCenter();

        double q2 = (l * l - 2 * d * d) / (2 * sqrt(l * l - d * d));
        double q1 = sqrt(d * d + q2 * q2);

        for (int k = 0; k < i; k++)
        {
            points[k][i-1] = -q2;   //set i-th dimension for all created points
            points[i][k] = 0;       //set all calculated dimension for new point
        }
        points[i][i-1] = q1;
    }
    

Фракталим


Очень легко можно заметить, что и треугольник (n=2) и тетраэдр (n=3) Серпинского создаются из базовой фигуры методом замены базовой фигуры на n+1 таких же, но поменьше. В каждой из новых фигур за основу берется одна точка базовой фигуры, остальные точки — центры масс отрезков, что включают данную точку. В принципе, всё очень просто и понятно.

Так и запишем:
void rec(QVector<Simplex>& storage, const Simplex& current, int recursionDepth)
{
    if (recursionDepth == maxRecursionDepth)
        storage.append(current);
    else
    {
        for (int i = 0; i <= n; i++)
        {
            Simplex newTriangle(current.dimensionsCount());
            for (int k = 0; k <= n; k++)
            {
                if (i == k)
                    newTriangle[k] = current[i];
                else
                    newTriangle[k] = (current[i] + current[k]) / 2.0;
            }
            rec(storage, newTriangle, recursionDepth + 1);
        }
    }
}

Как видите, тут нигде нет опоры на размерность пространства, и оно корректно отрабатывает и для 2D и для 3D:





Поворот & проекция


Вращать можно сложно (кватернионы, матричные преобразования), а можно просто… Мы будем делать всё просто, последовательно вращать по каждым двум координатам. Для 2D это можно воспринимать как поворот вокруг точки, для 3D — вокруг прямой, для ND — вокруг (N-2)-размерного пространства. Собственно, формула поворота высчитывается очень просто:


Ну и программируется это еще проще:
    for (int i = 0; i < coordinates.count(); i++)
        for (int k = i + 1; k < coordinates.count(); k++)
            {
                ratio = sqrt(2 + i * coordinates.count() + k);
                p1 = temp[i] * cos(angle * ratio) - temp[k] * sin(angle * ratio);
                p2 = temp[k] * cos(angle * ratio) + temp[i] * sin(angle * ratio);
                temp[i] = p1;
                temp[k] = p2;
            }

Где ratio — коэффициент, для того, чтобы вращение в разные стороны было с разными скоростями, желательно без зацикливаний. temp[i] — і-ая координата текущей точки.

Проекция — самый сложный момент работы с многомерными пространствами. Причин много:
1. Никто не привык понимать многомерные геометрические объекты.
2. Оптика физики молчит по этому поводу (насколько я знаю).
3. Куча разных методов и все перегружают картинку, и непонятно, что корректней.

Мы возьмем самый простой метод — перспективная проекция. Будем проецировать n-мерную точку на (n-1) пространство… Таким образом, каждый раз понижая N — дойдем до того, что точка уже двухмерная или трехмерная и уже можно отображать.

На примере 2D to 1D попробуем просчитать формулу перспективной проекции.


Видно, что с помощью 2 координат и некой константы focus можно сделать 1 координату. И формула очень простая, как и код:
  for (int i = coordinates.count() - 1; i > 3; i--)
        for (int k = 0; k < i; k++)
            temp[k] *= focus  / (focus + temp[i]);

Насколько это всё корректно? Вообще не корректно, ни разу, ни капли! Но судя по тому, что есть в инете, что-то очень похожее используется другими программистами. Вот, например, тессеракт, полученый таким методом проецирования:



Рендеринг


QPainter — самый простой способ, что заключается в использовании стандартных средств среды разработки, для прорисовки всего обычными линиями, без освещения, заливки треугольников, прочего. В моих исходниках он включен по умолчанию, и будет работать везде, где есть Qt (Windows, Linux, Mac OS...).

Собственно, вот так выглядит код рендеринга:
  QPoint center(this->width() / 2, this->height() / 2);

  foreach(const Simplex& simplex, simplexes)
     for (int i = 0; i < simplex.dimensionsCount() + 1; i++)
         for (int k = i+1; k < simplex.dimensionsCount() + 1; k++)
             p.drawLine(simplex[i].to2D(focus, angle) + center, simplex[k].to2D(focus, angle) + center);

Как видите, нет ничего проще… Но нам нужно, чтобы было красиво, так ведь?

OpenGL, DirectX — прогрессивный метод, что позволяет в real time рендерить всё красиво. Но есть беда: без прозрачности красивым ничего не будет, а прозрачность у этих двух монстров предполагает, что рендерить нужно от далекого (z -> max) к близкому (z -> min). Для этого, каждый кадр, треугольники нужно сортировать, а их в моем примере порядка 6000. Ну это не беда, беда в том, что в общем случае нельзя определить, какой треугольник ближе, а какой дальше. Более того, когда мы говорим о проекции с многомерного пространства, мы говорим о том что треугольники пересекаются. В итоге их нужно резать и сортировать каждую итерацию, что уже весьма сложно…
Этот метод я не реализовывал.

Трассировка лучей — то, что доктор прописал. Эта технология позволит получить картинку максимального качества и имеет недостаток только в скорости. Но, по сути, real time нам и не нужен.

Для трассировки я использовал POV-Ray, который идеально подошел для этой задачи лишь тем, что умеет запускаться с командной строки, без всякого там GUI.
Для использования сего чуда, я написал некий шаблон, который программой заполняется нужными точками, и на выходе получается готовый к трассировке .pov файл. Шаблон по набору точек строит треугольники и рамки, и очень прост по своей структуре:
#declare ppp = array[<<!!--count of points--!!>>]              
{                                       
<<!!--Main Array of points--!!>>
};                               

#declare i = 0;                             
#while(i < <<!!--count of points--!!>>)                       
    #if (vlength(ppp[i] - ppp[i+1])!=0)             
        cylinder{ppp[i],  ppp[i+1], 0.2             
            texture {pigment{color Gray}}         
        }                                           
    #end                                            
    #if (vlength(ppp[i] - ppp[i+2])!=0)             
        cylinder{ppp[i],  ppp[i+2], 0.2             
            texture {pigment{color Gray}}         
        }                                           
    #end                                            
    #if (vlength(ppp[i+1] - ppp[i+2])!=0)           
        cylinder{ppp[i+1],  ppp[i+2], 0.2           
            texture {pigment{color Gray}}         
        }                                           
    #end   
                 
    polygon {4                                      
        ppp[i], ppp[i+1], ppp[i+2] ,ppp[i]          
  	texture { Surface_Texture }}                                               
                                                    
#declare i=i+3;                                     
#end 

Собственно, на основе этого шаблона было сделано 1000 картинок с разными поворотами, из которых потом и было сделано вот это видео:


Исходники

Статья посвящается девушкам heavy metal коллектива Fight with Fate.
Tags:
Hubs:
+111
Comments49

Articles

Change theme settings