Tutorial 10 - Hello Terrain

jMonkeyEngine 3 Tutorial (10) - Hello Terrain

Anterior: Hello Collision, Próximo: Hello Audio

Uma maneira de criar terreno 3D é esculpir um modelo de terreno enorme. Isto dá a você muita liberdade artística - mas o rendering de ral modelo enorme pode ser muito lento. Este tutorial explica como criar terrenos com rápido rendering de mapas de altura, e como usar splatting de textura para fazer o terreno parecer bom.

Note: Se você obter um erri enquanto tentando criar seu objeto ImageBasedHeightMap, você pode precisar atualizar o SDK, clique em "Ajuda" ("Help") / "Checar por atualizações" ("Check for updates")

Para usar os ativos de exemplo em um novo projeto SDK da jMonkeyEngine, dê um clique no botão direito do seu projeto, selecione "Propriedades" ("Properties"), vá para "Bibliotecas" ("Libraries"), pressione "Adicionar Biblioteca" ("Add Library") e adicione a biblioteca "jme3-test-data".

Código de Amostra

package jme3test.helloworld;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.renderer.Camera;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.heightmap.AbstractHeightMap;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator;
import com.jme3.terrain.heightmap.HillHeightMap; // for exercise 2
import com.jme3.terrain.heightmap.ImageBasedHeightMap;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;
import java.util.ArrayList;
import java.util.List;

/** Sample 10 - How to create fast-rendering terrains from heightmaps,
and how to use texture splatting to make the terrain look good.  */
public class HelloTerrain extends SimpleApplication {

  private TerrainQuad terrain;
  Material mat_terrain;

  public static void main(String[] args) {
    HelloTerrain app = new HelloTerrain();
    app.start();
  }

  @Override
  public void simpleInitApp() {
    flyCam.setMoveSpeed(50);

    /** 1. Create terrain material and load four textures into it. */
    mat_terrain = new Material(assetManager, 
            "Common/MatDefs/Terrain/Terrain.j3md");

    /** 1.1) Add ALPHA map (for red-blue-green coded splat textures) */
    mat_terrain.setTexture("Alpha", assetManager.loadTexture(
            "Textures/Terrain/splat/alphamap.png"));

    /** 1.2) Add GRASS texture into the red layer (Tex1). */
    Texture grass = assetManager.loadTexture(
            "Textures/Terrain/splat/grass.jpg");
    grass.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex1", grass);
    mat_terrain.setFloat("Tex1Scale", 64f);

    /** 1.3) Add DIRT texture into the green layer (Tex2) */
    Texture dirt = assetManager.loadTexture(
            "Textures/Terrain/splat/dirt.jpg");
    dirt.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex2", dirt);
    mat_terrain.setFloat("Tex2Scale", 32f);

    /** 1.4) Add ROAD texture into the blue layer (Tex3) */
    Texture rock = assetManager.loadTexture(
            "Textures/Terrain/splat/road.jpg");
    rock.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex3", rock);
    mat_terrain.setFloat("Tex3Scale", 128f);

    /** 2. Create the height map */
    AbstractHeightMap heightmap = null;
    Texture heightMapImage = assetManager.loadTexture(
            "Textures/Terrain/splat/mountains512.png");
    heightmap = new ImageBasedHeightMap(heightMapImage.getImage());
    heightmap.load();

    /** 3. We have prepared material and heightmap. 
     * Now we create the actual terrain:
     * 3.1) Create a TerrainQuad and name it "my terrain".
     * 3.2) A good value for terrain tiles is 64x64 -- so we supply 64+1=65.
     * 3.3) We prepared a heightmap of size 512x512 -- so we supply 512+1=513.
     * 3.4) As LOD step scale we supply Vector3f(1,1,1).
     * 3.5) We supply the prepared heightmap itself.
     */
    int patchSize = 65;
    terrain = new TerrainQuad("my terrain", patchSize, 513, heightmap.getHeightMap());

    /** 4. We give the terrain its material, position & scale it, and attach it. */
    terrain.setMaterial(mat_terrain);
    terrain.setLocalTranslation(0, -100, 0);
    terrain.setLocalScale(2f, 1f, 2f);
    rootNode.attachChild(terrain);

    /** 5. The LOD (level of detail) depends on were the camera is: */
    TerrainLodControl control = new TerrainLodControl(terrain, getCamera());
    terrain.addControl(control);
  }
}

Quando você executar esta amostra você deveria ver um terreno com montanhas sujas, mais algumas estradas onludando entre.

O que é um Mapa de Altura (Heightmap)?

Mapas de Altura (Heightmaps) são uma maneira eficiente de representar a forma de um terreno montanhoso. Nem todo pixel do terreno é armazenado,ao invés disso, uma grade de valores de amostra é usada para dobrar a altura do terreno em certos pontos. As alturas entre as amostras é interpolada.

