Criando nebulosa de pixels usando ruído e corte médio

Eu queria uma nebulosa no meu jogo The Last Boundary . Eles parecem incríveis e o espaço sem eles não é espaço, mas simplesmente pixels brancos espalhados pelo fundo. Mas como eu criei o jogo no estilo de "pixel art", eu precisava de alguma forma fazer minha biblioteca de ruídos gerar imagens pixeladas.

Aqui estão alguns exemplos:



Mais exemplos





Nos exemplos de uma cor, 8 cores são usadas e, em outras, 16 cores. Neste artigo, falarei sobre como criei uma nebulosa pixelizada para The Last Boundary.

Quando trabalhamos com uma biblioteca de ruídos, como o LibNoise , independentemente do mecanismo que você usa (ou escreve seu próprio), os valores geralmente são distribuídos no intervalo de -1 a 1 . É teoricamente mais provável que o ruído 2D esteja na faixa de -0.7 a 0.7 , mas algumas implementações escalam o resultado, convertendo-o no intervalo de -1 a 1 . Para trabalhar com texturas 2D, geralmente é convertido em um intervalo de 0 a 1 e, em seguida, RGB(255,255,255) dentro do intervalo de RGB(0,0,0) a RGB(255,255,255) .


Ruído Perlin gerado a partir das coordenadas x,y de cada pixel dimensionado para 0.3f

Em seguida, você pode usar o movimento browniano fracionário para dar à imagem uma sensação de esplendor das nuvens.


O ruído de Perlin foi submetido a movimento browniano fracionário com 8 oitavas, frequência 0.01 , regularidade 0.5 e lacunaridade 2.0 .

Percebi que existem muitas implementações incorretas de ruído Perlin, ruído simplex e movimento browniano fracionário (fBm) na Internet. Parece haver muita confusão sobre o que é o quê. Certifique-se de usar a implementação correta, porque se você deseja criar a cadeia descrita acima, em caso de implementação incorreta, poderá não obter os resultados necessários.

Vamos imaginar que queremos criar um efeito de fumaça, ou seja, essa solução nos convém. Mas nosso jogo de pixel art pareceria estranho se um monte de novas cores aparecesse de RGB(0,0,0) a RGB(255,255,255) . De repente, 255 novas notas de cinza apareceriam no jogo.

Precisamos convertê-los para um número limitado de cores. É isso que faremos mais tarde. Enquanto isso ...

Gerar nebulosa aleatória


Repeti para tutoriais prontos para gerar nebulosas aleatórias, mas adicionei alguns dos meus passos e apliquei minha própria biblioteca de ruídos. Escrevi alguns anos atrás porque queria entender bem o ruído de Perlin e como você pode usá-lo junto com outros conceitos para criar texturas e coisas do gênero.

Talvez você possa repetir depois de mim passo a passo ou terá que fazer acréscimos ao código que afetará seu ruído. Vou explicar tudo, exceto a geração inicial de ruído e o fBm, para que você possa escrever o código; Eu acho que pode-se supor que você já tem a capacidade de gerar ruído e fBm.

Para começar, mostrarei o resultado da geração da nebulosa:


Resultado final

É importante notar que ainda não está pixelizado. Tem uma gama completa de cores com um céu estrelado pixelizado. A nebulosa vamos pixelizar mais tarde.

A primeira coisa a fazer é gerar cinco texturas diferentes: vermelho, verde, azul, alfa e máscara. As texturas vermelho, verde e azul são necessárias para os canais finais de cores correspondentes. Na verdade, eu apenas gero um ou dois canais de cores, porque o uso dos três produz uma nebulosa incrivelmente colorida que parece feia. Qualquer cor única ou uma combinação de duas cores funcionará bem.

O canal alfa é importante porque depende se as estrelas mais baixas brilharão através da nebulosa. Ilustrarei isso exibindo o canal alfa do exemplo mostrado acima.


Canal alfa pronto do nosso exemplo

Quanto mais branca a área, mais próximo o valor é de 1.0 , o que nos dá um valor alfa de 255 . Quanto mais preta a área, mais transparente ela é. Se você observar um exemplo, poderá ver que as áreas pretas correspondem às áreas nas quais o céu estrelado é visível.


Exemplo de céu estrelado

Essas não são as mesmas estrelas do exemplo, porque são geradas aleatoriamente em cada captura de tela. Espero que isso não o impeça de entender como a nebulosa é gerada.

