Primeira vez aqui? Seja bem vindo e cheque o FAQ!
x

Como usar trigonometria e ruídos de Perlin para criar padrões de ondulação suaves?

+3 votos
219 visitas
perguntada Set 2, 2020 em Design por Renata Oliveira (41 pontos)  

Como reproduzir, usando python, os padrões da figura 4.15 do livro “Generative Art: A Practical Guide Using Processing” por Matt Pearson?
A imagem será apresentada aqui.

As figuras foram geradas com o código abaixo:
A imagem será apresentada aqui.

Compartilhe

1 Resposta

+1 voto
respondida Set 2, 2020 por Renata Oliveira (41 pontos)  
editado Dez 18, 2020 por Renata Oliveira

Para “traduzir” o código da pergunta para python, os passos apresentados no livro foram seguidos. Antes de qualquer coisa, os módulos a serem utilizados são importados:

import numpy as np
from turtle import *
import noise

Primeiramente, uma circunferência simples é desenhada usando trigonometria: os pontos (x,y) que formam uma circunferência de raio r centrada no ponto (centerX, centerY) são obtidos ao incrementar o ângulo \(\theta\) e calcular:

\[x=centerX + r*cos(\theta)\\ y=centerY + r*sen(\theta)\]

Usando o módulo turtle do python é possível desenhar a circunferência com um loop que, para cada ângulo, calcula os valores de x e y dado o raio e as coordenadas do centro da circunferência:

def trig_circle(radius, centerX=0, centerY=0):
    for theta in range(360):
        rad=np.radians(theta) #converts degrees to radians
        x=centerX+radius*np.cos(rad)
        y=centerY+radius*np.sin(rad)
        goto(x,y)
        pendown()

Para desenhar uma circunferência de raio 100:

if __name__=='__main__':
    begin_fill()
    speed(0)
    penup()
    trig_circle(100)
    done()

O resultado é
A imagem será apresentada aqui.

Para criar uma espiral, o raio da circunferência é incrementado por uma constante a cada novo ponto (x,y). Na função abaixo, que difere da primeira apenas pela última linha que amplia o raio, a variável “size” indica quantas voltas terá a espiral:

def trig_spiral(size, radius, centerX=0, centerY=0, r_increment=0.1):
    for theta in range(size*360):
        rad=np.radians(theta)
        x=centerX+radius*np.cos(rad)
        y=centerY+radius*np.sin(rad)
        goto(x,y)
        pendown()
        radius=radius+r_increment

Para desenhar a espiral:

if __name__=='__main__':
    begin_fill()
    speed(0)
    penup()
    trig_spiral(4, 10)
    done()

A imagem será apresentada aqui.

Adiciona-se então um ruído à espiral usando o módulo “noise”, que permite criar ruídos de Perlin. Este tipo de ruído é utilizado em computação gráfica para criar texturas e padrões de ondulação mais “naturais” ou “suaves”, uma vez que a sequência de números é pseudoaleatória. Uma função que gera um ruído contínuo inicialmente gera pontos aleatórios e depois suavemente interpola estes pontos. O ruído de Perlin resulta da soma de funções de ruídos como estas com diferentes frequências e amplitudes.

Uma função foi definida para criar uma lista de ruídos de Perlin unidimensionais através da função “pnoise1”. Os argumentos desta função determinam a suavidade do ruído. O argumento “octaves” indica quantos ruídos serão sucessivamente adicionados para criar o ruído de Perlin (o padrão para um ruído simples é octaves=1). O argumento “persistence” especifica a amplitude de cada ruído sucessivo em relação ao anterior (default: persistence=0.5, de modo que a amplitude do ruído adicional é a metade da amplitude do ruído anterior). Por fim, “lacunarity” indica a frequência de cada ruído sucessivo relativamente ao anterior, sendo 2 o valor padrão. Na função abaixo, a lista de ruídos admite uma escala que em geral será igual a 100, de modo que os valores da lista variam entre -1 e 1.

def noise_generator(shape, scale=100, octaves=6, persistence=0.5, lacunarity=2):
    noise_list=np.zeros(shape)
    for i in range(shape):
        noise_list[i] = noise.pnoise1(i/scale, octaves=octaves, persistence=persistence, lacunarity=lacunarity)
    return noise_list

