quarta-feira, 26 de outubro de 2011

Tutorial Básico 3: Terreno (parte 1)

Neste tutorial, aprenderemos a criar, renderizar e configurar o terreno de um ambiente dentro do Ogre 3D. Devido a extensão dessa parte do tutorial, dividi ele em duas partes, de forma que nessa primeira parte nós sejamos capazes de criar um terreno. O projeto feito nessa parte será necessário para a próxima parte do tutorial.

Este tutorial foi traduzido e adaptado do Tutorial Básico 3 da Wiki do Ogre 3D. Link: http://www.ogre3d.org/tikiwiki/Basic+Tutorial+3




Preparando o projeto

Abra o Visual Studio 2010, crie um novo projeto com o nome TutorialBasico3 e do tipo OGRE Application. Selecione as opções Standard application e Postbuild Copy e clique em Finish.

Quando o projeto terminar de ser criado, abra o arquivo TutorialBasico3.h e adicione o seguinte código no arquivo (não exclua o que já estiver no arquivo):

#include <Terrain/OgreTerrain.h>
#include <Terrain/OgreTerrainGroup.h>
#include "BaseApplication.h"
 
class TutorialBasico3 : public BaseApplication
{
private:
    Ogre::TerrainGlobalOptions* mTerrainGlobals;
    Ogre::TerrainGroup* mTerrainGroup;
    bool mTerrainsImported;
 
    void defineTerrain(long x, long y);
    void initBlendMaps(Ogre::Terrain* terrain);
    void configureTerrainDefaults(Ogre::Light* light);
public:
    TutorialBasico3(void);
    virtual ~TutorialBasico3(void);
 
protected:
    virtual void createScene(void);
};

Da mesma forma, abra o arquivo TutorialBasico.cpp e adicione o seguinte código (não exclua o que já estiver lá):

#include "TutorialBasico3.h"
 

TutorialBasico3::TutorialBasico3(void)
{
}

TutorialBasico3::~TutorialBasico3(void)
{
}

void getTerrainImage(bool flipX, bool flipY, Ogre::Image& img)
{
}

void TutorialBasico3::defineTerrain(long x, long y)
{
}

void TutorialBasico3::initBlendMaps(Ogre::Terrain* terrain)
{
}

void TutorialBasico3::configureTerrainDefaults(Ogre::Light* light)
{
}

void TutorialBasico3::createScene(void)
{
}

Não esqueça de manter o método createScene vazio!

Para que seja possível compilar o código, é necessário vincular o componente de Terreno do Ogre ao projeto do Visual Studio. Adicione 'OgreTerrain.lib' (release) e 'OgreTerrain.d.lib' (debug) às bibliotecas de entrada do seu projeto no Windows. Elas podem ser adicionadas na janela de Propriedades do Projeto, em suas respecitivas configurações (release e debug), na seção Vinculador (Linker), Entrada (Input) e Dependências Adicionais (additional references).


Terreno

O projeto deste tutorial se baseia em dois componentes principais: TerrainGroup e Terrain. Também há o TerrainGlobalOptions, mas ele só é usado como componente suplementar. O TerrainGroup pode ser imaginado como uma zona moderada, como um prado, clareira ou um campo. O TerrainGroup pode agregar várias peças de Terrain.

Essa subdivisão é usada para a renderização de nível de detalhe (LOD), que é baseada na distância da câmera para cada um dos elementos do tipo Terrain. O Terrain consiste em "lajotas" (tiles) com algum material estampado neles.

TerrainGroups permitem a configuração de paginação. Quando um TerrainGroup é paginado, somente a parte visível pela câmera é processada. Sem paginação, todo o TerrainGroup é processado, mesmo se não estiver visível. Por enquanto, nós usaremos um TerrainGroup simples sem paginação.

Definindo a câmera

Primeiro, modificaremos o objeto câmera para visualizar o nosso terreno. Adicione o seguinte código à função TutorialBasico3::createScene:

    mCamera->setPosition(Ogre::Vector3(1683, 50, 2116));
    mCamera->lookAt(Ogre::Vector3(1963, 50, 1660));
    mCamera->setNearClipDistance(0.1);
    mCamera->setFarClipDistance(50000);
 
    if (mRoot->getRenderSystem()->getCapabilities()->hasCapability(Ogre::RSC_INFINITE_FAR_PLANE))
    {
        mCamera->setFarClipDistance(0);   // ativa a distancia de recorte minima infinita, se for possivel
    }