Minha biblioteca de ruídos consiste em módulos, seguindo o exemplo do Lib Noise . Tudo nesta biblioteca é "módulos" que podem ser encadeados. Alguns módulos geram novos valores (Módulo Perlin, Valor constante), outros os conectam (Multiply, Add) e outros simplesmente realizam operações no valor (Lerp, Clamp).

Canais coloridos


Não importa se trabalhamos com uma, duas ou três cores - os canais vermelho, verde e azul são gerados da mesma maneira; Eu apenas uso um valor de semente diferente para eles. Meus valores iniciais dependem da hora atual do sistema.

Abaixo, todos são apresentados em escala de cinza, mas teoricamente são simplesmente valores para um dos três canais. A escala de cinza está aqui apenas para ilustrar os resultados.

1. Barulho de Perlin


Como acima, o ruído de Perlin será o ponto de partida. Se você quiser, pode usar ruído simplex, parece que sua implementação em 2D não pertence a Ken Perlin, mas eu posso estar errado. Do ponto de vista matemático, o ruído simplex usa menos instruções, portanto a geração de uma nebulosa semelhante será mais rápida. Como ele usa simplexes em vez de uma grade, cria um ruído um pouco mais bonito, mas não trabalharemos muito com ele, portanto isso não é particularmente importante.

O código real não é mostrado abaixo, porque em fontes reais os valores x,y foram alterados por fBm na etapa 3. Essa é apenas a coordenada x,y da imagem, multiplicada pelo fator de escala estática.


Ruído Perlin gerado a partir das coordenadas x,y de cada pixel dimensionado para 0.3f . I.e. PixelValue = PerlinNoise(x * 0.3f, y * 0.3f)

Os valores criados pelo ruído Perlin estão aproximadamente no intervalo de -1 a 1 ; portanto, para criar a imagem em escala de cinza usual mostrada acima, os convertemos para o intervalo de 0 a 1 . Testei o escopo dos valores para que a conversão produza o maior contraste (o valor mais baixo corresponde a 0 , o maior - 1 ).

2. Multiplicação


O próximo módulo usado multiplica o ruído gerado por 5 . Isso pode ser considerado um ajuste de contraste. Valores negativos são mais escuros, valores positivos são mais claros.

Não tenho nada para mostrar aqui, porque no processo de conversão de valores do intervalo de -5 para 5 para o intervalo de 0 para 1 resultado não muda.

3. Movimento browniano fracionário (fBM)


Esse estágio transforma o ruído no que muitas pessoas consideram um "efeito de ruído" real. Aqui, executamos oitavas de amostras cada vez menores a partir da função de ruído (no nosso caso, a função é perlin(x,y) ) para adicionar fluffiness.


Movimento browniano fracionário do ruído Perlin mostrado acima. 8 oitavas, frequência .01f , regularidade .5f e 2.5f

Você já pode ver a origem de algo interessante. A imagem mostrada acima não é gerada dimensionando as coordenadas x,y dos pixels, o fBM faz isso. Novamente, esses valores são convertidos inversamente para um intervalo de 0 a 1 para um intervalo possível de -5 a 5 .

4. Restrição (braçadeira)


Agora vou limitar os valores a um intervalo de -1 a 1 . Qualquer coisa fora deste intervalo será completamente descartada.


O mesmo fBm, limitado a -1 a 1

A tarefa desta operação é converter os valores em um intervalo mais curto, criando gradientes mais nítidos e aumentando a área em branco ou preto. Essas áreas mortas ou vazias são importantes para o efeito da nebulosa, que abordaremos mais adiante. Se não tivéssemos multiplicado por 5 no início, o grampo não teria mudado nada.

5. Adicione 1


Agora pegamos os valores do grampo e adicionamos 1. A eles, transferimos os valores para o intervalo de 0 a 2 . Após a conversão, os resultados terão a mesma aparência de antes.

6. Divida por 2


Você provavelmente sabe o que acontecerá quando eu dividir o resultado por 2 (multiplicar por .5 ). Na imagem, nada mudará novamente.

As etapas 5 e 6 convertem os valores em um intervalo de 0 a 1 .

7. Crie uma textura de distorção


O próximo passo é criar uma textura de distorção. Farei isso com o ruído Perlin (com o novo valor inicial)> multiplicado por 4> execute fBm. Nesse caso, o fBm usa 5 oitavas, uma frequência de 0.025 , uma regularidade de 0.5 e uma lacunaridade de 1.5 .


Textura de distorção

Essa textura é necessária para criar mais detalhes do que na textura existente da nebulosa. A nebulosa é uma nuvem ondulada bastante grande, e essa textura fará pequenas alterações nela. Através dele, a natureza da grade do ruído de Perlin começará a emergir.