def noisy_spiral(size, radius, centerX=0, centerY=0, scale=100, r_increment=0.1):
    noise_list=noise_generator(shape=size*360, scale=scale)
    for theta in range(size*360):
        rad=np.radians(theta)
        radius_noise=noise_list[theta]*100
        thisradius=radius+radius_noise
        x=centerX+thisradius*np.cos(rad)
        y=centerY+thisradius*np.sin(rad)
        goto(x,y)
        pendown()
        radius=radius+r_increment*2

Desenhando a espiral com ruído:

if __name__=='__main__':
    begin_fill()
    speed(0)
    penup()
    noisy_spiral(4,10)
    done()

A imagem será apresentada aqui.

A partir destas formas iniciais é possível desenhar padrões ondulados. Começando pelo desenho de um círculo formado por linhas que conectam pontos opostos de uma circunferência. A função abaixo segue a mesma lógica da primeira, porém desta vez são calculados os pontos (x,y) da circunferência e os seus pontos opostos. Uma linha conecta os dois pontos.

def circle_lines(radius, size, centerX=0, centerY=0):
    for theta in range(size): 
        rad=np.radians(theta)
        opp_rad=rad+np.pi
        x=centerX+radius*np.cos(rad)
        y=centerY+radius*np.sin(rad)
        opp_x=centerX+radius*np.cos(opp_rad)
        opp_y=centerY+radius*np.sin(opp_rad)
        goto(x,y)
        goto(opp_x, opp_y)

Para desenhar este tipo de círculo:

if __name__=='__main__':
    begin_fill()
    speed(0)
    circle_lines(100, 130)
    goto(0,0)
    clear()
    circle_lines(100, 180)
    done()

A imagem será apresentada aqui.
A imagem será apresentada aqui.

Adicionando ruído ao raio do círculo anterior, assim como foi feito na primeira parte ao desenhar apenas a circunferência:

def noisy_circle_lines(radius, size, centerX=0, centerY=0, scale=100):
    noise_list=noise_generator(shape=size, scale=scale)
    for theta in range(size): 
        rad=np.radians(theta)
        opp_rad=rad+np.pi
        radius_noise=noise_list[theta]*100
        thisradius=radius+radius_noise
        x=centerX+thisradius*np.cos(rad)
        y=centerY+thisradius*np.sin(rad)
        opp_x=centerX+thisradius*np.cos(opp_rad)
        opp_y=centerY+thisradius*np.sin(opp_rad)
        goto(x,y)
        goto(opp_x, opp_y)

O desenho é feito a partir do código:

if __name__=='__main__':
    begin_fill()
    speed(0)
    noisy_circle_lines(100, 180)
    done()

A imagem será apresentada aqui.

Finalmente, é possível adicionar ruído também ao ponto que define o centro da forma circular de modo que a cada passo do loop o centro da figura é alterado. Esta última alteração permite a criação de padrões similares aos da figura 4.15. Para gerar estes padrões foi definida uma classe:

class Wave_clock:
    def __init__ (self,
                size,              #number of "laps" for a spiral
                radius,            #radius
                detail=2,          #increase this parameter for more detail
                centerX=0,         #center of the circle (x coordinate)
                centerY=0,         #center of the circle (y coordinate)
                r_increment=2,     #increment to the radius
                scale=100,         #scale of the noise list (100 for a list between -1 and 1)
                octaves=6,         #number of noise functions added
                persistence=0.5,   #amplitude of each successive octave added relative to the one below it
                lacunarity=2,      #frequency of each successive octave added relative to the one below it
                xnoise=12,         #noise to x coordinate for the center of the spiral
                ynoise=20          #noise to y coordinate for the center of the spiral
                ):
        self.size=size
        self.radius=radius
        self.detail=detail
        self.centerX=centerX
        self.centerY=centerY
        self.r_increment=r_increment
        self.scale=scale
        self.octaves=octaves
        self.persistence=persistence
        self.lacunarity=lacunarity
        self.xnoise=xnoise
        self.ynoise=ynoise

    def noise_generator(self): #creates a list of noises between 0 and 
        noise_list=np.zeros(self.size*360)
        for i in range(self.size*360):
        noise_list[i] = noise.pnoise1(i/self.scale, octaves=self.octaves, persistence=self.persistence, lacunarity=self.lacunarity)
        return noise_list

    def wave_clock(self): #adds noise to the center of the circumference
        noise_list=self.noise_generator()
        radius=self.radius
        centerX=self.centerX
        centerY=self.centerY
        for i in range(self.size*360):
            x_noise=noise_list[i]*self.xnoise
            y_noise=noise_list[i]*self.ynoise
            radius_noise=noise_list[i]*10
            angle=i*(1/self.detail)
            thisradius=radius*5+radius_noise
            rad=np.radians(angle)
            opp_rad=rad + np.pi
            x=centerX+thisradius*np.cos(rad)
            y=centerY+thisradius*np.sin(rad)
            opp_x=centerX+thisradius*np.cos(opp_rad)
            opp_y=centerY+thisradius*np.sin(opp_rad)
            goto(x,y)
            penup()
            goto(opp_x, opp_y)
            pendown()
            radius=radius+self.r_increment
            centerX=centerX+x_noise*2
            centerY=centerY+y_noise*2   

