Como cultivar uma floresta no Actionscript3 / Flash em algumas * linhas de código

Nesse comentário, eu me gabava de ter escrito um programa que criava uma floresta de aparência decente em duzentas linhas de código. Infelizmente, a realidade acabou sendo um pouco maior - as fontes escavadas contêm cerca de 2100 linhas de código, das quais cerca de 700 são comentários, pensamentos em voz alta, código descartado antigo e tentativas de documentar métodos. O tamanho do executável SWF, no entanto, acabou sendo 13112 bytes.

Tudo começou com o fato de que no fórum Kongregate.com, onde eu estava participando ativamente da época, um dos participantes sugeriu competir na geração processual de algo, o primeiro tópico foi "Floresta" .



Naturalmente, todos tinham sua própria idéia do que deveria ser a floresta que eles cultivariam. Naquela época, eu li livros sobre todo tipo de magia, como resultado, eu queria cultivar uma floresta. A floresta consiste em árvores - escrevemos a classe Árvore {...}. Uma árvore consiste em galhos e folhas - escrevemos na classe Branch {...} e pensamos: realmente precisamos levar em consideração cada folha da árvore? Como resultado, o “ramo” adquiriu o parâmetro “com folhas” e a árvore adquiriu um par de texturas, uma para galhos e um tronco, outra para folhas. A textura “sob a árvore” era relativamente simples de fazer - há um ruído constante, você pode esticar, embrulhar, pintar, considerar pronto, mas você teve que mexer nas folhas.

No entanto, eu não estava satisfeito apenas com o ruído da textura de uma árvore, em vez disso, criei o bumpmapping - ou seja, Ele criou um mapa de altura, ajustou-o sob o semicírculo do galho visível de lado, depois encheu a textura principal de marrom e sobrepôs um mapa de altura com a iluminação ajustada ao guincho lateral. O código resultante é o seguinte:

private function generateBranch():void { branchBitmap = new BitmapData(512, 512, true, 0xff8080ff); //branchBitmap.perlinNoise(32, 256, 2, 100 + Math.round(Math.random() * 900), true, true, 7, true); var hm:BitmapData = new BitmapData(512, 512, false, 0); var seed:int = 1000 + Math.random() * 2000; hm.perlinNoise(24,192, 2, seed, true, true, BitmapDataChannel.BLUE, false); // blue only. Is a heightmap var i:int; var j:int; for (i = 0; i < 512; i++) { if (Math.abs(i - 256) > 100) r = 0; else r = 200 * Math.sqrt(1 - (i - 256) * (i - 256) / 10000);// square curve //r = 200 * Math.sin(Math.PI * (i - 128) / 256); // sine curve for (j = 0; j < 512; j++) hm.setPixel(i, j, Math.round(r*(500.0 + (hm.getPixel(i, j)-128))*0.002)); // now, r means position on the "log", and initial perlin noise is log's texture. // perlinNoise median 128, highest offset taking as 100, the result offset needs to be ~0.2 in multiplier } var v:Vector.<int> = new Vector.<int>(); var vv:Vector.<Number> = new Vector.<Number>(3); for (i = 1; i < 511; i++) { v.length = 0; v.push(hm.getPixel(0, i-1), hm.getPixel(1, i-1), hm.getPixel(2, i-1), hm.getPixel(0, i), hm.getPixel(1, i), hm.getPixel(2, i), hm.getPixel(0, i+i), hm.getPixel(1, i+1), hm.getPixel(2, i+1)); for (j = 1; j < 510; j++) { var g:int = -1 * v[0] - 2 * v[1] - 1 * v[2] + 1 * v[8] + 2 * v[7] + 1 * v[6]; // gradient by Y var r:int = -1 * v[0] - 2 * v[3] - 1 * v[6] + 1 * v[2] + 2 * v[5] + 1 * v[8]; // gradient by X //if ((i > 50) && (i < 55) && (j > 50) && (j < 55)) trace(g, r); var b:int = v[5]; r += 128; g += 128; var p:uint = r *0x10000 + g*0x100 + b; branchBitmap.setPixel(j, i, p); v.shift(); v.push(hm.getPixel(j + 2, i + 1)); v[2] = hm.getPixel(j + 2, i - 1); v[5] = hm.getPixel(j + 2, i); } } var bf:BlurFilter = new BlurFilter(2,8); // ___ // bevelFilter is not what I need, it bevels a rectangle and just that [___] // dropShadowFilter requires empty alpha I believe // convolution filter works best on self, while it can do what I need branchBitmap.applyFilter(branchBitmap, branchBitmap.rect, P0, bf); hm.copyPixels(branchBitmap, branchBitmap.rect, P0); //branchBitmap.perlinNoise(32, 256, 0, seed, true, true, 7, true); // naked grayscale // 0 octaves means 50% gray filling branchBitmap.fillRect(branchBitmap.rect, 0xff808080); // it looks like I'll have enough details just by perlin-noising the heightmap var inc:Number = Math.PI / 3; var azi:Number = -Math.PI * 1 / 4; var cx:Number = Math.cos(inc) * Math.cos(azi); var cy:Number = Math.cos(inc) * Math.sin(azi); var cz:Number = Math.sin(inc); azi = 1 - 2 * cz; inc = 1 / cz; // cos(lighting) to be normalized into (0..1) via cz for (i = 0; i < 512; i++) for (j = 0; j < 512; j++) { p = branchBitmap.getPixel(j, i); var h:uint = hm.getPixel(j, i); // give a vector here somewhere vv[0]= (h >> 16)-128; vv[1] = ((h >> 8) & 255)-128; vv[2] = 26; // balance constant, a normal is always pointing upwards, Basis.Normalize(vv); var m:Number = inc*(cx * vv[0] + cy * vv[1] + cz * vv[2]); // cos(lightangle) r = (p >> 16) & 255; g = (p >> 8) & 255; b = p & 255; r = Math.max(0,Math.min(255, r * m)); g = Math.max(0,Math.min(255, g * m)); b = Math.max(0,Math.min(255, b * m)); branchBitmap.setPixel(j, i, 0x10000 * r + 0x100 * g + b); } branchBitmap.applyFilter(branchBitmap, branchBitmap.rect, P0,bf); // should be here, without blurring it's liney hm = new BitmapData(192, 512, false); hm.copyPixels(branchBitmap, new Rectangle(160, 0, 192, 512), P0); branchBitmap = hm; } 

"Base" é uma classe auxiliar para vetores no Vector3D, mas como o código foi escrito no Flash 10.1, ainda não existia esse tipo de vetor, ou eu preferia fazer minha própria bicicleta. A textura sob o galho com folhas foi desenhada da seguinte forma: primeiro uma folha foi feita, depois foi determinado se os galhos tinham uma folha central, determinando o comprimento da parte do galho ao qual as folhas foram anexadas e, em seguida, elas foram anexadas (calculadas na textura) em um ângulo ao galho pela largura calculada da folha . A forma da folha foi definida como um círculo distorcido com vários pontos de referência deslocados do círculo por um raio de meia folha e o comprimento das estacas foi definido separadamente, tudo isso foi desenhado na textura da folha em preto e branco e salvo para o futuro. (Mais precisamente, havia duas texturas de “galho com folhas”, uma para as extremidades, ou seja, galhos para os quais nada cresce a partir do “final”, mas com folhas, uma folha foi desenhada nela no final do galho, a segunda para “meio "Sem folha final.)

Então a parte mais difícil é a aparência da árvore? Aqui eu pensei e experimentei por um longo tempo. Decidi fazer a árvore crescer de verdade - os galhos se estendem em comprimento (na verdade, crescem do final), às vezes geram galhos para o lado, os galhos se estendem para o sol (para cima) e mais algumas condições. O resultado foi um hash terrível, a melhor opção que conseguimos compartilhar era a seguinte:

imagem
(Curiosamente, o diary.ru é um excelente serviço de hospedagem de fotos, até agora nada deu errado!)

Cheguei à conclusão de que precisamos de alguma forma reduzir a densidade dos galhos. Inicialmente, a ideia era limitá-los gravitacionalmente - ou seja, galhos "pesados" demais simplesmente quebram e caem. Comecei a contar o momento da força na flexão, comparando-o com a força da árvore (arrastei os valores de algum lugar, marquei-o como constantes e comecei a testar) - resultou mal, às vezes o tronco quebrava, mesmo que não devesse ter acontecido, e a árvore estava dobrada com segurança , às vezes um galho grande quebrou no início, o resultado levou a um tronco desequilibrado e quebrou novamente, desta vez por causa de uma perda de equilíbrio vertical, e às vezes um galho com estrutura bastante normal, crescendo em espessura, inicialmente dobrado sob seu peso e quebrou, vendas se nada nele não é mais cresceu. Ele marcou porque o desafio era um prazo.

A segunda tentativa foi limitar o crescimento de novos galhos e a sobrevivência de galhos antigos / anteriores usando a iluminação. Da terceira tentativa de implementação (as duas primeiras permaneceram na forma de funções comentadas), ficou assim: eu construí uma treliça de voxel tridimensional com um lado de 0,5 metros (sim, todos os valores que existem em metros e quilogramas - eu realmente queria física real para uma floresta real), que estava preenchida primeiro zeros, depois, ao contornar a árvore, cada ramo contribuía para o preenchimento da treliça na forma de seu volume dividido por um ou dois voxels. O fato é que todos os ramos (em todo caso, quase todos) como partes separadas do quadro calculado eram menores que 0,5 m, o que nos permitiu usar uma aproximação aproximada. Além do preenchimento, cada ramo "lança uma sombra" nos voxels subjacentes na forma de preenchimento adicional dos voxels abaixo e ligeiramente ao redor do voxel com um ramo (a forma final é uma pirâmide quadrada, mas brincar com um círculo estava quebrando e, portanto, não estava iluminando). Essa treliça foi usada como limitador, se um dos galhos começar a crescer no meio da árvore - haverá menos luz lá, será mais curto e poderá não crescer mais ou morrer por falta de iluminação. Galhos mortos caíram.

Essa opção tornou possível obter árvores que eram relativamente transparentes quando vistas e relativamente compactas em termos de escopo, a primeira versão de trabalho era assim:



Nesta versão, eu ainda depurei o mecanismo de crescimento da árvore, e a árvore podia ser vista de todos os lados. A árvore foi desenhada um ramo de cada vez, o conjunto de ramos foi classificado pela distância do observador, como no bom e velho curso VMX sobre gráficos tridimensionais de 1996, selecionei cores para fins cosméticos da linha HSB para cada chamada "desenhe-me uma árvore" para que a floresta não seja monótona, o esqueleto da árvore também é girado aleatoriamente para desenhar. No total, havia de seis a oito modelos de árvores para desenho, cada um crescendo sob sua própria influência RNG, a paisagem da terra provocou outro ruído, e o local onde cultivar a árvore foi selecionado aleatoriamente usando um conjunto de faixas de pontos permitidos para o crescimento ao se mover para o lado observador a distância. Se a árvore plantada no ponto A e o raio da árvore R selecionado para "crescer", os valores (AR, A + R) ficam proibidos de crescer na distância atual, ao passar para a próxima (-0,05), esse intervalo diminui em 0,1 e foi removido quando foi reduzido a zero.

A última (e de fato a primeira e imediatamente levada em consideração) nuance de todo esse algoritmo é que é MUITO LONGO. Para percorrer a árvore "adulta", são necessários alguns segundos para desenhar, mais alguns para desenhar as texturas de uma árvore, de meio segundo a dois, e o Adobe Flash não foi projetado para intervalos de computação tão longos sem atualizar a tela (mais precisamente, sem retornar o controle ao mecanismo) . Portanto, precisávamos de um algoritmo que soubesse manter o estado entre as chamadas, continuar trabalhando do local em que foi interrompido e controlar seu tempo de execução e, ao mesmo tempo, não entre em pânico e não entre em pânico no mecanismo flash. Salvar o estado foi implementado como um par de propriedades da classe Principal, dividindo-se em estágios - selecionando as funções "cultivar uma árvore uma vez", "desenhar uma árvore concluída" e "desenhar um pedaço de terra" e medir o tempo gasto, respectivamente, assim que a próxima "uma vez" levou mais de alguns segundos para uma árvore, a árvore foi considerada "pronta" e deixada de lado. Foram três grandes fases: a criação de texturas, o "cultivo" de árvores, a colocação de árvores prontas na tela.

O resultado fica assim:



Você pode jogar aqui . Otimizado (mais precisamente, escrito) para o Flash 10.1, levando em consideração várias atualizações do flash em termos de segurança pode ser muito lento - nesse caso, aconselho que você baixe a versão de depuração do Adobe Flash Player 11.5 e abra-a offline. O desenho inteiro leva de 5 a 6 minutos, após os dois primeiros na tela algum movimento começa a ocorrer, o que pode ser interessante de observar. Após o desenho, você pode pressionar Ctrl + clique para salvar o resultado como um arquivo PNG quádruplo em comparação com o tamanho da janela.

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


All Articles