O que estamos fazendo, além de definir a posição e orientação da câmera, é configurar as distâncias de recorte máxima e mínima (far e near). Como o nosso terreno vai ser grande, queremos que ela veja a uma distância máxima.

Definindo as luzes direcional e ambiente

O componente Terrain utiliza uma luz direcional para computar o mapa de luz do terreno, então vamos pôr uma luz direcional na cena (createScene):

    Ogre::Vector3 lightdir(0.55, -0.3, 0.75);
    lightdir.normalise();
 
    Ogre::Light* light = mSceneMgr->createLight("tstLight");
    light->setType(Ogre::Light::LT_DIRECTIONAL);
    light->setDirection(lightdir);
    light->setDiffuseColour(Ogre::ColourValue::White);
    light->setSpecularColour(Ogre::ColourValue(0.4, 0.4, 0.4));
 
    mSceneMgr->setAmbientLight(Ogre::ColourValue(0.2, 0.2, 0.2));

Nós também definimos uma luz ambiente para suavizar a iluminação.

Configurando nosso terreno

Primeiro, nós criaremos um conjunto de de opções de terreno global. Para esta tarefa, a classe TerrainGlobalOptions é usada. Ela somente armazena as opções padrão para todos os terrenos que nós vamos criar e fornece alguns gets e sets.

mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions();

Então construímos o nosso objeto TerrainGroup, que é uma classe auxiliar para o gerenciamento de uma grade de terrenos.

mTerrainGroup = OGRE_NEW Ogre::TerrainGroup(mSceneMgr, Ogre::Terrain::ALIGN_X_Z, 513, 12000.0f);
    mTerrainGroup->setFilenameConvention(Ogre::String("BasicTutorial3Terrain"), Ogre::String("dat"));
    mTerrainGroup->setOrigin(Ogre::Vector3::ZERO);

O construtor da classe TerrainGroup pega nossa instância do SceneManager, opções de alinhamento do terreno, tamanho do terreno e tamanho do mundo do terreno como parâmetros. Nós dizemos ao TerrainGroup que nome nós gostaríamos de usar ao salvar nosso terreno, através da função setFilenameConvention. E por último, definimos a origem do TerrainGroup.

Agora é a hora de configurar o nosso terreno.

configureTerrainDefaults(light);

Nós veremos essa função posteriormente, mas por enquanto, observe que estamos passando a nossa luz direcional como parâmetro.

E então, nós definimos nossos terrenos e instruímos ao TerrainGroup para carrega-los:

for (long x = 0; x <= 0; ++x)
        for (long y = 0; y <= 0; ++y)
            defineTerrain(x, y);
 
    // sync load since we want everything in place when we start
    mTerrainGroup->loadAllTerrains(true);

Como nós só temos um terreno, nós só estaremos chamando a função defineTerrain uma vez, mas se tivéssemos vários terrenos, teríamos que definir cada um deles.

Agora que já temos nossos terrenos importados, nós vamos calcular os blendmaps para cada terreno (veremos isso mais adiante):

if (mTerrainsImported)
    {
        Ogre::TerrainGroup::TerrainIterator ti = mTerrainGroup->getTerrainIterator();
        while(ti.hasMoreElements())
        {
            Ogre::Terrain* t = ti.getNext()->instance;
            initBlendMaps(t);
        }
    }

Tudo o que nos resta fazer agora é liberar os recursos após a criação inicial dos terrenos:

mTerrainGroup->freeTemporaryResources();

A função TutorialBasico3::createScene deve estar assim por enquanto:

void BasicTutorial3::createScene(void)
{
    mCamera->setPosition(Ogre::Vector3(1683, 50, 2116));
    mCamera->lookAt(Ogre::Vector3(1963, 50, 1660));
    mCamera->setNearClipDistance(0.1);
    mCamera->setFarClipDistance(50000);
 
    if (mRoot->getRenderSystem()->getCapabilities()->hasCapability(Ogre::RSC_INFINITE_FAR_PLANE))
    {
        mCamera->setFarClipDistance(0);   // ativa a distancia de recorte minima infinita, se for possivel
    }
 
    Ogre::MaterialManager::getSingleton().setDefaultTextureFiltering(Ogre::TFO_ANISOTROPIC);
    Ogre::MaterialManager::getSingleton().setDefaultAnisotropy(7);
 
    Ogre::Vector3 lightdir(0.55, -0.3, 0.75);
    lightdir.normalise();
 
    Ogre::Light* light = mSceneMgr->createLight("tstLight");
    light->setType(Ogre::Light::LT_DIRECTIONAL);
    light->setDirection(lightdir);
    light->setDiffuseColour(Ogre::ColourValue::White);
    light->setSpecularColour(Ogre::ColourValue(0.4, 0.4, 0.4));
 
    mSceneMgr->setAmbientLight(Ogre::ColourValue(0.2, 0.2, 0.2));
 
    mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions();
 
    mTerrainGroup = OGRE_NEW Ogre::TerrainGroup(mSceneMgr, Ogre::Terrain::ALIGN_X_Z, 513, 12000.0f);
    mTerrainGroup->setFilenameConvention(Ogre::String("BasicTutorial3Terrain"), Ogre::String("dat"));
    mTerrainGroup->setOrigin(Ogre::Vector3::ZERO);
 
    configureTerrainDefaults(light);
 
    for (long x = 0; x <= 0; ++x)
        for (long y = 0; y <= 0; ++y)
            defineTerrain(x, y);
 
    // sync load since we want everything in place when we start
    mTerrainGroup->loadAllTerrains(true);
 
    if (mTerrainsImported)
    {
        Ogre::TerrainGroup::TerrainIterator ti = mTerrainGroup->getTerrainIterator();
        while(ti.hasMoreElements())
        {
            Ogre::Terrain* t = ti.getNext()->instance;
            initBlendMaps(t);
        }
    }
 
    mTerrainGroup->freeTemporaryResources();
}

Não tente compilar agora! Não irá funcionar até que nós implementemos as funções utilitárias de terreno, que é o nosso próximo tópico.

Funções utilitárias de Terreno

configureTerrainDefaults

O componente de terreno do Ogre é bastante configurável. O mTerrainGlobals é a nossa instância do Ogre::TerrainGlobalOptions.

Vá até a função configureTerrainDefaults e adicione o seguinte código a ele:

    mTerrainGlobals->setMaxPixelError(8);
    mTerrainGlobals->setCompositeMapDistance(3000);

Primeiro nós configuramos duas funções globais: MaxPixelError e CompositeMapDistance. MaxPixelError decide o quão preciso o nosso terreno vai ser. Um número menor significa um terreno mais preciso, ao custo de desempenho (por causa do número de vértices). CompositeMapDistance decide o quão distante o terreno do Ogre vai renderizar um terreno ilumninado

Agora, usando a nossa luz direcional, vamos configurar a iluminação:

// Eh importante definir estes parametros para que o terreno saiba o que usar para dados derivados (em tempo não-real)
    mTerrainGlobals->setLightMapDirection(light->getDerivedDirection());
    mTerrainGlobals->setCompositeMapAmbient(mSceneMgr->getAmbientLight());
    mTerrainGlobals->setCompositeMapDiffuse(light->getDiffuseColour());

Ele usa a nossa luz para definir a direção e a cor difusa e define a cor difusa para coincidir com a luz ambiente do gerenciador de cena (scene manager).

Em seguida, definimos alguns valores de importação:

// Configure as configuracoes de importacao padrao para o uso de imagem importada
    Ogre::Terrain::ImportData& defaultimp = mTerrainGroup->getDefaultImportSettings();
    defaultimp.terrainSize = 513;
    defaultimp.worldSize = 12000.0f;
    defaultimp.inputScale = 600; // terrain.png eh 8 bpp
    defaultimp.minBatchSize = 33;
    defaultimp.maxBatchSize = 65;

Nós não vamos cobrir esses valores neste tutorial, mas terrainSize e worldSize são definidos para coincidir com os tamanhos globais (o que foi dito em TerrainGroup) e inputScale decide como a imagem no mapa de alturas é escalonada. Nós estamos usando escala aqui porque as imagens têm uma precisão limitada. Um mapa de alturas bruto, por instância, não precisa normalmente de escala porque os valores são armazenados como um array de floats não-escalonados.

A última parte é para nossas texturas:

