Хороший способ процедурно генерировать блоб-графику в 2D.

Я хочу создать «капля» в вычислительном порядке. Капля здесь определяется как набор пикселей, которые могут быть любой формы, но все связаны между собой. Примеры:

.ooo....  
..oooo..  
....oo..  
.oooooo.
..o..o..  

...ooooooooooooooooooo...  
..........oooo.......oo..  
.....ooooooo..........o..  
.....oo..................  


......ooooooo....  
...ooooooooooo...  
..oooooooooooooo.  
..ooooooooooooooo  
..oooooooooooo...  
...ooooooo.......  
....oooooooo.....  
.....ooooo.......  
.......oo........  

Где . — мертвое пространство, а — отмеченный пиксель. Меня интересует только "бинарная" генерация - пиксель либо включен, либо выключен. Так, например, они будут выглядеть как воображаемая капля кетчупа, или вымышленная бактерия, или любое другое органическое вещество.

Какой алгоритм может достичь этого? я действительно в растерянности


person Nektarios    schedule 27.08.2010    source источник
comment
Какие ограничения на ваш блоб? Программа, которая создает один пиксель, создает блоб в соответствии с вашими спецификациями и довольно эффективно. Если вы не скажете, чего хотите, вы можете получить эффективные ответы, которые удовлетворят ваш вопрос в том виде, в котором он был задан, и не будут такими, какими вы хотите.   -  person David Thornley    schedule 28.08.2010
comment
Справедливо! Размеры X и Y даны для размера ограничивающей рамки, независимо друг от друга, от 1 до, скажем, 20? Может принимать упрощающие предположения, например, что x и y должны быть четными или нечетными. Также для плотности капли было бы здорово иметь возможность сказать, что капля занимает от MIN% до MAX% ограничивающей области, лучше, если я могу сказать, затемнить SPECIFICNUM пикселей. Гибкий в этом, хотя   -  person Nektarios    schedule 28.08.2010
comment
Могут ли быть дырки в блобе?   -  person luke    schedule 28.08.2010
comment
Дырки нежелательны, но не критичны   -  person Nektarios    schedule 28.08.2010


Ответы (3)


Комментарий Дэвида Тонли верен, но я предполагаю, что вам нужна капля с «органической» формой и гладкими краями. Для этого вы можете использовать метаболы. Метабаллы — это степенная функция, работающая со скалярным полем. Скалярные поля можно эффективно визуализировать с помощью алгоритма марширующих кубов. Различные формы могут быть сделаны путем изменения количества шаров, их положения и их радиуса.

См. здесь введение в 2D-метаболы: https://web.archive.org/web/20161018194403/https://www.niksula.hut.fi/%7Ehkankaan/Homepages/metaballs.html

А вот введение в алгоритм марширующих кубов: https://web.archive.org/web/20120329000652/http://local.wasp.uwa.edu.au/%7Epbourke/geometry/polygonise/

Обратите внимание, что 256 комбинаций для пересечений в 3D — это всего лишь 16 комбинаций в 2D. Это очень легко реализовать.

РЕДАКТИРОВАТЬ:

Я собрал быстрый пример с шейдером GLSL. Вот результат использования 50 капель с функцией энергии с домашней страницы hkankaan. альтернативный текст

Вот реальный код GLSL, хотя я оцениваю его пофрагментно. Я не использую алгоритм марширующих кубов. Вам нужно отрендерить полноэкранный четырехугольник, чтобы он работал (два треугольника). Униформ-массив vec3 — это просто двумерные позиции и радиусы отдельных BLOB-объектов, передаваемых с помощью glUniform3fv.

/* Trivial bare-bone vertex shader */
#version 150
in vec2 vertex;
void main()
{
    gl_Position = vec4(vertex.x, vertex.y, 0.0, 1.0);
}

/* Fragment shader */
#version 150
#define NUM_BALLS 50
out vec4 color_out;
uniform vec3 balls[NUM_BALLS]; //.xy is position .z is radius

