Ponto V!

Home Matemática e Física Utilizando Motores de Física
Bruno Crivelari Sanches
Utilizando Motores de FísicaImprimir
Escrito por Bruno Crivelari Sanches
Índice do Artigo
Utilizando Motores de Física
Criando um Programa Exemplo
Integrando Bullet com Ogre3d
Configurando o Projeto e Download
Todas Páginas

No artigo anterior tivemos uma visão geral de como funciona a física nos jogos modernos e vimos alguns conceitos sobre o funcionamento e utilização de motores de física. Neste artigo iremos ver como integrar um desses motores e integrá-lo a um jogo 3d utilizando a Bullet Physics e o Ogre 3d.

Bullet Physics

Uma imagem da aplicação que iremos construirA biblioteca Bullet é uma biblioteca para detecção de colisões e simulação da dinâmica de corpos rígidos e corpos macios. Ela possui código aberto e pode ser usada em projetos comerciais ou não, contanto que o projeto se encaixe nos termos da licença ZLib, que inclusive torna possível o uso da biblioteca em consoles e outros dispositivos que não permitam ligação dinâmica.

Das bibliotecas físicas que conheço a Bullet é uma das mais completas e esta sempre em constante evolução. Escolhi ela devido a minha experiência com ela e por achar uma biblioteca fácil de se usar, apesar de a documentação a principio não ser muito amigável, ela possui uma interface intuitiva e consistente, o que simplifica o trabalho do programador.

A Bullet suporta as funcionalidades que geralmente qualquer biblioteca desse nível suporta e outras que não são tão comuns assim, sendo algumas funcionalidades:

  • Detecção de colisão discreta e continua, incluindo raios e formas convexas
  • Formas de colisão que incluem malhas côncavas e convexas, além de primitivas básicas
  • Simulação de corpos rígidos, dinâmica de veículos, controlador de personagens, juntas variadas e ragdolls
  • Corpos macios para simulação de tecidos, cordas e volumes que sofrem deformação, podendo interagir com corpos rígidos
  • Integração com o Blender, suporte a COLLADA

Instalando a Bullet

O primeiro passo para se utilizar a Bullet é fazer o download do pacote com o código fonte, para este artigo foi utilizada a versão 2.78, que pode ser encontrada na seção de downloads do projeto no Google Code clicando-se aqui. O arquivo utilizado foi o bullet-2.78.zip. Não espero mudanças nas versões futuras que venham a mudar a forma de instalação, caso isso ocorra vou atualizar o artigo com os novos passos.

Feito o download basta extrair os arquivos para o diretório de sua preferência, para o resto do artigo vou assumir que a biblioteca foi instalada no diretório “C:\develop\libs\bullet-2.78”, caso você use outro caminho, apenas modifique os passos a seguir conforme a necessidade.

A instalação não difere muito da instalação de qualquer outra biblioteca C/C++, a maior diferença é que é preciso compilar a Bullet antes do uso, pois ela não vem pré-compilada. Para este artigo foi utilizado o Visual Studio 2008 Express, que você pode aprender como instalar e utilizar com os artigos da seção sobre o Visual do PontoV, bastando clicar aqui. Caso utilize outro ambiente ou outra versão do Visual basta adaptar os passos conforme a necessidade.

Compilando a Bullet

Após extrair os arquivos você vai encontrar dentro da pasta de instalação a pasta “msvc”, que contém os projetos para serem utilizados com o Visual Studio. No meu caso acessei a pasta 2008 (que fica dentro de msvc) para abrir a solução “BULLET_PHYSICS.sln”.

Ao carregar a solução você vai se deparar com uma cena semelhante a da figura abaixo:

Visual após carregar o projeto da Bullet

Os projetos com o nome de “App*” são demonstrações de como utilizar a biblioteca e a principio precisamos de apenas os projetos que constroem a biblioteca, os projetos chamados “Bullet*” (e a LinearMath), selecione então estes projetos (BulletCollision, BulletDynamics, BulletFileLoader, BulletMultiTheaded, BulletSoftBody, BulletWorldImporter e LinearMath), clique no menu Build –> Build Selection, conforme mostra a figura abaixo:

Construindo a biblioteca Bullet

Assim que a compilação for concluída, modifique a configuração do projeto para “Release” (conforme mostrado na figura abaixo) e execute novamente a compilação, para assim construir a versão “Release” da biblioteca.

Untitled

Concluída compilação, observe que foram criadas duas pastas dentro da pasta 2008 da biblioteca, uma para a versão Debug e outra para versão Release:

Untitled

Agora que a biblioteca já foi construída, basta configurar o Visual Studio.

Configurando o Visual Studio

Para configurar o compilador, prefiro criar uma variável de ambiente que contenha o caminho para a biblioteca, dessa forma fica simples atualizar versões da biblioteca apenas modificando-se o valor da variável, este é o mesmo processo utilizado para se configurar o Ogre 3D neste artigo, onde você pode conferir detalhes de como criar variáveis de ambientes.

Uma variável de ambiente para a Bullet

