Recentemente, comecei a trabalhar em um jogo que ocorre em um sistema solar gerado processualmente. Após um pouco de uma curva de aprendizado (nunca trabalhei com Scala, OpenGL 2 ES ou Libgdx antes), eu tenho uma demonstração técnica básica, onde você gira em torno de um único planeta com textura processual:
O problema que estou enfrentando é o desempenho da geração de textura. Uma rápida visão geral do que estou fazendo: um planeta é um cubo que foi deformado em uma esfera. Para cada lado, a textura ansn (por exemplo, 256 x 256) é aplicada, agrupada em uma textura 8n xn que é enviada ao shader do fragmento. Os dois últimos espaços não são usados, eles estão lá apenas para garantir que a largura seja uma potência de 2. A textura é atualmente gerada na CPU, usando a versão atualizada de 2012 do algoritmo de ruído simplex vinculado no artigo 'Simplex ruído desmistificado ». A cena que estou usando para testar o algoritmo contém duas esferas: o planeta e o fundo. Ambos usam uma textura em escala de cinza que consiste em seis oitavas de ruído simplex 3D; portanto, se escolhermos 128x128 como o tamanho da textura, haverá 128 x 128 x 6 x 2 x 6 = cerca de 1,2 milhão de chamadas para a função de ruído.
O mais próximo que você chegará do planeta é do que é mostrado na imagem e como a resolução alvo do jogo é 1280x720, isso significa que eu prefiro usar texturas de 512x512. Combine isso com o fato de que as texturas reais certamente serão mais complicadas do que o ruído básico (haverá uma textura diurna e noturna misturada no shader de fragmentos com base na luz solar e uma máscara especular. Preciso de ruído para continentes, variação de cores do terreno , nuvens, luzes da cidade etc.) e estamos vendo algo como 512 x 512 x 6 x 3 x 15 = 70 milhões de ruídos que exigem apenas o planeta. No jogo final, haverá atividades ao viajar entre planetas; portanto, uma espera de 5 ou 10 segundos, possivelmente 20, seria aceitável, pois eu posso calcular a textura em segundo plano enquanto viajo, embora, obviamente, quanto mais rápido, melhor.
Voltando à cena do teste, o desempenho no meu PC não é muito terrível, embora ainda seja muito lento, considerando que o resultado final será cerca de 60 vezes pior:
128x128 : 0.1s
256x256 : 0.4s
512x512 : 1.7s
Isso ocorreu depois que eu mudei todo o código crítico de desempenho para Java, pois tentar fazê-lo no Scala era muito pior. Executar isso no meu telefone (um Samsung Galaxy S3), no entanto, produz um resultado mais problemático:
128x128 : 2s
256x256 : 7s
512x512 : 29s
Já é muito longo, e isso nem leva em consideração o fato de que serão minutos em vez de segundos na versão final. Claramente, algo precisa ser feito. Pessoalmente, vejo alguns caminhos possíveis, embora ainda não esteja particularmente interessado em nenhum deles:
- Não pré-calcule as texturas, mas permita que o shader do fragmento calcule tudo. Provavelmente não é viável, porque em um ponto eu tinha o fundo como um quad de tela cheia com um pixel shader e obtive cerca de 1 qps no meu telefone.
- Use a GPU para renderizar a textura uma vez, armazene-a e use a textura armazenada a partir de então. De cabeça para baixo: pode ser mais rápido do que fazê-lo na CPU, pois a GPU deve ser mais rápida nos cálculos de ponto flutuante. Desvantagem: efeitos que não podem (facilmente) ser expressos como funções de ruído simplex (por exemplo, vórtices de planetas a gás, crateras da lua etc.) são muito mais difíceis de codificar no GLSL do que no Scala / Java.
- Calcule uma grande quantidade de texturas de ruído e envie-as com o aplicativo. Eu gostaria de evitar isso, se possível.
- Abaixe a resolução. Compra-me um ganho de desempenho de 4x, o que não é suficiente, e eu perco muita qualidade.
- Encontre um algoritmo de ruído mais rápido. Se alguém tiver um, sou todo ouvidos, mas o simplex já deve ser mais rápido que o perlin.
- Adote um estilo de pixel art, permitindo texturas com menor resolução e menos oitavas de ruído. Embora eu originalmente visse o jogo nesse estilo, passei a preferir a abordagem realista.
- Estou fazendo algo errado e o desempenho já deve ser uma ou duas ordens de magnitude melhor. Se for esse o caso, entre em contato.
Se alguém tiver sugestões, dicas, soluções alternativas ou outros comentários sobre esse problema, eu adoraria ouvi-los.
Em resposta ao Layoric, aqui está o código que estou usando:
//The function that generates the simplex noise texture
public static Texture simplex(int size) {
byte[] data = new byte[size * size * columns * 4];
int offset = 0;
for (int y = 0; y < size; y++) {
for (int s = 0; s < columns; s++) {
for (int x = 0; x < size; x++) {
//Scale x and y to [-1,1] range
double tx = ((double)x / (size - 1)) * 2 - 1;
double ty = 1 - ((double)y / (size - 1)) * 2;
//Determine point on cube in worldspace
double cx = 0, cy = 0, cz = 0;
if (s == 0) { cx = 1; cy = tx; cz = ty; }
else if (s == 1) { cx = -tx; cy = 1; cz = ty; }
else if (s == 2) { cx = - 1; cy = -tx; cz = ty; }
else if (s == 3) { cx = tx; cy = - 1; cz = ty; }
else if (s == 4) { cx = -ty; cy = tx; cz = 1; }
else if (s == 5) { cx = ty; cy = tx; cz = - 1; }
//Determine point on sphere in worldspace
double sx = cx * Math.sqrt(1 - cy*cy/2 - cz*cz/2 + cy*cy*cz*cz/3);
double sy = cy * Math.sqrt(1 - cz*cz/2 - cx*cx/2 + cz*cz*cx*cx/3);
double sz = cz * Math.sqrt(1 - cx*cx/2 - cy*cy/2 + cx*cx*cy*cy/3);
//Generate 6 octaves of noise
float gray = (float)(SimplexNoise.fbm(6, sx, sy, sz, 8) / 2 + 0.5);
//Set components of the current pixel
data[offset ] = (byte)(gray * 255);
data[offset + 1] = (byte)(gray * 255);
data[offset + 2] = (byte)(gray * 255);
data[offset + 3] = (byte)(255);
//Move to the next pixel
offset += 4;
}
}
}
Pixmap pixmap = new Pixmap(columns * size, size, Pixmap.Format.RGBA8888);
pixmap.getPixels().put(data).position(0);
Texture texture = new Texture(pixmap, true);
texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
return texture;
}
//SimplexNoise.fbm
//The noise function is the same one found in http://webstaff.itn.liu.se/~stegu/simplexnoise/SimplexNoise.java
//the only modification being that I replaced the 32 in the last line with 16 in order to end up with
//noise in the range [-0.5, 0.5] instead of [-1,1]
public static double fbm(int octaves, double x, double y, double z, double frequency) {
double value = 0;
double f = frequency;
double amp = 1;
for (int i = 0; i < octaves; i++) {
value += noise(x*f, y*f, z*f) * amp;
f *= 2;
amp /= 2;
}
return value;
}