Em Java, um mapa de altura é um array de ponto flutuante contendo valores de altura entre 0f e 255f. Aqui está um exemplo muito simples de terreno gerado de um mapa de altura com 5x5=25 valores de altura.

Coisas importantes para se notar

  • Valores baixo (e.g. 0 ou 50) são vales.
  • Valores alto (e.g. 200, 255) são montanhas.
  • O mapa de altura só especifica alguns pontos e o motor interpola o resto. Interpolação é mais efciiente do que criar um modelo com vários milhões de vértices.

Quando carregando em Java tipos de dado para armazenar um array de floats entre 0 e 255, a classe Imagem vem a mente. Armazenar os valores de altura de um terreno como imagem em escala de cinza têm uma grande vantagem: O resultado é muito amigável para o usuário, como um mapa topográfico:

  • Valores baixo (e.g. 0 ou 50) são cinza escuro – estes são vales.
  • Valores alto (e.g. 200, 255) são cinza claro – estes são montanhas.

Olhe no próximo instantâneo: Na esquerda-topo você vê uma imagem em escala de cinza 128x128 (mapa de altura) que foi usada como uma base para gerar o terreno ilustrado. Para fazer a forma montanhosa melhor visível, os topos da montanha são coloridos em branco, vales marrom, e as áreas intermediárias verde:

}

Em um jogo real, você irá querer usar terrenos mais suaves e mais complexos que os mapas de altura simples mostrados aqui. Mapas de altura tipicamente têm tamanhos quadrados de 512x512 ou 1024x1024, e contém centenas de milhares para 1 milhão de valores de altura. Não importa qual o tamanho, o conceito é o mesmo que o descrito aqui.

Olhando no Código do Mapa de Altura

O primeiro passo para a criação de terreno é o mapa de altura. Você pode criar um por si mesmo em qualquer aplicação gráfica padrão. Tenha certeza que ele tem as seguintes propriedades:

  • O tamanho deve ser quadrado e potência de dois.
    • Exemplos: 128x128, 256x256, 512x512, 1024x1024
  • Modo de cor deve ser 255 escalas de cinza.
    • Não forneça uma imagem colorida, ela será interpretada como escala de cinzam cin resultados possivelmente estranhos.
  • Salve o mapa como um arquivo de imagem .jpg ou .png

O aqruivo mountains512.png que você vê aqui é um exemplo típico de uma imagem de um mapa de altura.

Aqui está como você cria o objeto de mapa de altura em seu código jME:

  • Crie um objeto Textura (Texture).
  • Carregue sua imagem de mapa de altura preparada dentro do objeto textura.
  • Crie um objeto Mapa de Altura Abstrato (AbstractHeightmap) de um ImageBasedHeightMap.
  • Ele requer uma imagem de uma Textura JME.
  • Carregue o mapa de altura.
AbstractHeightMap heightmap = null;
    Texture heightMapImage = assetManager.loadTexture(
            "Textures/Terrain/splat/mountains512.png");
    heightmap = new ImageBasedHeightMap(heightMapImage.getImage());
    heightmap.load();

O que é Splatting de Textura?

Anteriormente você aprendeu a como criar um material para uma forma simples como a de um cubo. Todos os lados do cubo têm a mesma cor. Você pode aplicar o mesmo material para um terreno, mas então você tem uma grande planície, um grande deserto de rocha, etc. Isto não é sempre o que você quer.

Splatting de textura permite a você criar um material customizado e "pintar" texturas nele como com um "pincel de pintura". Isto é muito útil para terrenos. Como você vê no exemplo aqui, você pode pintar uma textura de grama dentro dos vales, uma textura de sujeira nas montanhas, e estradas livre de forma de forma intermediária.

O SDK da jMonkeyEngine vem com um plugin Editor de Terreno (TerrainEditor). Usando o plugin TerrainEditor plugin, você pode esculpir o terreno com o mouse e salvar o resultado como um mapa de altura. Você pode pintar texturas no terreno e o plugin salva as texturas splat resultantes como mapas de alfa (alphamap(s)). Os próximos parágrafos descrevem o processo manual para você. Você pode escolher criar o terreno a mão, ou usar o plugin TerrainEditor.

Texturas de Splat são baseadas na definição de material Terrain.j3md. Se você abrir o arquivo Terrain.j3md e olhar na seção Parâmetros do Material (Material Parameters), você verá que você tem várias camadas de textura para pintar: Tex1, Tex2, Tex3, etc.

Antes de você iniciar a pintarm você tem de fazer algumas decisões:

Escolha três texturas. Por exemplo grass.jpg, dirt.jpg, e road.jpg. jmonkeyengine.googlecode.com_svn_trunk_engine_test-data_textures_terrain_splat_road.jpg jmonkeyengine.googlecode.com_svn_trunk_engine_test-data_textures_terrain_splat_dirt.jpg jmonkeyengine.googlecode.com_svn_trunk_engine_test-data_textures_terrain_splat_grass.jpg
Você "pinta" três camadas de textura por usar três cores: vermelho, verde e azul. Você arbitrariamente decide que…

  • Vermelho é grama - vermelho é a camada Tex1, então ponha a textura de grama em Tex1.
  • Verde é sujeira - verde é a camada Tex2, então ponha a textura de sujeira em Tex2.
  • Azul são as estradas - azul é a camada Tex3, então ponha a textura de estradas em Tex3.

Agora você inicia a pintar a textura:

  • Faça uma cópia do mapa de altura dos seus terrenos, mountains512.png. Você quer ela como uma referência para a forma do terreo.
  • Nomeie a cópia alphamap.png.
  • Abra alphamap.png em um editor gráfico e mude o modo de imagem para imagem colorida.
    • Pinte o vales negros de vermelho - isto será a grama.
    • Pinte as encostas brancas de verde - isto será a sujeira das montanhas.
    • Pinte linhas azuis onde você quer estradas para para cruzar (criss-cross) o terreno.
  • O resultado final deveria parecer similar a isto:

Olhando no Código de Texturização

Como normal, você cria um objeto Material. Baseie ele na Definição de Material Terrain.j3md que é inclusa na estrutura jME3.

Material mat_terrain = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");

Carregue quatro texturas dentro deste material. A primeira, Alfa, é o mapa de alfa que você acabou de criar.

mat_terrain.setTexture("Alpha",
    assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));

As três outras texturas são as camadas que você tem previamente decidido pintar: grama, sujeira e estrada. Você cria objetos de textura e carrega as três texturas como normal. Note como você atribui eles para as respectivas camadas de textura (Tex1, Tex2, e Tex3) dentro do Material!

    /** 1.2) Add GRASS texture into the red layer (Tex1). */
    Texture grass = assetManager.loadTexture(
            "Textures/Terrain/splat/grass.jpg");
    grass.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex1", grass);
    mat_terrain.setFloat("Tex1Scale", 64f);

    /** 1.3) Add DIRT texture into the green layer (Tex2) */
    Texture dirt = assetManager.loadTexture(
            "Textures/Terrain/splat/dirt.jpg");
    dirt.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex2", dirt);
    mat_terrain.setFloat("Tex2Scale", 32f);

    /** 1.4) Add ROAD texture into the blue layer (Tex3) */
    Texture rock = assetManager.loadTexture(
            "Textures/Terrain/splat/road.jpg");
    rock.setWrap(WrapMode.Repeat);
    mat_terrain.setTexture("Tex3", rock);
    mat_terrain.setFloat("Tex3Scale", 128f);

As escalas de textura individual (e.g. mat_terrain.setFloat("Tex3Scale", 128f);) dependem do tamanho das texturas que você usa.
  • Você pode dizer que você escolheu uma escala pequena demais se, por exemplo, os tiles da sua estrada parecem como grãos de areia.
  • Você pode dizer que você escolheu uma escala grande demais se, por exemplo, as laminas da grama parecem gravetos.

Use setWrap(WrapMode.Repeat) para fazer a pequena tetxura preencher a área comprida. Se a repetição é muito visível, tente ajustar o respectivo valor Tex*Scale.

O que é um Terreno?

Internamente, a malha de terreno gerada é quebrada em tiles e blocos. Isto é uma otimização para fazer culling mais fácil. Você não precisa se preocupar sobre "tiles e blocos" tanto, apenas use os valores recomendados por enquanto - 64 é um bom início.

Vamos assumir que você queira gerar um terreno 512x512. Você já tem criado o objeto mapa de altura. Aqui estão os passos que você realiza a cada vez que você cria um novo terreno.

Crie um TerrainQuad com os seguintes argumentos:

  • Especificque um nome: E.g. my terrain.
  • Especifique o tamanho do tile: Você quer tiles de terreno do tamanho 64x64, então você fornece 64+1 = 65.
    • Em geral 64 é um bom valor de início para tiles de terreno.
  • Especifique o tamanho do bloco: Desde que você preparou um mapa de altura de tamanho 512x512, você fornece 512+1 = 513.
    • Se você fornecer um tamanho de bloco de 2x o tamanho do mapa de altura (1024+1=1025), você consegue um terreno mais esticado, comprido e plano.
    • Se você fornecer um tamanho de bloco de 1/2 o tamanho do mapa de altura (256+1=257), você consegue um terreno menor, mais detalhado.
  • Forneça o objeto de mapa de altura 512x512 que você criou.

Olhando no Código de Terreno

Aqui está o código:

terrain = new TerrainQuad(
  "my terrain",               // name
  65,                         // tile size
  513,                        // block size
  heightmap.getHeightMap());  // heightmap