Criada a variável basta abrir o Visual novamente, se você não fechou o Visual durante a criação da variável é preciso re-iniciar ele para que ele possa “enxergar” a nova variável de ambiente, caso não esteja utilizando uma variável, não é preciso fechá-lo.

Com o Visual aberto, clique em “Tools –> Options” e na janela que se abriu selecione “Projects and Solutions –> VC++ Directories”, na opção “Show Directories for:” selecione “Include Files”, de um duplo clique numa linha em branco e entre com o valor: $(BULLET_HOME)\src. O valor “$(BULLET_HOME)” indica a variável de ambiente recém criada, “\src” é a pasta dentro da pasta da Bullet onde podemos encontrar os arquivos de cabeçalho. Caso não tenha usado a variável de ambiente, entre apenas com o caminho de instalação da Bullet junto com a pasta src, como, por exemplo: C:\bullet-2.78\src.

Untitled

Por enquanto iremos configurar apenas os arquivos de cabeçalho, pois os arquivos de biblioteca iremos configurar diretamente no projeto. Faremos isso devido a um pequeno inconveniente da Bullet: os arquivos de “Debug” e “Release” possuem exatamente o mesmo nome. Como é preferível utilizar a versão correta de cada compilação de acordo com a compilação do nosso projeto, teremos que configurar quais arquivos utilizar diretamente no nosso projeto, então vamos partir para a construção do programa exemplo!


Criando um Programa Exemplo

Para o primeiro projeto com Bullet optei por utilizar o Ogre 3d por motivos como:

  • Manter o foco do artigo na utilização e funcionalidades da Bullet e não em detalhes como configurar modo de vídeo, carregar texturas, gerar malhas, etc.
  • Ogre 3D é um motor simples de usar para construir pequenos exemplos e possuímos uma boa cobertura no PontoV, que pode ser conferida aqui.
  • Independente do modo usado para exibir os itens na tela, a lógica da física é sempre a mesma

Esclarecido os motivos vamos começar o código, que vai ser baseado no código do artigo “Primeiro programa com Ogre 3d”, a diferença maior é que o novo código não vai carregar modelos externos como é feito nesse tutorial, pois a principio vamos trabalhar apenas com formas simples, no nosso caso, apenas cubos e paralelepípedos.

A primeira adição ao código original é o “include” do arquivo de cabeçalho da Bullet:

#include <btBulletDynamicsCommon.h>

Nada demais, vamos então começar a pensar em como gerenciar a física, para manter o projeto mais organizado decidi por criar uma classe chamada de PhysicsManager, que vai gerenciar os objetos da Bullet e cuidar de criar alguns como se fosse uma factory. O primeiro trabalho dessa nova classe é criar os objetos da Bullet que cuida da simulação, isso é mostrado no código a seguir:

class PhysicsManager
{
    private:
        btDiscreteDynamicsWorld *mWorld;
        btCollisionDispatcher *mCollisionDispatcher;

        btDefaultCollisionConfiguration mCollisionConfig;        
        btDbvtBroadphase mBroadphase;
        btSequentialImpulseConstraintSolver mConstraintSolver;

    public:
        PhysicsManager()
        {            
            mCollisionDispatcher = new btCollisionDispatcher(&mCollisionConfig);
    
            mWorld = new btDiscreteDynamicsWorld(mCollisionDispatcher, &mBroadphase, &mConstraintSolver, &mCollisionConfig);
        }

        ~PhysicsManager()
        {            
            delete mWorld;    
            delete mCollisionDispatcher;
        }
};

Nessa versão inicial da classe temos apenas alguns atributos, alocamos alguns dinamicamente para garantirmos a ordem de inicialização deles (poderíamos até nos aventurar com a ordem de inicialização de variáveis membro, mas isso sempre da um bocado de dores de cabeça).

Os atributos que precisamos para fazer a Bullet começar a funcionar são declarados como membros da classe e são listados a seguir:

  • mWorld: ponteiro para uma instância de btDiscreteDynamicsWorld, que ao pé da letra representa um mundo discreto dinâmico. Isso quer dizer que um objeto dessa classe vai simular um mundo físico com objetos em movimento. O discreto aqui quer dizer que a detecção de colisão dos objetos pode sofrer o efeito de tunelamento. Existe um outro tipo de instância dessa classe na Bullet que usa colisão continua que evita esse problema, mas não iremos abordar essa nesse artigo. Outra variação dessa classe é para uso com corpos macios, que também não iremos abordar nesse artigo.
  • mCollisionDispatcher: ponteiro para uma instância de btCollisionDispatcher, uma classe que a grosso modo cuida de dizer como deve ser tratado cada colisão, por exemplo, quando um objeto côncavo colidir com um objeto convexo, essa classe é quem diz qual o algoritmo que deve ser usado para manipular a colisão. Essa classe também cuida de gerenciar os objetos que estão colidindo, callbacks de colisão, etc.
  • mCollisionConfig: uma classe usada para configurar o btCollisionDispatcher, é do tipo btDefaultCollisionConfiguration e cuida de fornecer ao btCollisionDispatcher informações e serviços como tamanho de certos buffer, pools para alocação de objetos usados durante as colisões, etc.
  • mBroadphase: um objeto do tipo btDbvtBroadphase que é quem cuida da primeira fase da detecção de colisão, o trabalho desse objeto pode ser resumido a identificar objetos que estejam colidindo ou prestes a colidir, esses objetos vão ser passados ao btCollisionDispatcher que vai então decidir quais objetos realmente colidem e por fim tratar das colisões. Esse objeto especificamente faz o trabalho usando duas árvores de volumes hierárquicos que trabalham com AABB, existem outras implementações que utilizam outras técnicas, mas esta é a mais recomendada para mundos onde existem vários objetos dinâmicos se movendo e a remoção e adição de objetos é frequente.