bool energyField(in vec2 p, in float gooeyness, in float iso)
{
    float en = 0.0;
    bool result = false;
    for(int i=0; i<NUM_BALLS; ++i)
    {
        float radius = balls[i].z;
        float denom =  max(0.0001, pow(length(vec2(balls[i].xy - p)), gooeyness));
        en += (radius / denom);
    }
    if(en > iso)
        result = true;
    return result;
}
void main()
{
    bool outside;
    /* gl_FragCoord.xy is in screen space / fragment coordinates */
    outside = energyField(gl_FragCoord.xy, 1.0, 40.0);
    if(outside == true)
        color_out = vec4(1.0, 0.0, 0.0, 1.0);
    else
        discard;
}
person Community    schedule 27.08.2010
comment
Лучший ответ, который я получил на SO. Я ценю ваше время и согласен, что это прекрасное решение (этой проблемы и других) - person Nektarios; 28.08.2010
comment
Если вам нравится компьютерная графика и процедурно сгенерированные данные, не стесняйтесь посетить нас на IRC/FreeNode. Я на #algorithms, ##opengl и ##opengl3 - person ; 28.08.2010

Вот подход, в котором мы сначала генерируем кусочно-аффинную картофелину, а затем сглаживаем ее путем интерполяции. Идея интерполяции основана на взятии ДПФ, затем оставлении низких частот такими, какие они есть, дополнении нулями на высоких частотах и ​​использовании обратного ДПФ.

Вот код, требующий только стандартных библиотек Python:

import cmath
from math import atan2
from random import random

def convexHull(pts):    #Graham's scan.
    xleftmost, yleftmost = min(pts)
    by_theta = [(atan2(x-xleftmost, y-yleftmost), x, y) for x, y in pts]
    by_theta.sort()
    as_complex = [complex(x, y) for _, x, y in by_theta]
    chull = as_complex[:2]
    for pt in as_complex[2:]:
        #Perp product.
        while ((pt - chull[-1]).conjugate() * (chull[-1] - chull[-2])).imag < 0:
            chull.pop()
        chull.append(pt)
    return [(pt.real, pt.imag) for pt in chull]


def dft(xs):
    pi = 3.14
    return [sum(x * cmath.exp(2j*pi*i*k/len(xs)) 
                for i, x in enumerate(xs))
            for k in range(len(xs))]

def interpolateSmoothly(xs, N):
    """For each point, add N points."""
    fs = dft(xs)
    half = (len(xs) + 1) // 2
    fs2 = fs[:half] + [0]*(len(fs)*N) + fs[half:]
    return [x.real / len(xs) for x in dft(fs2)[::-1]]

pts = convexHull([(random(), random()) for _ in range(10)])
xs, ys = [interpolateSmoothly(zs, 100) for zs in zip(*pts)]   #Unzip.

Это генерирует что-то вроде этого (начальные точки и интерполяция):

Кусочно-аффинный картофель и сглаженная версия

Вот еще попытка:

pts = [(random() + 0.8) * cmath.exp(2j*pi*i/7) for i in range(7)]
pts = convexHull([(pt.real, pt.imag ) for pt in pts])
xs, ys = [interpolateSmoothly(zs, 30) for zs in zip(*pts)]

Примеры

Иногда они имеют изломы и вогнутости. Такова природа этого семейства капель.

Обратите внимание, что SciPy имеет выпуклую оболочку и БПФ, поэтому указанные выше функции могут быть ими заменены.

person Evgeni Sergeev    schedule 24.09.2016
comment
Можете ли вы указать мне на какие-либо статьи, в которых говорится о математике этого способа создания случайных капель? я хотел бы реализовать что-то подобное в JS - person oldboy; 20.12.2020

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

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

Вы можете сформулировать свою задачу как задачу о непересекающихся объединениях. Изначально каждая отдельная ячейка представляет собой отдельный раздел. Что вы хотите, так это объединять разделы, пока не получите небольшое количество разделов (не обязательно два) связанных ячеек. Затем вы просто выбираете один (возможно, самый большой) из разделов и рисуете его.

Для каждой ячейки вам понадобится указатель (изначально нулевой) для объединения. Вам, вероятно, понадобится битовый вектор, чтобы действовать как набор соседних ячеек. Первоначально каждая ячейка будет иметь набор из четырех (или восьми) соседних ячеек.

Для каждой итерации вы выбираете ячейку случайным образом, затем следуете по цепочке указателей, чтобы найти ее корень. В деталях из корня вы найдете набор его соседей. Выберите из него случайный элемент, затем найдите для него корень, чтобы идентифицировать соседний регион. Выполните объединение (укажите один корень на другой и т. д.), чтобы объединить две области. Повторяйте, пока не будете довольны одним из регионов.

При объединении разделов новый набор соседей для нового корня будет представлять собой набор симметричных разностей (исключающее ИЛИ) наборов соседей для двух предыдущих корней.

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

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

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

person Steve314    schedule 27.08.2010