Usando os parâmetros padrão da classe, obtém-se a imagem:

if __name__=='__main__':
    my_waveclock=Wave_clock(2,100)
    begin_fill()
    speed(0)
    screensize(15000,15000)
    my_waveclock.wave_clock()
    done()

A imagem será apresentada aqui.

Alterando alguns parâmetros é possível criar diferentes padrões. Os parâmetros a seguir dão origem às imagens abaixo, respectivamente:

if __name__=='__main__':
    my_wave_clock1 = Wave_clock(1, 100, detail = 2)
    my_wave_clock2 = Wave_clock(1, 100, detail = 2, xnoise=-14, ynoise=-10)
    my_wave_clock3 = Wave_clock(1, 100, detail = 2, xnoise=-12, ynoise=12)
    my_wave_clock4 = Wave_clock(1, 100, detail = 2, xnoise=0, ynoise=10)
    my_wave_clock5 = Wave_clock(2, 100, detail = 4)
    my_wave_clock6 = Wave_clock(2, 100, detail = 8)

A imagem será apresentada aqui.
A imagem será apresentada aqui.
A imagem será apresentada aqui.
A imagem será apresentada aqui.
A imagem será apresentada aqui.
A imagem será apresentada aqui.

comentou Nov 11, 2020 por Carlos Alexandre (51 pontos)  
Olá Renata! Achei sua resposta muito legal, completa, didática e bem escrita, com todos os passos muito claros! Está um ótimo tutorial de iniciação do uso do turtle (eu não conhecia até então). Muito interessante também que, com poucas regras geométrica e um pouco de ruído, é possível gerar tais imagens. Legal também que utilizou orientação a objetos na implementação final. Testei todos os segmentos de códigos, e estão 100% replicáveis. No meu caso, testei no Spyder, na distribuição 2020.07 do Anaconda, e foi necessário apenas a instalação adicional da biblioteca "noise" (conda install -c conda-forge noise). Como contribuição, após pesquisar um pouco sobre o turtle, poderia apenas indicar que, para quem desejar, o comando turtle.speed("fastest") pode ser interessante para a execução mais rápida do desenho, e caso deseje-se salvar a imagem em formato postscript, por exemplo, pode-se utilizar turtle.getcanvas().postscript(file="nome_arquivo.ps") antes de turtle.done(), com a observação de que a imagem salva será a da porção visível da tela no momento em que o desenho é finalizado, e não de toda a área alocada para o turtle. Por fim, na primeira definição da função noise_generator, na formação do noise_list, talvez seja interessante incluir os argumentos de persistence e lacunarity na noise.pnoise1 (como foi feito na mesma função na implementação final, quando faz parte da classe Wave_clock).
comentou Dez 18, 2020 por Renata Oliveira (41 pontos)  
Obrigada Carlos! fiz a alteração sugerida na função noise_generator. Sobre a velocidade do turtle, turtle.speed(0) é equivalente a speed("fastest") para fazer o desenho mais rápido.
comentou Dez 18, 2020 por Carlos Alexandre (51 pontos)  
Obrigado Renata! Mas o código estava com o speed(0) desde a primeira implementação? Não devo ter visto então, não sei por que, para ter ido atrás de alguma forma acelerar o desenho rsrs
comentou Dez 18, 2020 por Renata Oliveira (41 pontos)  
Antes apenas o último desenho estava com a velocidade máxima, por ser uma imagem maior. Mas alterei a resposta e inclui a linha speed(0) para fazer todos os outros desenhos ficarem mais rápidos!
comentou Dez 18, 2020 por Carlos Alexandre (51 pontos)  
Ah, legal! Obrigado Renata!
...