Com estes cinco objetos podemos finalmente criar o mundo da Bullet e finalmente teremos nosso sistema de física pronto para ser usado, isso é feito no construtor da classe com o código abaixo:

mCollisionDispatcher = new btCollisionDispatcher(&mCollisionConfig);
   
mWorld = new btDiscreteDynamicsWorld(mCollisionDispatcher, &mBroadphase, &mConstraintSolver, &mCollisionConfig);

Primeiramente criamos a instância do btCollisionDispatcher usando como parâmetro a configuração do sistema de colisão e na sequência criamos o mundo onde a física será simulada.

Observe que a Bullet é bem flexível e modular, sendo possível customizar varias partes do motor sem que seja necessário modificar o código fonte do mesmo.

Por fim temos o destrutor do PhysicsManager, onde cuidamos de destruir os objetos alocados dinamicamente.

Objetos de Colisão

Como já temos nosso mundo físico criado, podemos começar a pensar em criar os objetos para popular ele. O primeiro item que precisamos definir para um objeto do mundo físico é como vai ser sua forma física, se vamos ter caixas, cilindros, esferas ou quem sabe, uma malha representando um objeto 3d.

Na Bullet os objetos de colisão são representados pela classe btCollisionShape, esta possui varias classes derivadas que representam as mais diversas formas de colisão, como podemos ver no diagrama abaixo:

Um detalhe importante sobre as formas de colisão é que elas podem ser compartilhadas entre corpos rígidos, ou seja, se você precisar criar 10 cilindros com a mesma dimensão, não é preciso criar 10 instâncias de btCollisionShape, pode-se criar apenas uma e usar com todos os corpos rígidos.

Para lidar com as formas de colisão iremos fazer um pequeno gerenciador destes dentro do PhysicsManager, iremos então armazenar as instâncias criadas dentro de um std::vector e no destrutor cuidaremos de destruir todas elas, primeiramente iremos adicionar aos atributos do PhysicsManager a declaração:

std::vector<btCollisionShape*> mCollisionShapes;

Modificaremos então o destrutor para quando o PhysicsManager for destruído, este cuide de destruir também as formas de colisão, isso pode ser feito com o código a seguir:

~PhysicsManager()
{
    for(int i = 0, len = mCollisionShapes.size();i < len; ++i)
        delete mCollisionShapes[i];

    delete mWorld;    
    delete mCollisionDispatcher;
}

Neste artigo iremos utilizar apenas a btBoxShape que é usada como o nome já sugere, para criarmos caixas no mundo físico, adicionemos um método para criar caixas:

btCollisionShape &createBoxShape(float x, float y, float z)
{
    btCollisionShape *shape = new btBoxShape(btVector3(x / 2, y / 2, z / 2));
    mCollisionShapes.push_back(shape);

    return *shape;
}

O método é bem simples, recebe como parâmetros 3 valores, chamados de x, y, e z, que são usados para representar as dimensões da caixa. O construtor da btBoxShape recebe como parâmetro “metade” da dimensão da caixa em cada eixo, por isso que dividimos o valor por 2 durante a criação da instância de btBoxShape. Depois de criado o objeto, o inserimos no vector e devolvemos uma referência para quem invocou o método.

Os valores usados para representar a dimensão da caixa dependem da sua aplicação, o valor 1, por exemplo, pode representar 1m ou 1cm, cabe a sua aplicação definir com que escala ela vai trabalhar.

Criando Corpos Rígidos

Como já temos como criar formas de colisão para os objetos, precisamos agora criar os corpos rígidos, para tal vamos adicionar um novo método ao PhysicsManager que vai criar um corpo rígido, que podemos ver abaixo:

btRigidBody &createBody(const btTransform &transform, float mass, btCollisionShape &shape)
{
    bool isDynamic = (mass != 0.0f);

    btVector3 localInertia(0,0,0);

    if (isDynamic)
        shape.calculateLocalInertia(mass,localInertia);

    btDefaultMotionState* myMotionState = new btDefaultMotionState(transform);

    btRigidBody::btRigidBodyConstructionInfo rbInfo(mass,myMotionState,&shape,localInertia);
    btRigidBody* body = new btRigidBody(rbInfo);
    
    mWorld->addRigidBody(body);

    return *body;
}

