Acelerando a geração de textura processual

14

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:

insira a descrição da imagem aqui

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; 
}
FalconNL
fonte
Você poderia postar o que você tem atualmente em Java para sua função de ruído? Não estou dizendo que existem ganhos de desempenho, mas alguém pode descobrir algo para lhe dar um impulso.
Darren Reid
Adicionei o código que estou usando na postagem original.
FalconNL
Não está relacionado ao seu Q por si só, mas você deve chamar dispose () no seu pixmap depois de terminar.
Junkdog

Respostas:

10

Você pode combinar abordagens (2) e (3) assim:

  • Primeiro, use a GPU para gerar várias texturas de ruído e salvá-las. Este será o seu "cache de ruído"; você pode fazer isso apenas uma vez na primeira execução.
  • Para gerar uma textura no jogo, combine algumas texturas do cache - isso deve ser bem rápido. Em seguida, se necessário, adicione efeitos especiais como vórtices.
  • Como alternativa, você pode pré-gerar algumas texturas de "efeitos especiais" também, e apenas misturá-las para obter o resultado final.
deixa pra lá
fonte
+1 Acho que gerar várias texturas e empacotá-las com o jogo para combinar ou aplicar efeitos simples seria a melhor maneira de fazê-lo.
TheNickmaster21
2

Geração de textura processual é ab * * de um mofo em termos de tempo de computação. É o que é.

A melhor implementação do Simplex Noise que encontrei é a de Stefan Gustavson .

Além da melhoria do tempo real de computação (é realmente muito difícil superar o fato de que você simplesmente pede muito do seu computador ao calcular texturas procedurais de 1024x1024), uma das melhores maneiras de reduzir o tempo de espera percebido é ter seu aplicativo faz o máximo possível de threads de segundo plano.

Portanto, comece a gerar texturas no lançamento do jogo no segmento de segundo plano , enquanto o usuário ainda estiver mexendo nas opções e no menu ou assistindo ao trailer de início de nível.

A outra coisa a considerar é: basta armazenar em cache várias centenas de texturas geradas em disco e selecionar aleatoriamente uma delas no momento do carregamento. Mais disco, mas menos tempo de carregamento.

bobobobo
fonte