Você criou o ibjeto de terreno.

  • lembre-se de aplicar o material criado:
    terrain.setMaterial(mat_terrain);
  • Lembre-se de anexar o terreno para o nó raiz (rootNode).
    rootNode.attachChild(terrain);

Se necessário, escalone e translade o objeto terreno, apenas como quaqluer outro Espacial (Spatial).

DEica: Terrain.j3md é uma definição de material não tonalizada, então você não precisa de uma fonte de luz. Você também pode usar TerrainLighting.j3md mais uma luz, se você quer um terreno tonalizado.

O que é LOD (Level of Detail - Nível de Detalhe)?

JME3 inclui uma otimização que ajusta o nível de detalhe (LOD) do terreno renderizado dependendo de quão próximo ou longe a câmera está.

    TerrainLodControl control = new TerrainLodControl(terrain, getCamera());
    terrain.addControl(control);

Partes próximas do terreno são renderizados em detalhe integral. Partes do terreno que estão distantes não estão claramente visíveis de qualquer forma, e JME3 melhora o desempenho por renderizar elas de maneira menos detalhada. Desta maneira você pode carregar enormes terrenos sem penalidade causada por detalhes invisíveis.

Exercícios

Exercício 1: Camadas de Textura

O que acontece quando você troca duas camadas, por exemplo Tex1 e Tex2?

...
mat_terrain.setTexture("Tex2", grass);
...
mat_terrain.setTexture("Tex1", dirt);

Você vê que é mais fácil trocar duas camadas no código, do que trocar as cores no mapa de alfa (alphamap).

Exercício 2: Terrenos Aleatorizados

As seguintes duas linhas geram o objeto mapa de altura baseado na sua imagem definida pelo usuário:

Texture heightMapImage = assetManager.loadTexture(
        "Textures/Terrain/splat/mountains512.png");
    heightmap = new ImageBasedHeightMap(heightMapImage.getImage());

Ao invés disso pode também pode deixar JME3 gerar um terreno aleatório para você:

  • Qual resultado você obtém quando você substitui as duas linhas do mapa de altura acima pelas linhas seguintes e executa a amostra?
    HillHeightMap heightmap = null;
    HillHeightMap.NORMALIZE_RANGE = 100; // optional
    try {
        heightmap = new HillHeightMap(513, 1000, 50, 100, (byte) 3); // byte 3 is a random seed
    } catch (Exception ex) {
        ex.printStackTrace();
    }

Mude um parâmetro por vez, e então execute a amostra de novo. Note as diferenças. Você pode descobrir qual dos valores tem efeito no terreno gerado (olhe no javadoc também)?

  • Qual valor controla o tamanho?
    • O que acontece se o tamanho nãio é um número quadrado +1?
  • Qual valor controla o número de montes gerados?
  • Quais valores controlam o tamanho e a aclividade dos montes?
    • O que acontece se o mínimo é maior que ou igual ao máximo?
    • O que acontece se tanto o mínimo quanto o máximo são valores pequenos (e.g. 10/20)?
    • O que acontece se tanto o mínimo quanto o máximo são valores grandes (e.g. 1000/1500)?
    • O que acontece se o mínimo e o máximo são muito próximos (e.g. 1000/1001, 20/21)? Muito distantes (e.g. 10/1000)?

Você vê a variedade de terrenos montanhosos que podem ser gerados usando este método.

Para este exercício você pode continuar usando o Material de splat do código de amostra acima. Apenas não esteja surpreso que o Material não combina com a forma do terreno recentemente aleatorizado. Se você quiser gerar texturas de splat de casamento real para mapas de altura randomizados, você precisa escrever um método customizado que, por exemplo, cria um mapa de alfa (alphamap) do mapa de altura por substituir certos valores de escala de cinza por certos valores RGB.

Exercício 3: Terrenos Sólidos

Você pode combinar o que você aprendeu aqui e em Hello Collision, e fazer o terreno sólido?

Conclusão

Você aprendeu a como criar terrenos que são mais eficientes que carregar um modelo gigante. Você sabe como gerar mapas de altura aleatórios ou feitos a mão. Você também pode adicionar um controle de LOD para renderizar grandes terrenos mais rápido. Você está consciente que você pode combinar o que você aprendeu sobre detecção de colisão para fazer o terreno sólido para um jogador físico. Você é também capaz de texturizar um terreno "like a boss" usando Materiais em camada e splatting de textura. Você está consciente que o SDK da jMonkeyEngine fornece um Editor de Terreno (TerrainEditor) que ajuda com a maioria destas tarefas manuais.

Você quer ouvir os jogadores dizerem "Ai!" quando eles batem em uma parede ou caem de um monte? Continue aprendendo como adicionar som para seu jogp.

Veja Também:

  • Colisão de Terreno
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-Share Alike 2.5 License.