O método recebe três parâmetros, sendo eles:

  • transform: uma referência para um btTransform, que é uma classe da Bullet usada para representar transformações, ela armazena as rotações e posição do objeto no espaço. Para conhecer um pouco mais sobre transformações recomendo a leitura deste artigo e deste outro.
  • mass: um número de ponto flutuante que indica a massa do objeto, novamente cabe a sua aplicação definir o que este valor representa, geralmente quilos.
  • shape: uma referência para a forma de colisão que este objeto irá usar.

A primeira operação do método é descobrir se o objeto sendo representado é dinâmico ou não, no caso da Bullet, todo objeto com massa zero é considerado estático, ou seja, não importa o que aconteça, ele nunca irá se mover. Esse tipo de objeto é utilizado para construirmos as partes fixas dos cenários de um jogo, como paredes, chão, etc.

Caso o objeto seja dinâmico, precisamos calcular seu tensor de inércia, que é um vetor que representa matematicamente como o objeto se comporta com forças angulares, forças essas que fazem com que o objeto gire. Por exemplo, um lápis gira facilmente no seu eixo longitudinal, mas se tentarmos girar ele em outro eixo a resistência a rotação é maior devido ao seu comprimento.

Para simplificar o processo as formas de colisão da Bullet já calculam o vetor de inércia de um corpo e isso é feito com o código abaixo:

btVector3 localInertia(0,0,0);

if (isDynamic)
   shape.calculateLocalInertia(mass,localInertia);

O código acima possui um detalhe que pode passar despercebido pela maioria: repare que o vetor localInertia é inicializado explicitamente para zero, isso é feito devido a todos os objetos matemáticos da Bullet não possuírem valores default, ou seja, se fizermos apenas:

btVector3 localInertia;

Os valores x, y e z do vetor não vão ser inicializados e vão ter inicialmente valores indefinidos.

Após calcular o vetor de inércia, precisamos criar uma instância de btMotionState, esta classe tem como objetivo ser uma interface entre um motor gráfico e o sistema de física, ela sempre contém a versão mais atual das transformações de um corpo rígido e pode ser usado por um motor gráfico para atualizar as transformações dos objetos visuais.

É possível criar seu próprio btMotionState para fazer uma acoplagem mais direta com um motor gráfico, no nosso caso iremos usar apenas a versão padrão para simplificar o artigo, sendo esta operação realizada pela linha abaixo:

btDefaultMotionState* myMotionState = new btDefaultMotionState(transform);

Repare que o btDefaultMotionState foi inicializado com transform, que é a transformação que o método recebeu como parâmetro, essa transformação é usada como transformação inicial do corpo rígido.

Agora podemos finalmente criar o corpo rígido, para isso preenchemos um objeto do tipo btRigidBody::btRigidBodyConstructionInfo, que contém todos os parâmetros necessários para a construção do corpo, além da transformação, existem outros parâmetros usados para controlar, por exemplo:

  • fricção: indica um fator de atrito para o corpo quando ele estiver “esfregando” em outro corpo ou outro corpo “esfregar” nele.
  • damping: existe o linear e o angular, este valor é usado para que o objeto tenha uma perda de velocidade com o passar do tempo. Se este valor for zero, um objeto rodando no ar, por exemplo, pode ficar eternamente nessa condição. Podemos ver o damping como uma representação bem aproximada do atrito do objeto com o ar.

Vamos então criar o corpo rígido e adicioná-lo ao mundo físico:

btRigidBody::btRigidBodyConstructionInfo rbInfo(mass,myMotionState,&shape,localInertia);
btRigidBody* body = new btRigidBody(rbInfo);
    
mWorld->addRigidBody(body);

Para utilizar o PhysicsManager e criar um corpo rígido podemos fazer um código como abaixo:

PhysicsManager manager;

btCollisionShape &shape = manager.createBoxShape(1, 1, 1);
btRigidBody &body = manager.createBody(btTransform(btQuaternion::getIdentity(), btVector3(0, 0, 0)), 1, shape);

Por fim, é preciso modificar o PhysicsManager para que ele destrua os corpos rígidos criados, modificaremos o construtor novamente para adicionar o código abaixo:

for (int i=mWorld->getNumCollisionObjects()-1; i>=0 ;i--)
{
    btCollisionObject* obj = mWorld->getCollisionObjectArray()[i];
    btRigidBody* body = btRigidBody::upcast(obj);
    if (body && body->getMotionState())
    {
        delete body->getMotionState();
    }
    mWorld->removeCollisionObject( obj );
    delete obj;
}

O código acima acessa um vetor de objetos do mundo da Bullet, percorre objeto por objeto, caso ele seja um corpo rígido, libera a memória do seu btMotionState. Depois remove o objeto da lista do mundo físico e por fim destrói o objeto usando delete.

Atualizando a Simulação

Para que a simulação aconteça é necessário invocar o método stepSimulation da classe btDiscreteDynamicsWorld. Este método é quem faz a simulação física acontecer movimentando os objetos, tratando colisões e atualizando o btMotionState de cada um, sua invocação é bem simples e fazemos usando o método abaixo:

void update(float ticks)
{
    mWorld->stepSimulation(ticks);
}

Integrando Bullet com Ogre3d