8. Desloque a textura da cor usando a textura de deslocamento


Em seguida, vou pegar essas duas texturas e usar uma para compensar as coordenadas da outra por um fator. No nosso caso, a combinação é assim:


Resultado de viés

A textura da distorção é usada para alterar as coordenadas x,y que estamos procurando nos dados de ruído da fonte.

Lembre-se de que as imagens mostradas acima são apenas para fins ilustrativos. Em cada estágio, na verdade só temos uma função de ruído. Passamos o valor x,y , e ele retorna um número. Em certos estágios, o intervalo desse número pode ser diferente, mas acima o convertemos novamente em escala de cinza para criar uma imagem. A imagem é criada usando cada coordenada x,y da imagem como x,y , transmitida pela função de ruído.

Ou seja, quando dizemos:

Dê-me o valor para o pixel do canto superior esquerdo com X = 0 e Y = 0

A função nos retorna um número. Se pedirmos a Perlin isso, sabemos que será entre -1 e 1 ; se, como acima, aplicamos grampo, adição e multiplicação, obtemos um valor entre 0 e 1 .

Tendo entendido isso, aprendemos que a função de ruído de distorção cria valores na faixa de -1 a 1 . Portanto, para executar o viés quando dizemos:

Dê-me o valor do pixel no canto superior esquerdo com o pixel X = 0 e Y = 0

o módulo de correção solicita primeiro à função de correção as coordenadas x,y . O resultado disso é entre -1 e 1 (como foi acima). Então é multiplicado por 40 (este é o coeficiente que eu selecionei). O resultado será um valor entre -40 e 40 .

Em seguida, pegamos esse valor e o adicionamos às coordenadas no x,y que estávamos procurando, e usamos esse resultado para pesquisar a textura da cor. Cortamos valores negativos com grampo para 0, porque é impossível procurar coordenadas x,y negativas nas funções de ruído (pelo menos na minha biblioteca de ruído).

Ou seja, em geral, é assim:

 ColourFunction(x,y) =     0  1 DisplaceFunction(x,y) =     -1  1 DoDisplace(x,y) = { v = DisplaceFunction(x,y) * factor clamp(v,0,40) x = x + v; y = y + v; if x < 0 then x = 0 if y < 0 then y = 0 return ColourFunction(x,y) } 

Espero que você entenda isso. De fato, não estamos olhando para o x,y que estávamos, mas para o deslocamento. E como a magnitude também é um gradiente suave, ela muda suavemente.

Existem outras maneiras de executar o deslocamento. Minha biblioteca de ruídos possui um módulo que cria um deslocamento em espiral. Ele pode ser usado para desenhar textura, diminuindo gradualmente para vários pontos. Aqui está um exemplo .

Isso é tudo. Repetimos as operações acima três vezes, usando novos valores de sementes para cada canal de cores. Você pode criar um ou dois canais. Não acho que valha a pena criar um terceiro.

Canal alfa