// texturas
    defaultimp.layerList.resize(3);
    defaultimp.layerList[0].worldSize = 100;
    defaultimp.layerList[0].textureNames.push_back("dirt_grayrocky_diffusespecular.dds");
    defaultimp.layerList[0].textureNames.push_back("dirt_grayrocky_normalheight.dds");
    defaultimp.layerList[1].worldSize = 30;
    defaultimp.layerList[1].textureNames.push_back("grass_green-01_diffusespecular.dds");
    defaultimp.layerList[1].textureNames.push_back("grass_green-01_normalheight.dds");
    defaultimp.layerList[2].worldSize = 200;
    defaultimp.layerList[2].textureNames.push_back("growth_weirdfungus-03_diffusespecular.dds");
    defaultimp.layerList[2].textureNames.push_back("growth_weirdfungus-03_normalheight.dds");

Aqui nós definimos o número de camadas de textura de terreno para 3, chamando defaultimp.layerList.resize(3). Então nós inicializamos cada camada definindo o 'worldSize' e especificando os nomes de textura. 'worldSize' decide o quão grande cada porção de texturas vai ser. Um valor menor aumentará a resolução da camada de textura renderizada.

O gerador padrão de materiais leva duas texturar por camada:

1- diffuse_specular - textura difusa com um mapa especular no canal alpha
2- normal_height - mapa normal com um mapa de alturas no canal alpha

Para mais informações sobre como as texturas são feitas, visite este link. Os arquivos usados aqui estão incluídos no diretório de exemplos da distribuição de origem do Ogre.

Nossa função configureTerrainDefaults  ficará assim:

void TutorialBasico3::configureTerrainDefaults(Ogre::Light* light)
{
    // Configure global
    mTerrainGlobals->setMaxPixelError(8);
    // testing composite map
    mTerrainGlobals->setCompositeMapDistance(3000);
 
    // Important to set these so that the terrain knows what to use for derived (non-realtime) data
    mTerrainGlobals->setLightMapDirection(light->getDerivedDirection());
    mTerrainGlobals->setCompositeMapAmbient(mSceneMgr->getAmbientLight());
    mTerrainGlobals->setCompositeMapDiffuse(light->getDiffuseColour());
 
    // Configure default import settings for if we use imported image
    Ogre::Terrain::ImportData& defaultimp = mTerrainGroup->getDefaultImportSettings();
    defaultimp.terrainSize = 513;
    defaultimp.worldSize = 12000.0f;
    defaultimp.inputScale = 600;
    defaultimp.minBatchSize = 33;
    defaultimp.maxBatchSize = 65;
    // textures
    defaultimp.layerList.resize(3);
    defaultimp.layerList[0].worldSize = 100;
    defaultimp.layerList[0].textureNames.push_back("dirt_grayrocky_diffusespecular.dds");
    defaultimp.layerList[0].textureNames.push_back("dirt_grayrocky_normalheight.dds");
    defaultimp.layerList[1].worldSize = 30;
    defaultimp.layerList[1].textureNames.push_back("grass_green-01_diffusespecular.dds");
    defaultimp.layerList[1].textureNames.push_back("grass_green-01_normalheight.dds");
    defaultimp.layerList[2].worldSize = 200;
    defaultimp.layerList[2].textureNames.push_back("growth_weirdfungus-03_diffusespecular.dds");
    defaultimp.layerList[2].textureNames.push_back("growth_weirdfungus-03_normalheight.dds");
}

defineTerrain

Está e a nossa função defineTerrain:

void TutorialBasico3::defineTerrain(long x, long y)
{
    Ogre::String filename = mTerrainGroup->generateFilename(x, y);
    if (Ogre::ResourceGroupManager::getSingleton().resourceExists(mTerrainGroup->getResourceGroup(), filename))
    {
        mTerrainGroup->defineTerrain(x, y);
    }
    else
    {
        Ogre::Image img;
        getTerrainImage(x % 2 != 0, y % 2 != 0, img);
        mTerrainGroup->defineTerrain(x, y, &img);
        mTerrainsImported = true;
    }
}

Ela é simples, mas faz muita coisa:

Primeiro, ela consulta o nosso TerrainGroup qual o nome do arquivo ele deve usar para gerar o terreno.

Depois, ele verifica se há um arquivo com aquele nome no grupo de recursos. Se houver, significa que nós já geramos um arquivo binário de dados de terreno, então não há necessidade de importar de uma imagem. Se não houver um arquivo de dados, significa que nós temos que gerar nosso terreno, então nós carregamos a imagem e utilizamos para definir.

A função utiliza uma pequena função utilitária chamada getTerrainImage(), explicada a seguir.

getTerrainImage

Como esta função é realmente pequena e só é usada uma vez, ela fica no arquivo de implementação como uma função local estática:

void getTerrainImage(bool flipX, bool flipY, Ogre::Image& img)
{
    img.load("terrain.png", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
    if (flipX)
        img.flipAroundY();
    if (flipY)
        img.flipAroundX();
 
}

Ele carrega o arquivo 'terrain.png' dos locais de recursos e a inverte, se necessário.

Nota: a inversão (flipping) é feita para imitar um terreno "sem emendas", assim, é possível fazer um terreno grande ilimitado usando apenas um mapa de alturas 513x513. Esta é só uma dica, se o terreno de alturas do seu terreno já é "sem emendas", você não precisa fazer a inversão, só precisa definir o mapa de alturas individuais para cada Terrain. Neste tutorial nós só usamos um TerrainGroup de tamanho 1x1 (observe isto na função createScene() function) então este código, na realidade, não é usado.

initBlendMaps

Lembra-se dos nossos três tipos de camadas de terrenos, definidos em configureTerrainDefaults()? Agora, nós iremos combinar essas camadas, baseando-se na altura da lajota. Em um projeto real, você pode usar as estampas armazenadas em canais RGBA em um arquivo ou um arquivo que está separado do mapa de alturas.

Esta é toda a função initBlendMaps:

void TutorialBasico3::initBlendMaps(Ogre::Terrain* terrain)
{
    Ogre::TerrainLayerBlendMap* blendMap0 = terrain->getLayerBlendMap(1);
    Ogre::TerrainLayerBlendMap* blendMap1 = terrain->getLayerBlendMap(2);
    Ogre::Real minHeight0 = 70;
    Ogre::Real fadeDist0 = 40;
    Ogre::Real minHeight1 = 70;
    Ogre::Real fadeDist1 = 15;
    float* pBlend1 = blendMap1->getBlendPointer();
    for (Ogre::uint16 y = 0; y < terrain->getLayerBlendMapSize(); ++y)
    {
        for (Ogre::uint16 x = 0; x < terrain->getLayerBlendMapSize(); ++x)
        {
            Ogre::Real tx, ty;
 
            blendMap0->convertImageToTerrainSpace(x, y, &tx, &ty);
            Ogre::Real height = terrain->getHeightAtTerrainPosition(tx, ty);
            Ogre::Real val = (height - minHeight0) / fadeDist0;
            val = Ogre::Math::Clamp(val, (Ogre::Real)0, (Ogre::Real)1);
 
            val = (height - minHeight1) / fadeDist1;
            val = Ogre::Math::Clamp(val, (Ogre::Real)0, (Ogre::Real)1);
            *pBlend1++ = val;
        }
    }
    blendMap0->dirty();
    blendMap1->dirty();
    blendMap0->update();
    blendMap1->update();
}

Nós não vamos adentrar nos telhas de como esta função funciona neste tutorial. Apenas vamos dizer que ela usa a altura do terreno para estampar as três camadas no terreno. Observe o uso de ofgetLayerBlendMap() e getBlendPointer().

Compilando e testando!

Finalmente, poderemos compilar o nosso programa para ver como ficou!

Utilize a configuração Direct3D. Por alguma razão, o OpenGL não renderiza o terreno inteiro ao mesmo tempo (somente uma área próxima ao usuário).

O programa pode demorar um pouco até que ele permita o movimento com o mouse e o teclado. Isso porque ele está construindo e renderizando todo o terreno, e bloqueia qualquer ação do usuário. Portanto, tenha paciência na hora de compilar. No próximo tutorial, colocaremos uma função para que o nosso Aplicativo mostre uma mensagem de carregamento.

O seu programa deve ficar assim:


Dica: se você apertar 'R', você verá todos os objetos e terrenos da cena como uma malha de triângulos. O terreno ficará desse jeito:


Experimente fazer isto e explore o terreno!

Na próxima aula, adicionaremos mais algumas funções à esse nosso programa e aprenderemos a configurar céu e neblina! Lembrem-se de guardar esse projeto, pois continuaremos a partir dele no próximo post!

2 comentários:

  1. Onde consigo as Libs e a imagem usada no tutorial?

    OgreTerrain.lib
    OgreTerrain.d.lib

    img.load("terrain.png")

    ResponderExcluir
  2. Como vcs disseram realmente tem um problema com a OpenGL, agora como eu vou usar com Directx e o programinha de configuração rápida só funciona corretamente com OpenGL?

    ResponderExcluir