A integração de ambos é relativamente simples, precisamos a principio apenas atualizar a posição dos SceneNodes do Ogre com base nos objetos físicos. Lembre-se que agora a Bullet é quem vai cuidar de movimentar os objetos da cena, precisamos então informar ao Ogre as novas posições dos objetos.

Para fazer esse trabalho vamos criar uma classe chamada SceneObject, que vai possuir referência para um SceneNode e para um RigidBody e um método para sincronizar a transformação do corpo rígido com o SceneNode, o código pode ser conferido a seguir:

class SceneObject
{
    private:
        Ogre::SceneNode &mNode;
        btRigidBody &mRigidBody;

    public:
        SceneObject(Ogre::SceneNode &node, btRigidBody &body):
          mNode(node),
          mRigidBody(body)
        {
            //empty
        }
        
        void prepareToRender()
        {
            btTransform transform;

            mRigidBody.getMotionState()->getWorldTransform(transform);

            btQuaternion rotation(transform.getRotation());

            mNode.setOrientation(rotation.getW(), rotation.getX(), rotation.getY(), rotation.getZ());

            const btVector3 &origin = transform.getOrigin();
            mNode.setPosition(origin.getX(), origin.getY(), origin.getZ());
        }
};

A parte inicial da classe e seu construtor não trazem grandes complicações: temos dois atributos, sendo uma referência para o SceneNode e outra para o RigidBody. No construtor apenas inicializamos ambos os atributos.

A mágica mesmo ocorre no método prepareToRender, que iremos chamar toda vez que for preciso desenhar a tela. O método primeiramente cria um btTransform (que como você deve se lembrar é a classe da Bullet que armazena transformações). Na sequência acessamos o btMotionState do corpo rígido e copiamos sua transformação para coordenadas do mundo, como mostrado abaixo:

btTransform transform;

mRigidBody.getMotionState()->getWorldTransform(transform);

Com a transformação do corpo rígido vamos então copiar seu Quaternio e alterar a transformação do SceneNode, isso é feito no código a seguir:

btQuaternion rotation(transform.getRotation());

mNode.setOrientation(rotation.getW(), rotation.getX(), rotation.getY(), rotation.getZ());

Por fim, vamos pegar a posição do corpo rígido e refletir essa posição no SceneNode, como mostrado abaixo:

const btVector3 &origin = transform.getOrigin();
mNode.setPosition(origin.getX(), origin.getY(), origin.getZ());

E com esta pequena classe já tornamos possível manter um SceneNode “sincronizado” com um corpo rígido. Se para cada objeto da cena que tiver um corpo rígido criarmos uma instância da classe SceneObject e invocarmos o método prepareToRender sempre antes de desenharmos os objetos, vamos manter os objetos sempre sincronizados, vamos ver como fazer isso no próximo item.

Criando uma Cena de Teste

Como já foi mencionado, iremos utilizar o código fonte do artigo “Primeiro programa com Ogre 3d”, bastando a principio incluir as novas classes que foram criadas ao longo deste artigo. Agora começaremos a modificar o código já existente.

Vamos então adicionar na classe Tutorial1 a declaração do PhysicsManager e um std::vector para armazenar as instâncias de SceneObject, como é feito no código abaixo:

class Tutorial1 
{
    private:
        Ogre::Root *mRoot;
        Ogre::Camera* mCamera;
        Ogre::SceneManager* mSceneMgr;
        Ogre::RenderWindow* mWindow;

        PhysicsManager mPhysics;

        std::vector<SceneObject *> mObjects;

        //resto do código existente
        //...
};

São declarações bem simples e similares com as que realizamos em outras partes do projeto, além dessas precisamos adicionar um código no destrutor da classe Tutorial1 para liberarmos a memória dos SceneObjects, isso é feito no código abaixo:

~Tutorial1(void)
{            
    for(int i = 0, len = mObjects.size();i < len; ++i)
        delete mObjects[i];

    delete mRoot;
}

O código acima apenas percorre o vetor e destrói qualquer objeto que tenha sido alocado, sem grandes complicações.

Agora que já temos uma infra-estrutura básica no programa exemplo, vamos adicionar um método para criar um objeto de cena e um corpo rígido. O método irá se chamar createBoxObject e vai criar apenas caixas através dos parâmetros:

  • name: nome do objeto a ser criado, que vai ser usado para nomear o SceneNode e a entidade do Ogre.
  • size: um Vector3 que irá conter as dimensões da caixa
  • pos: a posição inicial do objeto
  • mass: a massa do objeto
  • shape: a forma de colisão a ser usada com o objeto

A definição completa do código podemos ver abaixo:

void createBoxObject(const char *name, const Ogre::Vector3 &size, const Ogre::Vector3 &pos, float mass, btCollisionShape &shape)
{
    Ogre::SceneNode* node1 = mSceneMgr->getRootSceneNode()->createChildSceneNode(name);
    
    btRigidBody &body = mPhysics.createBody(btTransform(btQuaternion::getIdentity(), btVector3(pos.x, pos.y, pos.z)), mass, shape);
    
    mObjects.push_back(new SceneObject(*node1, body));
    
    Ogre::Entity *entity = mSceneMgr->createEntity(name, "Prefab_Cube");
    node1->attachObject(entity);

    node1->setScale(size.x / 100.0f, size.y / 100.0f, size.z / 100.0f);
}