Um canal alfa é criado da mesma maneira que os canais de cores:

  1. Começamos com o barulho de Perlin
  2. Multiplique por 5
  3. fBM com 8 oitavas, frequência 0.005 , regularidade 0.5 e lacunaridade 2.5
  4. Limitamos os resultados usando o Grampo ao intervalo de -1 a 1 , adicionamos 1 , dividimos por 2 (ou seja, alteramos o intervalo de -1 para 1 para o intervalo de 0 para 1 .
  5. Mudamos o resultado em uma pequena quantidade na direção negativa. Eu compenso por 0.4 . Graças a isso, tudo se torna um pouco mais sombrio.
  6. Limitamos os resultados a um intervalo de 0 a 1 . Como mudamos tudo, tornando-o um pouco mais escuro, de fato, criamos mais áreas com 0 , e algumas áreas tiveram valores negativos.

O resultado é uma textura de canal alfa.


Textura alfa

Como eu disse, as áreas pretas serão transparentes e as brancas serão opacas.

Máscaras de canal


Esta é a última textura usada para criar sombras sobrepostas sobre tudo o resto. Começa como todas as outras texturas:

  1. Perlin de ruído
  2. Multiplique por 5
  3. Realizamos fBm, 5 oitavas, frequência 0.01 , regularidade 0.1 , lacunaridade 0.1 . A regularidade é pequena, então a nuvem é menos densa
  4. Execute uma mudança de intervalo de -1 para 1 para um intervalo de 0 para 1

Mas criamos duas dessas texturas:


Mascarar um


Máscara B

Expomos essas duas texturas ao que chamo de módulo Select . De fato, usamos o valor do módulo A ou do módulo B. A escolha depende do valor do módulo C. Ele requer mais dois valores - Select Point e Falloff .

Se o valor no ponto x,y módulo C for maior ou igual ao SelectPoint , usaremos o valor no ponto x,y módulo B. Se o valor for menor ou igual ao SelectPoint - Falloff , usaremos o valor em x,y módulo A.

Se estiver entre o SelectPoint - Falloff e o SelectPoint , executamos a interpolação linear entre os valores x,y do módulo A e do módulo B.

 float select(x, y, moduleA, moduleB, moduleC, selectPoint, falloff) { float s = moduleC(x,y); if(s >= selectPoint) return moduleB(x,y); else if(s <= selectPoint - falloff) return moduleA(x,y); else { float a = moduleA(x,y); float b = moduleB(x,y); return lerp(a, b, (1.0 / ((selectPoint - (selectPoint-falloff)) / (selectPoint - s))); } } 

No nosso caso, o módulo A é um módulo constante com um valor 0 . O módulo B é a primeira textura da máscara A e o Seletor (módulo C) é a segunda máscara de B. O SelectPoint será 0.4 e o Falloff será 0.1 . Como resultado, obtemos:


Ultimate mask

Ao aumentar ou diminuir o SelectPoint , SelectPoint ou aumentamos a quantidade de preto na máscara. Ao aumentar ou diminuir a falloff , aumentamos ou diminuímos as bordas suaves das máscaras. Em vez de uma das máscaras, eu poderia usar o módulo Constant com o valor 1 , mas queria acrescentar um pouco de aleatoriedade às áreas “não mascaradas”.

Misturar canal de cores e máscara


Agora precisamos aplicar uma máscara a cada um dos canais de cores. Isso é feito usando o módulo Blending . Ele combina as porcentagens de valores de dois módulos para que a soma dos valores seja 100%.

Ou seja, podemos pegar 50% do valor em x,y módulo A e 50% do valor em x,y módulo B. Ou 75% e 25%, etc. A porcentagem que extraímos de cada módulo depende de outro módulo - módulo C. Se o valor em x,y módulo C for 0 , obteremos 100% do módulo A e 0% do módulo B. Se for 1 , utilizamos valores inversos.

Combine para cada textura de cor.

  • Módulo A - Valor constante 0
  • O módulo B é o canal de cores que já vimos
  • Módulo C - resultado da máscara

Isso significa que o ruído do canal de cores será exibido apenas onde a máscara tiver valores acima de 0 (áreas mais próximas do branco), e a magnitude de sua visibilidade depende do valor da máscara.

Aqui está o resultado para o nosso exemplo:


Resultado final

Compare isso com o original antes de aplicar a mistura com uma máscara.


Antes de misturar com uma máscara

Talvez este exemplo não seja muito óbvio, mas, devido ao acaso, é difícil selecionar especificamente um bom exemplo. O efeito da máscara é criar áreas mais escuras. Obviamente, você pode personalizar a máscara para que fique mais pronunciada.

É importante aqui que a mesma máscara seja aplicada a todo o canal de cores, ou seja, as mesmas áreas apareçam na sombra.

Combinamos tudo juntos


Nosso exemplo final concluído:


Exemplo pronto

Ele usa os canais vermelho, verde e alfa:


Canal vermelho


Canal verde


Canal alfa

E então nós apenas os colocamos sobre o céu estrelado.

Agora tudo parece muito bom, mas não muito adequado para um jogo de pixel art. Precisamos reduzir o número de cores ...

Corte médio


Esta parte do artigo pode ser aplicada a qualquer coisa. Digamos que você gere uma textura de mármore e queira reduzir o número de cores. É aqui que o algoritmo de corte médio é útil. Vamos usá-lo para reduzir o número de cores na nebulosa mostrada acima.

Isso acontece antes de se sobrepor ao céu estrelado. O número de cores é completamente arbitrário.

O algoritmo Median Cut, conforme descrito na Wikipedia:

Suponha que tenhamos uma imagem com um número arbitrário de pixels e desejemos gerar uma paleta de 16 cores. Coloque todos os pixels da imagem (ou seja, seus valores RGB ) na lixeira . Descubra qual canal de cores (vermelho, verde ou azul) entre todos os pixels da cesta tem o maior intervalo de valores e, em seguida, classifique os pixels de acordo com os valores desse canal. Por exemplo, se o canal azul tiver o maior intervalo de valores, o pixel com o valor RGB (32, 8, 16) será menor que o pixel com o valor RGB (1, 2, 24), porque 16 <24. Após classificar a cesta, coloque a metade superior dos pixels em uma nova cesta. (Esta etapa deu o nome ao algoritmo de corte mediano; cestas são divididas ao meio pela mediana da lista de pixels.) Repita o processo para as duas cestas, o que nos dará 4 cestas, depois repita para todas as 4 cestas, obtenha 8 cestas e, em seguida, 8 cestas, obtemos 16 cestas. Calculamos a média dos pixels em cada uma das cestas e obtemos uma paleta de 16 cores. Como o número de cestas dobra a cada iteração, o algoritmo pode gerar apenas essas paletas, o número de cores em que é uma potência de duas . Por exemplo, para gerar uma paleta de 12 cores, você precisa primeiro gerar uma paleta de 16 cores e, de alguma forma, combinar algumas cores.

Fonte: https://en.wikipedia.org/wiki/Median_cut

Essa explicação me pareceu bastante ruim e não particularmente útil. Ao implementar o algoritmo, imagens bastante feias são obtidas dessa maneira. Eu o implementei com algumas mudanças:

  1. Armazenamos o contêiner de boxes junto com o valor que indica o intervalo (mais sobre isso abaixo). A box simplesmente armazena um número dinâmico de pixels da imagem original.
  2. Adicione todos os pixels da imagem original como a primeira e use o intervalo 0
  3. Embora o número total de menor que o número necessário de cores, continuamos as etapas a seguir.
  4. Se o valor do intervalo for 0 , para cada caixa atual, determinamos o canal de cores principal dessa box e, em seguida, classificamos os pixels nessa box por essa cor. — Red, Green, Blue Alpha, . , redRange = Max(Red) - Min(Red) . , .
  5. box boxes . , box .
  6. , 4 5 box , boxes . , , , . , , .
  7. box ( == ) boxes . 0 ( ). , , , — . .

Quando atingimos o número de caixas igual ao número desejado de cores, simplesmente calculamos a média de todos os pixels em cada caixa para determinar o elemento da paleta que melhor se adequa a essas cores. Acabei de usar a distância euclidiana, mas existem soluções perceptivas que podem fazer isso melhor.

Aqui está uma imagem que explica tudo mais claramente. Para demonstração, eu uso apenas RGB, porque o alfa é difícil de mostrar.


Vamos aplicar esse método à nossa imagem de exemplo.


O original


Mediana Cortar até 16 cores

Descobri que ao usar dois canais de cores, um bom efeito é obtido com 16 cores. Mas lembre-se de que aqui usamos o canal alfa, que também está envolvido no cálculo da distância entre cores. Portanto, se você não se importa com transparência, pode usar menos cores. Como meu corte mediano, ao contrário do exemplo da Wikipedia, pode usar um número arbitrário de cores (e não apenas dois graus), você pode personalizá-lo para atender às suas necessidades.


De 16 a 2. Cores.

Selecionamos uma cor para cada uma box, calculando a média de todos os valores. No entanto, este não é o único caminho. Você deve ter notado que nosso resultado em comparação com o original não é tão brilhante. Se você precisar, poderá dar preferência nos intervalos superiores, adicionando peso à definição de intervalos. Ou você pode facilmente selecionar 1, 2 ou 3 das cores mais brilhantes da imagem e adicioná-las à paleta. Portanto, se você precisar de 16 cores, gere uma paleta de 13 cores e adicione manualmente suas cores brilhantes.


Uma paleta com as três cores mais brilhantes

Agora tudo parece muito bom, mas a imagem é muito irregular. Possui grandes áreas da mesma cor. Agora precisamos suavizá-los.

Dithering


Não preciso dizer o que é o pontilhamento, porque você já trabalha com pixel art. Portanto, para obter uma imagem mais suave, usaremos um dos algoritmos de pontilhamento, dos quais existem muitos.

Eu implementei um algoritmo de pontilhamento simples de Floyd-Steinberg . Não houve surpresas desagradáveis. No entanto, o efeito foi bastante forte. Aqui está o nosso exemplo novamente:


Original

Em seguida, cortamos a paleta em 16 cores:


Os valores são mapeados para uma paleta de cores 16.

E agora o pontilhamento é seguido pela conversão em uma paleta:


Resultado final com pontilhamento

Source: https://habr.com/ru/post/pt464203/


All Articles