A primeira ação do método é criar um SceneNode que irá representar o objeto na tela, isso é feito usando o código do Ogre que já conhecemos nos tutoriais de Ogre do PontoV.

Logo em seguida criamos o corpo rígido usando a função createBody que definimos no inicio do artigo, repare que o corpo é criado com a rotação inicial sendo a identidade, ou seja, sem rotação e configuramos a posição deste usando Vector3 passado como parâmetro, observe também que precisamos converter o Vector3 do Ogre para o btVector3 da Bullet.

De posse do corpo rígido e do SceneNode, podemos então criar o SceneObject e adicioná-lo no vetor que usamos para controlar as instâncias ativas.

Continuando iremos criar então uma Entity para termos um modelo 3d representando o objeto. Poderíamos muito bem carregar um modelo externo, mas como vamos apenas representar cubos, utilizamos um objeto pré-fabricado do Ogre, que já vem com formas primitivas básicas definidas. Após criar o cubo, anexamos ele ao SceneNode que criamos.

Um ultimo passo na criação do nosso objeto é ajustar o tamanho do modelo 3d ou do cubo. O cubo pré-fabricado pelo Ogre vem com dimensões 100x100x100, então alteramos a escala do SceneNode para que o cubo adquira a mesma dimensão do valor passado como parâmetro, nesse caso, o parâmetro size.

Por fim, como já temos um método prático para criação de objetos, vamos criar alguns para popular nossa cena, faremos isso na listagem abaixo:

void createObjects()
{
    btCollisionShape &planeShape = mPhysics.createBoxShape(200, 1.0f, 200);
    btCollisionShape &cubeShape = mPhysics.createBoxShape(10, 10, 10);
    btCollisionShape &plane2Shape = mPhysics.createBoxShape(50, 1.0f, 50);

    createBoxObject("cube", Ogre::Vector3(10, 10, 10), Ogre::Vector3(0, 100, 0), 1, cubeShape);
    createBoxObject("cube2", Ogre::Vector3(10, 10, 10), Ogre::Vector3(0, 80, 0), 5, cubeShape);
    createBoxObject("cube3", Ogre::Vector3(10, 10, 10), Ogre::Vector3(0, 130, 0), 5, cubeShape);
    createBoxObject("cube4", Ogre::Vector3(10, 10, 10), Ogre::Vector3(30, 150, 0), 10, cubeShape);
    createBoxObject("plane2", Ogre::Vector3(50, 1, 50), Ogre::Vector3(20, 110, 0), 1, plane2Shape);

    createBoxObject("floor", Ogre::Vector3(200, 1, 200), Ogre::Vector3(0, -50, 0), 0, planeShape);        
}

O método acima cria inicialmente três formas de colisão, que são atribuídas as variáveis planeShape, cubeShape e plane2Shape. Criamos-las em separado para podermos compartilhar o maior número possível entre os objetos. A primeira forma chamada de planeShape vai ser usada para representar o chão da cena, a segunda forma, cubeShape é usada para representar caixas que vão cair e por fim, a terceira é usada para representar uma prancha que cai junto com as caixas.

Em seguida temos varias chamadas a createBoxObject, sendo a primeira:

createBoxObject("cube", Ogre::Vector3(10, 10, 10), Ogre::Vector3(0, 100, 0), 1, cubeShape);

Nessa chamada estamos criando um SceneObject chamado de “cube”, que vai ter tamanho 10x10x10, posicionado nas coordenadas 0, 100, 0, com massa 1 e vai ter como forma de colisão a cubeShape.

Depois criamos mais 3 cubos, repare que são mais pesados que esse primeiro, isso é para eles causarem um maior impacto no primeiro cubo quando caírem, apenas para um efeito mais interessante no exemplo final.

No final temos a linha abaixo:

createBoxObject("floor", Ogre::Vector3(200, 1, 200), Ogre::Vector3(0, -50, 0), 0, planeShape); 

Esta é praticamente idêntica as anteriores, mas repare aqui que a massa do objeto é definida como zero e não custa lembra que isso significa que temos um objeto estático, que nunca vai se mover.

Preenchendo a Cena e Atualizando a Simulação

Agora que já podemos criar uma pequena cena de demonstração vamos modificar o método runApplication para que este inicialize a cena e depois cuide de simular nossa física, primeiro vamos adicionar a chamada ao método createObjects, logo após a configuração da câmera e depois no loop principal cuidar de atualizar a simulação, o código todo pode ser visto abaixo:

bool runApplication()
{            
    // Posiciona a Camera
    mCamera->setPosition(Ogre::Vector3(0, 0, 300));
    // Manda a camera olhar para um ponto
    mCamera->lookAt(Ogre::Vector3(0, 0, -300));
    // Distancia mínima para que o objeto deve estar da camera para ser renderziado
    mCamera->setNearClipDistance(5);
    // Distancia máxima para que o objeto deve estar da camera para ser renderziado
    mCamera->setFarClipDistance(5000);            

    createObjects();

    // Cria Luz ambiente
    mSceneMgr->setAmbientLight(Ogre::ColourValue(0.5, 0.5, 0.5));
     
    // Cria uma Luz
    Ogre::Light* l = mSceneMgr->createLight("Luz");
    l->setPosition(20, 80, 50);                

    //Loop Principal
    for(;;)
    {
        // Processa as mensagens que o Sistema Operaciona envia para aplicação
        Ogre::WindowEventUtilities::messagePump();

        // Parar o programa caso a janela seja fechada
        if(mWindow->isClosed()){
            return false;
        }

        mPhysics.update(0.001);

        for(int i = 0, len = mObjects.size();i < len; ++i)
            mObjects[i]->prepareToRender();


        // Renderiza Um Frame
        if(!mRoot->renderOneFrame()) 
            return false;
    }
    return false;
}

O código é bem similar ao código original, apenas adicionamos:

  • a chamada a createObjects logo após configuração da câmera para que fossem criados os objetos.
  • No loop principal invocamos o método update do PhysicsManager para que este rode a simulação física, repare que não usamos qualquer informação de clock, apenas colocamos um valor hardcoded para testarmos a simulação.
  • Após a chamada de update no PhysicsManager, percorremos a lista de SceneObject invocando o método prepareToRender de cada um, assim garantimos que cada SceneNode vai estar na posição correta.

    Repare também que foi removido o código original que carregava modelos 3d aleatoriamente, pois agora vamos utilizar apenas os objetos que criamos no código.


  • Configurando o Projeto

    Agora que o código esta pronto, precisamos incluir as bibliotecas da Bullet nas informações do linker, para isso clique como botão direito do mouse no nome do projeto e escolha propriedades, como mostrado na figura abaixo:

    Configurando as bibliotecas no projeto

    Na tela que se abrir selecione Linker –> Input e na caixa “Additional Dependencies” adicione as bibliotecas BulletDynamics.lib BulletCollision.lib LinearMath.lib, como é mostrado na figura abaixo:

    Bibliotecas a serem adicionadas ao projeto

    O projeto esta quase pronto para ser executado, faltando apenas dizer ao Visual onde ele deve ser executado e onde ele pode encontrar as DLLs, para isso, na mesma tela clique em “Debugging”, na caixa “Working Directory” entre com o diretório onde estão os arquivos de configuração do Ogre, no download abaixo você encontra esses arquivos dentro do diretório work, bastando então colocar o caminho para este diretório (na minha maquina, ficou como: “C:\develop\srcs\pontov\artigos\fisica\tutorial_01\work”). Em seguida é preciso dizer ao Visual que ele deve incluir o diretório das DLL´s do Ogre na variável PATH do sistema, isso é para que o Windows possa encontrar as DLLs ao executar a sua aplicação, pois os caminhos contidos na variável PATH é um dos locais onde o Windows procura DLLs, sendo que na minha estação de trabalho o valor ficou como: PATH=C:\develop\libs\ogre\build\bin\debug. Altere o caminho conforme a necessidade do seu sistema, a configuração final pode ser vista abaixo:

    Untitled

    Agora seu projeto esta pronto para executar!

    Considerações Finais

    Vimos neste artigo como utilizar uma biblioteca profissional para simulação física, incluindo sua instalação, configuração e integração com um motor gráfico 3d. O código aqui mostrado visa principalmente a simplicidade para facilitar o entendimento dos conceitos envolvidos, sendo que em jogos completos é recomendável organizar esses objetos de forma mais robusta, mas as classes aqui criadas para realizar o gerenciamento podem ser facilmente utilizadas para se testar conceitos ou até mesmo prototipar um jogo simples, ficando como exercício ao leitor expandir estas para suportar novas formas de colisão e experimentar outros tipos de objetos físicos.

    Em artigos futuros iremos explorar outros conceitos da biblioteca Bullet e também nos aprofundar nos conceitos apresentados neste artigo, aguardem!

    Downloads

    Para baixar o código fonte e projetos do Visual Studio clique no ícone abaixo.

    O código fonte do projeto também pode ser encontrado no SVN do PontoV clicando-se aqui.


    Comentários (15)
    • Luis Eduardo Nery Santos  - Visual Studio 2010
      avatar

      Olá, Bruno.

      Estou criando um jogo estilo Arkanoid, como projeto na faculdade.

      Gostaria de saber se essa biblioteca é indicada para utilizar em jogos 2d, feitos com c++ e OpenGL e também estou com duvidas na hora de configurar a variavel de ambiente no visual studio 2010, pois o VC++ não está habilitado.

      Parabéns pelos posts.

      Obrigado. =)

    • Vinícius Godoy de Mendonça
      avatar

      Para jogos 2D provavelmente será mais fácil utilizar a Box2D:
      http://box2d.org/

      O VC++ só será habilitado depois que você criar o primeiro arquivo .cpp dentro do seu projeto. Crie ele e o menu aparece. :)

    • Luis Eduardo Nery Santos  - Instalação da biblioteca
      avatar

      Valeu Vinicius! vou utilizar essa biblioteca para fazer meu jogo então.

      Mas eu não sei muito bem como instalar bibliotecas, tem algum tutorial pra me indicar?

      ou se for simples tem como dar uma explicada resumida?

      Obrigado. :lol:

    • Bruno Crivelari Sanches
      avatar

      Luis,

      no geral bibliotecas C/C++ seguem sempre um mesmo padrão:
      - extrair os arquivos para algum lugar
      - configurar diretório de includes do compilador
      - configurar diretório de libs do linker
      - nas configurações do seu projeto, adicionar as bibliotecas novas como dependencias

      Nesse artigo é feito tudo isso e ainda inclui o passo extra de compilar a bilbioteca. O processo pode variar uma biblioteca para outra, mas os passos básicos são os que listei aqui.

      Esse outro artigo mostra o processo para uma outra biblioteca (SDL) e explica em mais detalhes alguns conceitos, serve como uma "receita" para instalação de bibliotecas:
      http://pontov.com.br/site/cpp/46-conceitos-basicos/1 55-como-usar-bibliotecas-cc

    • augustowebd  - Versão para impressão
      avatar

      Bruno mais uma vez parabéns pelo excelente artigo, só gostaria de solicitar, não querendo ser abusado, uma versão para impressão completo, porque a versão para impressão atual pega apenas a página corrente.

      Grato.

    • Bruno Crivelari Sanches
      avatar

      Obrigado Augusto!

      Quando clico ali no "Todas as páginas" e depois na impressorinha, funciona com todas as páginas.

      T+

    • Napoleon  - Oiii
      avatar

      :cheer: Ei eu gostei muito de sua matéria, tem como falar do box2D :woohoo:

    • Bruno Crivelari Sanches
      avatar

      Oi Napoleon, vou por na lista aqui! :)

    • japaseki
      avatar

      Fala Sanches, estou tentando executar este projeto mas quando ele entra na tela para escolher o render (opengl ou DIRECTX), ele cai automaticamente....o q poderia ser ?

    • Bruno Crivelari Sanches
      avatar

      Olá,

      você seguiu a risca as instruções da ultima página no ultimo item "Configurando o Projeto" ?

      Se sim, tem que depurar para ver onde ocorre o erro e o porque.

      Para saber como usar o depurador: http://www.pontov.com.br/site/cpp/41-visual-c/216-como-usar-o-depurado r-do-visual-c

      T+

    • matheus bueno
      avatar

      ola,


      parabens pelo post, muito bom mesmo,

      fugindo um, pouco do assunto, eu estou criando um jogo( na verdadde é uma especie d PacMan modficado) em allegro, tem como usar a box 2D junto com o allegro, e se tiver, compensa usar?

      obriigado.

    • Bruno Crivelari Sanches
      avatar

      Para um pacman? Não vejo porque usar biblioteca de física. É um sistema de colisão tão simples.

    • Matheus Bueno
      avatar

      no caso haveriam modificações nele, seria meio temático, por exemplo em uma faze com o tema espaço sideral os fantasmas seriam et's que disparariam lazer's e por ai vai, o pacman também ganharia up grades de acordo com os pontos acumulados, esses up grades poderiam ser, por exemplo, mais tempo invencivel quando se come uma pirula, ou almento na velocidade,pensei q em algumas ocasioens aplicar uma biblioteca de física facilitaria meu trabalho.

      o problema maior no jogo esta sendo a AI, mas eu ta pensando em criar um grafo pra representar o labirinto, e calcular o menor caminho entre o fantasma e o pacman, só q eu to em duvida, se isso vai pesar meu jogo(se existe uma solução melhor ficaria grato em ouvir).

      quanto a biblioteca de fisica, gostaria de saber apenas se existe essa possibilidade, ate pra outros jogos com allegro, o pacman é meu primeiro jogo, é mais pra aprendizado, mas eu gostaria de fazer direitinho.

    • Bruno Crivelari Sanches
      avatar

      Pesar? Depende de onde você vai rodar, se for para um PC, um pacman dificilmente vai pesar.

      Sobre a física não vejo dificuldades em integrar com Allegro, geralmente as bibliotecas são independentes de API, como foi mostrado no artigo.

      T+

    • Aml  - Octree e outros
      avatar

      Bruno, antes de mais nada, Parabéns pelos artigos.
      Eu vou utilizar a engine, mas fiquei com uma dúvida:
      Bullet já trabalha com Octree ou é necessário implementar em caso de múltiplos elementos no cenário?


    Escrever um comentário
    Your Contact Details:
    Gravatar enabled
    Comentário:
    [b] [i] [u] [url] [quote] [code] [img]   
    :angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch::(:shock:
    :X:side::):P:unsure::woohoo::huh::whistle:;):S:!::?::idea::arrow:
    Security
    Por favor coloque o código anti-spam que você lê na imagem.
    LAST_UPDATED2  

    Busca

    Linguagens

    Twitter