Docstoc

Desenvolvimento de Jogos - Ogre3D

Document Sample
Desenvolvimento de Jogos - Ogre3D Powered By Docstoc
					RAFAEL RIEDEL

DESENVOLVIMENTO DE JOGOS:
Uma abordagem prática ao Ogre3D e seus subsistemas.

BELO HORIZONTE UNIVERSIDADE FUMEC 2007

RAFAEL RIEDEL

DESENVOLVIMENTO DE JOGOS:
Uma abordagem prática ao Ogre3D e seus subsistemas.

Monografia de Conclusão de Curso apresentada à disciplina Trabalho de Conclusão de Curso da Universidade FUMEC como requisito parcial para obtenção do título de Bacharel em Ciência da Computação. Orientadores: Prof. Roberlei Panasiewicz e Prof. Paulo Henrique.

BELO HORIZONTE 2007

RAFAEL RIEDEL

DESENVOLVIMENTO DE JOGOS:
Uma abordagem prática ao Ogre3D e seus subsistemas.

Monografia de conclusão de curso submetida à Universidade FUMEC como requisito parcial para obtenção do título de Bacharel em Ciência da Computação e aprovada pela seguinte banca examinadora:

______________________________________ Professor Roberlei Panasiewicz (Professor TCC) Universidade Fumec

_________________________________________ Professor Paulo Henrique (Orientador) Universidade Fumec

Belo Horizonte, 25 de Junho de 2007

RESUMO

A área de desenvolvimento de jogos e simulações é extensa, envolve desenvolvimento de aplicações, modelagem e animação tridimensional, composição de áudio, artes gráficas entre outros. Porém, no que se diz em desenvolvimento de aplicações, existem diversos tipos de motores gráficos, sistemas interface, sistemas dinâmicos, plataformas de áudio, gerenciadores de dispositivos de entrada. Realizar uma integração destas ferramentas é crítico para ter sucesso no desenvolvimento da aplicação. É isto que será visto nesta monografia, como realizar esta integração utilizando-se ferramentas de código aberto e/ou gratuítas e multiplataformas.

PALAVRAS CHAVES: Desenvolvimento de Jogos; Engine Gráfico; Ogre3D; CEGUI; OpenAL; ODE; Newton Dynamics; Python.

Índice de ilustrações
RapidSVN - Checkout..............................................................................................................24 IDE SPE....................................................................................................................................25 Tela principal do Blender..........................................................................................................27 Janela de configuração do Ogre3D...........................................................................................32 Câmera Frustrum.......................................................................................................................34 Primeira Renderização..............................................................................................................38 Cenário Carregado....................................................................................................................46 SkyBox......................................................................................................................................48 SkyBox - Distância Reduzida...................................................................................................49 SkyDome...................................................................................................................................51 Iluminação Ambiente 10%........................................................................................................55 Iluminação Pontual...................................................................................................................56 Iluminação por Spotlight...........................................................................................................58 Iluminação direcional................................................................................................................59 Texture Shadows.......................................................................................................................64 Stencil Shadows........................................................................................................................65 Tambores com dinâmica aplicada.............................................................................................87 Esfera de movimento................................................................................................................95 Exemplo 1 de Janela do CEGUI ............................................................................................114 Exemplo 2 de Janela do CEGUI.............................................................................................115

Lista de Quadros de Código
Código basico do TechDemo....................................................................................................30 Definição do método setupOgre( ),iniciando o sistema............................................................32 Realizando a chamada ao método setupOgre( ), no método run( )...........................................32 Implementação da destruição dos objetos no desconstrutor da classe......................................32 Definição do método configuracaoVisual( ) juntamente com a criação do SceneManager.....34 criação da câmera......................................................................................................................34 Configuração dos parâmetros da câmera..................................................................................35 Chamada ao método configuracaoVisual( )..............................................................................35 Método para carregar os recursos a partir do arquivo resources.cfg. .......................................36 Execução do método configuraRecursos( ) e iniciando os grupos de recursos........................37 Iniciando o loop principal de renderização...............................................................................37 Uma outra froma de realizar o loop de renderização................................................................37 Declaração da classe BasicoListener.........................................................................................40 Implementação do método configuraFrameListener( ).............................................................40 Importação do OIS....................................................................................................................42 Implementação da classe InputListener....................................................................................42 Instanciando a classe InputListener e atribuindo a instância como frameListener...................43 Importando o dotScene.............................................................................................................46 Implementação do método criaCenario( ).................................................................................46 Configurando skyBox...............................................................................................................48 Configurando skyDome............................................................................................................51 Configurando a luz ambiente....................................................................................................55 Criando um sistema de iluminação pontual..............................................................................57 Criando um sistema de iluminação por spotlight......................................................................58 Criando um sistema de iluminação por luz direcional..............................................................60 Configurando Texture Shadows................................................................................................64 Utilizando técnica de sombra Stencil Shadows Additive..........................................................65 Importação dos pacotes necessários para a classe InputManager.............................................70 Implementação da classe InputManager...................................................................................71 Definição do construtor do InputManager................................................................................71 Instanciando os objetos MouseListener e KeyListener.............................................................71

Atribuição dos callbacks para o OIS.........................................................................................72 Implementação do desconstrutor do InputManager..................................................................72 Implementando os registradores e desregistradores do InputManager.....................................73 Realizando a captura dos dispositivos de entrada.....................................................................73 Implementação da classe KeyListener......................................................................................74 Implementação da classe MouseListener..................................................................................74 Importando o InputManager dentro do TechDemo...................................................................74 Instanciando o InputManager....................................................................................................75 Adicionando a instância do InputManager no FrameListener..................................................75 Modificando a classe BasicoListener para o novo processo de abandonar a aplicação............76 Implementando o método updateKeyPressed( ), na classe BasicoListener, para informar quando alguma tecla for pressionada........................................................................................76 Implementação do método frameStarted( ) na classe BasicoListener.......................................76 Registrando a instância da classe BasicoListener como observador do InputManager............76 Importando o pacote OgreNewt................................................................................................79 Método setupNewton( ), para realizar a configuração básica do Newton................................79 Realizando a chamada ao método setupNewton( ) no método run( ).......................................80 Criando materiais......................................................................................................................81 Configuração do par de material...............................................................................................82 Implementação do método criaCorpoTreeCollision( )..............................................................83 Implementando chamada ao método criaCorposRigidos( )......................................................83 Implementação do método criaCorposRigidos( ).....................................................................84 Criando os corpos rígidos dos objetos necessários no cenário.................................................84 Implementação do método criaCorpoConvexHull( )................................................................85 Realizando a configuração para os corpos rígidos móveis.......................................................86 Implementação da classe NewtonUpdateListener.....................................................................87 Colocando a instância da classe NewtonUpdateListener como frameListener.........................87 Implementação da classe ator....................................................................................................89 Definições das variáveis da classe............................................................................................90 Definição do método criaCaracter( ).........................................................................................90 Determinando o tamanho do raio para a esfera.........................................................................91 Ajustando o tamanho do raio da esfera.....................................................................................91 Definindo a configuração dos parâmetros dinâmicos da esfera................................................92 Sincronizando a posição da esfera para mesma posição da entidade........................................92

Definindo as junções da esfera..................................................................................................92 Informando ao Newton para não deixar a esfera “dormir”.......................................................93 Definindo o método de forceCallback......................................................................................93 Anexando a esfera para o nodo dinâmico.................................................................................94 Implementação do método andaParaFrente( )..........................................................................94 Implementações dos métodos virarParaEsquerda( ) e virarParaDireita( )................................95 Implementação do método pararDeAndar( )............................................................................95 Implementação do método atorControleCallback( ).................................................................95 Ajuste dos valores do ângulo de direção...................................................................................96 Definição do vetor de direção...................................................................................................97 Aplicação da força de gravidade...............................................................................................97 Ajustando a velocidade do corpo..............................................................................................98 Sincronizando a posição e orientação do personagem com a posição e orientação da esfera.. 98 Fazendo a esfera mudar de orientação......................................................................................98 Definições das variáveis básicas de animação........................................................................100 Carregando as animações do ator............................................................................................100 Implementação do método frameStarted( ), para sincronizar a animação..............................100 Atribuindo a animação de andar, no método andarParaFrente( )............................................101 Atribuindo a animação idle, no método pararAndar( )...........................................................101 Implementação do método atirar( ).........................................................................................101 Decisão do que o ator fará quando uma determinada tecla for pressionada...........................102 Atribuindo o valor None à variável teclaAtual.......................................................................102 Implementações dos métodos updateKeyPressed( ) e updateKeyReleased( )........................103 Implementação do método trackMe( )....................................................................................103 Fazendo a câmera olhar para a o ator......................................................................................103 Importação da classe Ator na classe TechDemo....................................................................104 Implementação do método configuraAtores( ).......................................................................104 Realizando a chamada ao método configuraAtores( )............................................................105 Lista de widgets suportados pelo TaharezLook......................................................................108 Exemplo de um arquivo XML com o layout de uma interface...............................................108 Importações dos pacotes básicos na classe CEGUIManager..................................................109 Implementação da classe CEGUIManager.............................................................................109 Método de configuração do CEGUI........................................................................................110 Exemplo da configuração da fonte..........................................................................................111

Implementação dos métodos de injeção de eventos no CEGUI..............................................113 Implementação do método exibeGUI( )..................................................................................113 Implementação da mudança da GUI, no método updateKeyPressed( )..................................114 Métodos registraCEGUIObservadores( ) e cancelaCEGUIObservadores( )..........................114 Instanciando a classe CEGUIManager e registrando seus eventos.........................................114 Registrando o método abandonarAplicacao a um evento no CEGUI.....................................117 Atribuindo a instância do método basicoListener.requestShutdown( ) à variável CEGUIManager.metodoSair...................................................................................................117 Método configuraAudio( ). Instanciando a classe OgreAL.SoundManager( ).......................118 Carregando um arquivo de som e anexando-o a um sceneNode.............................................119 Código fonte do dotscene.py. Extraído da demonstração do Python-Ogre.............................123 Conteúdo do arquivo resources.cfg.........................................................................................124 Conteúdo do arquivo plugins.cfg............................................................................................124

Sumário
RESUMO...............................................................................................................................4 INTRODUÇÃO...................................................................................................................15 1 APRESENTAÇÃO E CONFIGURAÇÃO..................................................................17 1.1 Apresentação das Ferramentas......................................................................................17 1.1.1 Ogre – O motor gráfico 3D ...................................................................................17 1.1.2 Python – A linguagem de programação ................................................................18 1.1.3 Python-Ogre – A ponte entre o Ogre e o Python ..................................................18 1.1.4 CEGUI – Interface Gráfica com o Usuário...........................................................19 1.1.5 OpenAL – Mecanismo de Áudio ..........................................................................20 1.1.6 Dinâmica com OgreNewt .....................................................................................20 1.1.7 Blender – Estúdio de modelagem e animação 3D ................................................21 1.1.8 Integração entre Blender e Ogre - Ferramentas de conversão ..............................22 1.2 Configuração do ambiente de desenvolvimento ..........................................................22 1.2.1 Instalando as ferramentas .....................................................................................23 1.2.2 Python ...................................................................................................................23 1.2.3 wxPython ..............................................................................................................23 1.2.4 RapidSVN..............................................................................................................24 1.2.5 Python IDE ...........................................................................................................24 1.2.6 DirectX..................................................................................................................26 1.2.7 Python-Ogre ..........................................................................................................27 1.2.8 Blender ..................................................................................................................27 1.2.9 Exportador para o Ogre ........................................................................................28 1.3 Testando o ambiente de desenvolvimento ....................................................................29 1.4 Configurando o Projeto Inicial......................................................................................29 2 OGRE3D........................................................................................................................31 2.1 Configurando e Iniciando o Ogre..................................................................................31 2.2 Frame Listener: Princípios Gerais ................................................................................39 2.3 Gerenciamento de Dispositivos de Entrada: Abordagem superficial ao OIS................41 2.4 Criando Cenários ..........................................................................................................44 2.5 A Estrutura do SceneManager.......................................................................................44 2.5.1 SkyPlane, SkyBox e SkyDome.............................................................................48 2.5.1.1 SkyBox...........................................................................................................48 2.5.1.2 SkyDome........................................................................................................50 2.5.1.3 SkyPlane........................................................................................................52 2.5.2 Iluminação e Sombras...........................................................................................53 2.5.2.1 Luzes .............................................................................................................53 2.5.2.1.a Luz Ambiente ........................................................................................55

2.5.2.1.b Luz Pontual (LT_POINT)......................................................................56 2.5.2.1.c Luz Spotlight (LT_SPOTLIGHT) .........................................................57 2.5.2.1.d Luz Direcional (LT_DIRECTIONAL) ..................................................59 2.5.2.2 Sombras .........................................................................................................60 2.5.2.2.a Stencil Shadow ......................................................................................61 2.5.2.2.b Texture-Based Shadow ..........................................................................62 2.5.2.2.c Sombras Modulativas ............................................................................63 2.5.2.2.d Sombras de Máscara Aditiva .................................................................63 3 OIS E DINÂMICA........................................................................................................68 3.1 OIS – Object Oriented Input System ............................................................................68 3.1.1 Classes OIS Listeners ...........................................................................................68 3.1.1.1 Classe KeyListener .......................................................................................68 3.1.1.2 Classe MouseListener ...................................................................................69 3.1.2 Implementação da classe InputManager................................................................69 3.2 Dinâmica com Newton..................................................................................................77 3.2.1 Classe Mundo........................................................................................................79 3.2.2 Materiais................................................................................................................80 3.2.3 Corpos Rígidos......................................................................................................82 3.3 Classe Ator....................................................................................................................88 3.3.1 Implementação.......................................................................................................89 3.3.2 Dinâmica................................................................................................................89 3.3.3 Animação...............................................................................................................99 3.3.4 Configuração do Teclado.....................................................................................102 4 INTERFACES E ÁUDIO...........................................................................................106 4.1 Interfaces - CEGUI......................................................................................................106 4.1.1 Estrutura XML do CEGUI...................................................................................107 4.1.2 Implementação.....................................................................................................108 4.1.3 Respondendo aos eventos do CEGUI..................................................................116 4.2 Áudio com OgreAL.....................................................................................................118 4.2.1 Implementação.....................................................................................................118 CONCLUSÃO...................................................................................................................120 ANEXOS............................................................................................................................121 Anexo 1 – Dotscene.py.....................................................................................................121 Anexo 2 - resources.cfg....................................................................................................124 Anexo 3 - plugins.cfg.......................................................................................................124 Anexo 4 – Scene.xml........................................................................................................125 REFERÊNCIA BIBLIOGRÁFICA.................................................................................128

13

INTRODUÇÃO

Este é um trabalho de pesquisa realizado sobre o motor gráfico Ogre3D e sua integração com os demais sistemas, os quais são necessários para a confecção de um jogo ou simulação, como Newton para simulação de dinâmica com corpos rígidos, CEGUI para interfaces, OIS para gerenciar os dispositivos de entrada e OpenAL para realizar manipulação dos sons. A proposta desta monografia é a) demonstrar que é possível desenvolver jogos e simulações de qualidade utilizando-se ferramentas de código aberto e/ou gratuítas, sem restrição de uso (tanto para aplicações gratuítas de código aberto, ou não, e/ou comerciais), b) a aplicação deverá ser multiplataforma, sem necessidade de alterações, ou com alterações mínimas em seu código fonte, c) utilizando uma linguagem de programação fácil de aprender, com sintaxe simples e mais eficiente no desenvolvimento, d) fazendo com que a pessoa que leia esta monografia, seguindo os passos do desenvolvimento da aplicação nela contida, irá aprender como desenvolver este tipo de programa com estas ferramentas e e) despertar o interesse das pessoas sobre o desenvolvimento de jogos e simulações. Com base nesta pesquisa, foi feito um levantamento e estas ferramentas se destacaram pela qualidade, maturidade, licença de uso, documentação disponível na internet e a grande quantidade de projetos que as utilizam. Para a linguagem de programação foi selecionado o Python, devido a sua rápida curva de aprendizado, além de ser completamente orientada a objetos. Contudo, estas ferramentas estão disponíveis nativamente apenas para a linguagem de programação C++, impossibilitando o seu uso diretamente com o Python. Neste ponto entra o projeto Python-Ogre, que realiza os bindings1 não só do Ogre3D, mas de todo o conjunto de ferramentas (Ogre3D, OgreAL, OgreODE, OgreNewt, CEGUI e OIS), fechando assim o pacote essencial de bibliotecas. Nota-se que foi citado OgreAL ao invés do OpenAL, OgreODE ao invés de ODE e OgreNewt ao invés do Newton. Isto se deve que o OgreAL, OgreODE e OgreNewt são projetos independentes que encapsulam os sistemas OpenAL, ODE e Newton com o Ogre3D, adicionando alguns métodos extras para facilitar o desenvolvimento.
1Bindigns ou wrappers, é um tipo de aplicação que realiza a conexão de bibliotecas que se encontram nativas em uma linguagem (como C++), para uma outra linguagem qualquer, desde que seja incompatível com a biblioteca original, no caso Python.

14 Será feito uma abordagem prática da pesquisa realizada em torno desta integração, o qual será construído uma pequena aplicação, que servirá de demonstração da tecnologia (daí o nome TechDemo), que terá um fim didádico, explicitando como que é feita tal integração, além de ensinar de forma teórica e prática o funcionamento e a estrutura destas ferramentas. Esta abordagem será no formato de um tutorial da construção desta desmonstração, cobrindo todos os tópicos relevantes ao sistemas utilizados. Concluindo o tutorial, existirá uma aplicação funcional básica, utilizando-se apenas os recursos de mídia (sons, modelos, texturas, etc) oferecidos pela instalação do Python-Ogre. Esta monografia não é um manual de uso destas ferramentas. Os pré-requisitos, para o desenvolvedor, é possuir prática no desenvolvimento em algumas linguagens de programação, como C++ e/ou Java e ter conhecimentos de orientação a objetos. É desejável possuir conhecimento em Python, que é a linguagem utilizada neste projeto. Recomenda-se também ter algum conhecimento em Padrões de Projetos (Design Pattern). Quanto aos requisitos de ambiente, deve-se utilizar o sistema operacional Windows XP, com o DirectX 9c (ou OpenGL). Apesar de não ter sido testado, mas esta monografia deverá funcionar também no Linux, utilizando-se OpenGL. A única modificação que deverá ser feita, serão os caminhos utilizados, que no Linux serão outros. Para o equipamento, recomenda-se utilizar um computador com um processador de pelo menos 1 GHz, com 512 MB de memória RAM, dispositivo de vídeo nVidia GeForce ou ATI Radeon (as restrições gráficas aos dispositivos de vídeo podem ser vista no sítio do Ogre3D) e espaço em disco livre, recomendado para a instalação, de pelo menos 300 MB. Além de obrigatoriamente ter conexão com a Internet. Esta monografia foi realizada em um equipamento com o processador Atlhon XP 1700+, 256MB de memória RAM, dispositivo de vídeo nVidia GeForce 3 Ti (foi testado também com sucesso em uma SiS 661 FX) e com 6 GB de espaço livre em disco e conexão com a Internet de 100kbps.

15

1

APRESENTAÇÃO E CONFIGURAÇÃO

1.1

Apresentação das Ferramentas

1.1.1

Ogre – O motor gráfico 3D

Ogre (Object Oriented Graphics Rendering Engine) é um motor de renderização gráfica em tempo real de código aberto. Em outras palavras, é uma plataforma gráfica que provê o acesso às aplicações que a utilizar, a um complexo ambiente gráfico tridimensional, reduzindo e simplificando o difícil trabalho de operar neste ambiente. O Ogre é mantido por um pequeno time de desenvolvedores que trabalham em seu núcleo principal, e que também tem sido contribuído por muitos usuários pela Internet. As plataformas que o Ogre pode ser utilizado são Linux, Windows e Mac OS X, sendo este último não suportado oficialmente, mas realizando a compilação do Ogre no Mac é possível utiliza-lo. Também existe possibilidade de escolha entre os dois renderizadores principais do mercado, o OpenGL e DirectX. Apesar que por padrão seja possível utilizar o Ogre apenas na linguagem C++, é possível utilizar em outras linguagens, como Java, Python e .NET, através de bibliotecas que mapeiam as bibliotecas para a linguagem específica. É valido lembrar que o Ogre não é uma plataforma para criar jogos, e tampouco um game engine. Pois ele não fornece as bibliotecas requeridas para sua criação, como Som, Rede, Entrada e Colisão, segundo a página do Ogre3D (2007, Tradução Nossa):
OGRE é um componente em um grande sistema de desenvolvimento. OGRE não é, e nunca teve, a pretensão de ser uma plataforma de desenvolvimento de jogos completa, é uma ferramenta para um objetivo específico.

O Ogre é apenas um componente de um todo, que tem como fim, somente e só, como ferramenta para renderização gráfica. Com Ogre é possível realizar, sem limitações, qualquer tipo de aplicação que necessite um ambiente gráfico tridimensional, seja um simulador de vôo, um FPS – First Person Shooter – (jogos de primeira pessoa), jogos em

16 terceira pessoa, software de visualização CAD. Não importa o fim que terá a aplicação, o Ogre proverá o ambiente gráfico, sem restrição quanto ao desenvolvimento.

1.1.2

Python – A linguagem de programação

A parte mais importante para o desenvolvimento de alguma aplicação, é a escolha da linguagem, e para este caso, a linguagem de programação Python foi escolhida para ser o idioma principal para os tutoriais. O motivo da escolha de seu uso, esta em sua própria definição, extraído do site:
Python é uma linguagem de programação orientada a objeto que pode ser usada para vários tipos de desenvolvimento de software. Ele oferece um forte suporte para integração com outras linguagens e ferramentas, sua biblioteca padrão é extensa, e pode ser aprendido em alguns dias. Muitos programadores de Python relatam um ganho de produtividade substancial e sentem que a linguagem encoraja o desenvolvimento de um código com maior qualidade e de fácil manutenção. (Python, 2007, Tradução Nossa)

Python pode ser executado em diversas plataformas, como Linux/Unix, Windows, Mac OS X, Palms Handhelds, e inclusive foi portado para as maquinas virtuais do Java e .NET. Python preenche os pré-requisitos para trabalhar com o Ogre, e vai além, a sua curva de aprendizado é extremamente curta e o ganho de produtividade é bem alto, devido ao fato de poder escrever menos e fazer mais. A sua licença de uso foi um dos motivos envolvidos na sua escolha, apesar de não ser licenciado através da GPL, e sim da PSF – Python Software Foundation, sua licença é compatível com a GPL. A PSF – Python Software Foundation, é quem mantém e protege os direitos de propriedade intelectual do Python e também que financia o seu desenvolvimento.

1.1.3

Python-Ogre – A ponte entre o Ogre e o Python

17

Considerando que por padrão o Ogre utiliza a linguagem de programação C++, suas bibliotecas não são compatíveis com o Python. Para que o desenvolvimento seja possível, é necessário que a biblioteca do Ogre seja mapeada para o Python, através dos bidings, que servem de interface da biblioteca original, de forma que permita usar todas as funcionalidades do Ogre dentro do Python. O Python-Ogre pode ser considerado uma suíte completa para o desenvolvimento de jogos no Python, pois além de possuir os bidings para o Ogre, ele vem com os bindings para todas as demais ferramentas que integram o ambiente de desenvolvimento do Ogre, como OpenAL, ODE e Newt (para física) e CEGUI. O Python-Ogre é disponível tanto para Linux, Windows e OS X. Porém para o Linux, a equipe de desenvolvimento não disponibilizou um pacote pré-compilado, fazendo com que a instalação no Linux requeira a aquisição de diversos programas através de CVS e Subversion e a conseqüente compilação da ferramenta em si. Enquanto não é disponibilizado um pacote pré-compilado para o Linux, será coberto o Python-Ogre no Windows. Para esta plataforma foi disponibilizado um pacote précompilado de fácil instalação.

1.1.4

CEGUI – Interface Gráfica com o Usuário

Além de todo o ambiente gráfico que o Ogre fornece, é necessário ainda diversas ferramentas para que o desenvolvimento da aplicação seja completa, e isto também se diz a interface interna a ser utilizada dentro da aplicação, dentro do ambiente gráfico tridimensional. CEGUI – Crazy Eddie's GUI System, irá ser a ferramenta que irá prover esta funcionalidade, sua definição, extraída do site:
Crazy Eddie's GUI System é uma biblioteca gratuita que provê janelas e controlas para API's gráficas e/ou motores onde que tal funcionalidade não é nativamente disponível, ou que simplesmente não tenha presença. Esta biblioteca é orientada a objetos e foi escrita em C++, e que possui como alvo desenvolvedores de jogos, o qual deve ter o seu tempo consumido pelo desenvolvimento do jogo, e não construindo sub sistemas de interface gráfica com o usuário! (CEGUI, 2007, Tradução Nossa)

18

O seu principal propósito é prover uma interface gráfica com janelas, botões, caixas de texto, opções, listas entre outros em um ambiente no qual não fornece uma interface gráfica para o usuário. Também é possível personalizar o CEGUI através de skins, onde pode ser alterado a sua aparência de forma que possa encaixar visualmente com um layout que condiz a interface da aplicação. CEGUI está disponível através da licença de uso MIT.

1.1.5

OpenAL – Mecanismo de Áudio

OpenAL é uma API de áudio 3D multi-plataforma, apropriada para o uso com jogos e outros tipos de aplicações que necessitam de áudio. O OpenAL é licenciado através da LGPL para os dispositivos de áudio genéricos, e através da licença Creative Labs Inc, para os dispositivos de áudio da Creative (como por exemplo a Audigity e a X-Fi). O motivo do uso do OpenAL detrimento de outros mecanismos de áudio, como Python Multimedia Services, Snack Sound Toolkitk e Pygame, é pelo fato dos demais não serem capaz de trabalhar com um sistema de áudio 3D, sendo apenas um mecanismo simples de áudio playback.

1.1.6

Dinâmica com OgreNewt

Além da visualização gráfica, é necessário que os objetos na tela tenham algum tipo de consistência, que não sejam apenas “fantasmas” no qual um objeto pode transpassar o outro. Para isto é necessário o uso de um sistema que seja capaz de manipular corpos rígidos, onde objetos podem colidir entre si, causar fricção com uma superfície e também sendo possível trabalhar com junções, onde pode-se criar complexos mecanismos como engrenagens, suspensões, dobradiças e esqueletos. Existem diversos sistemas que trabalham com dinâmica de corpos rígidos. Para

19 esta pesquisa foi selecionado o Newton, pelo fato da sua integração com o Ogre, permitindo escrever aplicações com simulações físicas de maneira mais simples e prática, além de atender os requisitos da licença de uso, podendo ser distribuído tanto via LGPL ou BSD. Newton Dynamics – é um simulador de corpos rígidos, de código fechado, porém livre para o uso comercial e não comercial, estável, maduro e independe da plataforma a ser usada. Características necessárias para atender este projeto. Newton possui um avançado sistema de junções e detecção de colisões, através de um sistema de callbacks, ideal para ser utilizado em jogos ou em simuladores onde seja necessário o uso de um sistema de corpos rígidos.

1.1.7

Blender – Estúdio de modelagem e animação 3D

Blender é um estúdio de modelagem e animação em três dimensões, profissional, gratuito e com o código fonte aberto. O Blender foi concebido dentro do estúdio alemão de animação NeoGeo por Ton Roosendaal. Que logo em seguida separou-se da Neo Geo para abrir sua própria empresa, a NaN (Not a Number), disponibilizando o Blender de duas formas, uma gratuita e com menos recursos e outra completa e atualizada através de uma licença paga. Porém devido às oportunidades e capacidade de negócios, a NaN veio a falir em Outubro de 2001. Então em 2002 Ton Roosendaal criou a fundação sem fins lucrativos chamada Blender Foundation, que teve como objetivo ressusitar o Blender, comprando o seu código fonte através de dinheiro recebido por doações. Hoje o Blender esta disponível sob a licença GPL e o seu desenvolvimento é contínuo. No Blender é possível realizar a modelagem tridimensional de objetos, cenários, personagens, além da definição do material (cores e texturas), montagem de esqueletos (kinematics) e realização de animações e renderizações. Neste software serão modelados os objetos que irão ser utilizados neste projeto de pesquisa. Além disto, será utilizado também um utilitário chamado blender-ogrexml, que tem como função exportar os objetos modelados no Blender para que seja utilizado no Ogre e também um exportador de cenário, para que este seja facilmente carregado dentro da aplicação.

20

1.1.8

Integração entre Blender e Ogre - Ferramentas de conversão

Para que seja possível utilizar os objetos e cenas modelados no Blender, dentro do Ogre, algumas ferramentas básicas de conversão devem ser usadas para que esta integração seja possível. Para esta demonstração serão utilizadas duas ferramentas: Ogre Meshes Exporter - plugin para ser utilizado dentro do Blender, que realiza a exportação de objetos de dentro do Blender para arquivos no formato que o Ogre possa carregar. É feita a exportação dos objetos malhas, com múltiplos materiais e texturas, esqueletos e animações. Existe uma página com um guia de como utilizá-lo em: http://ogre.cvs.sourceforge.net/*checkout*/ogre/ogrenew/Tools/BlenderExport/ogrehelp/ogre meshesexporter.html Blender dotScene Exporter - script que realiza a exportação dos cenários do Blender para arquivos XML no formato definido pelo projeto dotScene Format. Este script exporta os dados de posição de objetos como, malhas, câmeras, luzes, névoa entre outros. Estes objetos devem ser exportados previamente com o Ogre Meshes Exporter para que possa ser processado pelo parser do dotScene.

1.2

Configuração do ambiente de desenvolvimento

Para que seja possível desenvolver as aplicações, é necessário que o ambiente de desenvolvimento esteja devidamente configurado e funcionando. A abordagem desta monografia será para o ambiente Windows, devido a não disponibilização do pacote binário do Python-Ogre para o Linux. Porém o código escrito poderá ser usado em ambas plataformas.

21

1.2.1

Instalando as ferramentas

Neste ponto será abordado o processo de configuração e teste do ambiente de desenvolvimento, desde buscar a ferramenta pela internet, a sua instalação e a execução de um teste para avaliar se a instalação foi bem sucedida.

1.2.2

Python

A versão a ser utilizada será o Python 2.4.4, lançado em 18 de Outubro de 2006, apesar da última versão disponível ser a 2.5.1, será usada a versão 2.4.4 devido a compatibilidade com o Blender, para que seja possível utilizar os scripts de exportação para o Ogre. Para instalar, aponte o navegador para a página de downloads do Python (http://www.python.org/download/releases/2.4.4/ ) e baixe o arquivo python-2.4.4.msi, versão para processadores x86 (Pentium, Atlhon, Duron, Semprom). Este pacote contém o instalador do Python sem o código fonte.

1.2.3

wxPython

wxPython é um GUI (Graphical User Interface) toolkit, para o Python. Com o wxPython, é possível criar programas em Python que requerem a construção de uma interface gráfica. Neste projeto o wxPython é pré-requisito para que possa ser executado a IDE SPE. O wxPython esta disponível na página http://www.wxpython.org, na seção Download, no link Binaries, onde deverá ser baixado para o Microsoft Windows, o wxPython Runtime para a versão do Python 2.4. Porém existem duas versões que podem ser adquiridas, a versão Unicode e a versão ANSI. Recomenda-se pegar a versão Unicode, se a versão do Windows for

22 XP. Usuários de Windows 98/ME devem pegar o ANSI. Uma vez baixado, basta executar o programa de instalação e seguir os passos, que o wxPyhton estará instalado no sistema.

1.2.4

RapidSVN

RapidSVN é um software para controle de versão de arquivos utilizando Subversion. Este software deverá ser instalado para realizar downloads de outros programas que se encontram em repositórios. Além disto, é extremamente útil para gerenciar versões dos programas escritos. O RapidSVN pode ser baixado através do endereco http://rapidsvn.tigris.org, onde que será informardo qual é a última versão disponível, atualmente a versão é 0.9.4. Seguindo este link, será direcionado para uma listagem de arquivos, que deverá ser baixado o RapidSVN-0.9.4.exe, com tamanho aproximado de 3.8 MB. Uma vez com o arquivo recebido, ele deverá ser executado para que possa ser instalado no sistema, bastando seguir os passos do programa de instalação e o RapidSVN estará instalado corretamente. Quando instalado, ele estará disponível no menu Iniciar, Programas, RapidSVN-0.9.4, RapidSVN.

1.2.5

Python IDE

IDE é uma sigla para Integrated Development Enviroment. A IDE é o ambiente principal de desenvolvimento, onde os programas serão editados. Para o Python, existe diversas IDE's, entre elas o Eclipse (através do plugin PyDev), Eric4, SPE, BOA, uma lista completa de IDE para o Python está disponível no endereço A http://en.wikipedia.org/wiki/List_of_integrated_development_environments_for_Python.

questão de qual IDE usar, ficará por conta do desenvolvedor. Para esta monografia, será utilizado a IDE SPE, que é Open Source e Multiplataforma. O SPE deverá ser baixado através do repositório SVN, com a ferramenta RapidSVN. Dentro do RapidSVN, vá no menu

23 Repositório, Checkout. Será aberta uma janela como abaixo:

Ilustração 1: Janela do RapidSVN com a caixa de diálogo de Checkout aberta. Fonte: Fonte Nossa.

No

campo

URL,

deverá

ser

informado

o

endereço

à

seguir:

svn://svn.berlios.de/python/spe/trunk/_spe, que é o endereço do repositório do SPE. Esta URL aponta para o trunk, que é sempre a última versão do SPE. A Pasta de Destino poderá ter qualquer caminho, contanto que a pasta que irá conter o SPE seja _spe. Neste caso, iremos usar C:\_spe. Ao confirmar, o RapidSVN irá realizar a transferência de arquivos do repositório para o caminho informado. Para executar o SPE, basta dar um duplo clique no arquivo SPE.py, que ele será executado pelo Python e a janela do SPE se abrirá na tela.

24

Ilustração 2: SPE sendo executado, com um novo arquivo aberto. Fonte: Fonte Nossa.

1.2.6

DirectX

Como o Ogre aceita o DirectX como renderizador, este deverá ser atualizado para a versão mais recente do DirectX 9c. Esta atualização poderá ser feita utilizando o Windows Update, ou indo à página da Microsoft para fazer a atualização, ou então pegando a atualização em algum site de downloads, como Superdownloads em http://www.superdownloads.com.br ou Tucows em http://www.tucows.com.br.

25 1.2.7 Python-Ogre

Para instalar o Python-Ogre, basta ir à pagina http://python-ogre.org e realizar o download do Python-Ogre para Windows. Uma vez instalado, não será preciso instalar nenhum outro subsistema, pois o Python-Ogre já possui todos os subsistemas já incorporados. Após o Python-Ogre instalado, devera ser feito um teste executando qualquer um dos demos que o acompanha. É importante lembrar que caso for usar o DirectX, tenha a última versão instalada do DirectX 9c.

1.2.8

Blender

Aponte o navegador para o endereço http://www.blender.org, vá para a seção de downloads e baixe a última versão do Blender para o Windows. A versão utilizada neste projeto será a 2.43. Uma vez baixado, basta executar o instalador e o software estará instalado. Quando o Blender for executado, veja se no console dele apresente algum erro de importação do Python. Se apresentar algum erro, será devido que as variáveis de ambiente do Pyhton não foram corretamente configuradas no sistema. Para configurar, siga o procedimento a seguir. Vá em propriedades do computador, na guia Avançado, e no botão Variáveis de Ambiente. Caso não esteja criado, crie uma Variável do Sistema, chamada PYTHONPATH com o seguinte caminho: C:\PYTHON24;C:\PYTHON24\DLLS;C:\PYTHON24\LIB;C:\PYTHON24\LIB\ LIB-TK. Ajustando corretamente para o caminho do Python instalado no seu sistema. Uma vez configurado, execute novamente o Blender e certifique-se que no console não esteja exibindo nenhum erro de import.

26

Ilustração 3: Janela principal do Blender Fonte: Fonte Nossa.

1.2.9

Exportador para o Ogre

Estes dois scripts são escritos em Python e serão executados apartir do Blender, então é importante que o Python esteja corretamente configurado no sistema para que os scripts possam ser executados. Ogre Meshes Exporter esta disponível através do CVS em http://www.ogre3d.org/wiki/index.php/OGRE_Meshes_Exporter o Blender Scene Exporter, também via CVS em http://ogre.cvs.sourceforge.net/ogre/ogreaddons/blendersceneexporter/. Para instalar ambos scripts, os arquivos deverão ser copiados para o diretório C:\Arquivos de programas\Blender Foundation\Blender\.blender\scripts, e em seguida executar o Blender e ir no Menu Scripts e em Update Menus, e deverá aparecer a opção para exportar no menu de exportação.

27

1.3

Testando o ambiente de desenvolvimento

Uma vez com estas ferramentas instaladas, será possível realizar os testes para verificar se o ambiente de desenvolvimento foi corretamente configurado. Ao instalar o Python-Ogre, são instalados diversas demonstrações de funcionamento referente ao Ogre, Física e Som. Basta executar qualquer um destes demos para verificar se os softwares foram instalados corretamente. Caso ocorra algum erro referente a uma biblioteca do DirectX que esteja faltando, a d3dx9_3x.dll, certifique-se se o DirectX foi instalado e atualizado. Se aparecer algo na tela demonstrando o uso do Ogre, então o ambiente foi corretamente configurado e esta pronto para o desenvolvimento da aplicação. Caso ocorra algum erro, verifique os passos tomados e veja se algo não foi deixado para traz.

1.4

Configurando o Projeto Inicial

Antes de iniciar a construção do aplicativo de demonstração, deverá ser feita a configuração do projeto como diretórios, arquivos de configuração, tudo que irá ser necessário para a confecção do projeto de demonstração. O projeto será criado dentro do diretório de demonstração do Python-Ogre, então deverá ser criado o diretório C:\PythonOgre\demos\techDemo\, e então os arquivos de configuração do Ogre deverão ser criados dentro deste diretório. Estes arquivos se encontram como anexo nesta monografia.
dotScene.py; Scene.xml; resource.cfg plugins.cfg

28

O desenvolvimento de uma aplicação que utilizam estes recursos, possui uma estrutura básica de funcionamento, onde que deverá instanciar as classes bases (Ogre, Newton, OIS, CEGUI) e disponibilizá-las para o seu uso posterior, configurar os recursos iniciais, criar objetos e por fim iniciar a simulação. O que será demonstrado abaixo é o esqueleto que a aplicação deverá ter, para que possam ser implementados nestes métodos, os códigos que serão vistos adiante.

import ogre.renderer.OGRE as ogre class TechDemo: def __init__( self ): """ metodo constructor, nele serao declarados os atributos principais a seremutilizados pela aplicacao. """ def __del__( self ): """ metodo destructor. Ao finalizar a classe, será neste ponto que os recursos alocados previamente, serao finalizados """ def run( self ): """ Metodo que inicia a simulacao. Neste ponto serao ativados todos os sistema e tambem serao configurados todos os recursos """ """ Ponto de execucao do codigo principal. """ if __name__ == "__main__": try: # criando a classe principal td = TechDemo( ) # excecutando a aplicacao td.run( ) except ogre.OgreException, e: print str(e)

Código basico do TechDemo

29

2

OGRE3D

2.1

Configurando e Iniciando o Ogre

De forma a desenvolver uma aplicação utilizando o Ogre3D, deve-se entender primeiro como que o programa deverá comportar para que seja atendido todas as etapas de inicialização e preparação do ambiente em tempo de execução. Esta etapa se consiste em iniciar o Ogre, configurar os recursos, configurar as variáveis de ambiente e então iniciar a aplicação em si. Com base nesta estrutura, serão feitas implementações no esqueleto básico, para a aplicação então tomar forma. A inicialização do Ogre se dá através da chamada do método ogre.Root( ), que retorna uma instância para o objeto base do Ogre, que é um Singleton2, sobre o qual serão executadas todas as operações e deverá ser obrigatoriamente criando antes de qualquer chamada ao Ogre. Este construtor recebe três parâmetros: pluginFile - indica o caminho para o arquivo de configuração, indicando aonde que os plugins do Ogre se localizam; configFile - aponta para o arquivo de configuração da janela de configuração da renderização; logFile - indica para qual deverá ser o arquivo de log a ser usado pelo Ogre; Em seguida deverá ser realizada a configuração dos parâmetros para que o renderizador do Ogre possa ser iniciado, através da chamada ao método root.initialise( ). Este método tem um parâmetro boolean que serve para indicar que o Ogre inicie automaticamente a janela de renderização. Para informar o Ogre os parâmetros de inicialização, será utilizado o método self.root.showConfigDialog( ), que irá exibir uma caixa de diálogo para o usuário, solicitando que informe o renderizador, resolução entre demais outras informações. Se o usuário pressionar OK, a aplicação então irá iniciar o Ogre com root.initialise( ) e prosseguirá com a execução da aplicação, caso pressionar Cancel, então a aplicação terminará. 2 Singleton é um padrão de projeto no qual permite a existência de apenas uma instância da classe,
disponibilizando um método publico para buscar a instância desta classe

30
def setupOgre( self ): ## instanciando o Ogre self.root = ogre.Root( "plugins.cfg", "ogre.cfg", "ogre.log" ) if self.root.showConfigDialog( ): ## Botao OK pressionado, inciando o Ogre self.janela = self.root.initialise(True, "TechDemo") return True else: ## Botão de cancelar pressionado, a aplicação irá sair return False

Definição do método setupOgre( ),iniciando o sistema

A chamada a este método, deverá ficar dentro de run( ). Se ocorrer alguma falha na inicialização do Ogre, o método setupOgre( ) retornará falso e a aplicação não deverá prosseguir.

if not self.setupOgre( ): return False

Realizando a chamada ao método setupOgre( ), no método run( ).

Uma boa prática no desenvolvimento de uma aplicação estável, sempre que alocar um recurso (como instanciar uma classe a um objeto), ao término da aplicação, estes objetos deverão ser desalocados na memoria, que deverá ficar no método __del__( ) (desconstrutor) da classe.

def __del__( self ): del self.janela del self.root

Implementação da destruição dos objetos no desconstrutor da classe.

Após a implementação deste código, a aplicação poderá ser executada, e deverá ter a saída conforme a tela abaixo, que é a janela de configuração do Ogre, onde poderá ser informado os parâmetros de configuração do Ogre.

31

Ilustração 4: Janela de configuração do Ogre Fonte: Fonte Nossa.

A criação do SceneManager é um processo importante na inicialização do Ogre, o SceneManager é o cenário em si, é nele que serão anexados as entidades e objetos que irão ser exibidos em tela. Existem diversos tipos de SceneManager, mas por padrão o SceneManager que será criado é o genérico (ST_GENERIC). O SceneManager deverá ser criado antes de qualquer operação que envolva a criação de objetos e entidade e uma vez criado, este deverá estar disponível para toda a aplicação para que possa ser trabalhado. Definir uma estratégia de quando criar o SceneManager é importante, já que dependerá da aplicação o momento que ela deverá existir. Para implementar os códigos seguintes, deverá primeiro criar um método chamado configuracaoVisual( ), onde será criado o sceneManger principal da aplicação.

def configuracaoVisual( self ): """ Método que realiza a configuração básica referente à estrutura visual do Ogre, como a criação de SceneManager """ self.sceneManager = self.root.createSceneManager( ogre.ST_GENERIC )

32
Definição do método configuracaoVisual( ) juntamente com a criação do SceneManager

Apesar que nesse ponto já não é obrigatório, pode criar a também a câmera. A câmera é o ponto de vista do observador. Como dito anteriormente, todos os objetos criados no Ogre devem repousar no SceneManager, e isto é válido para câmeras também. Para criá-la, deverá ser chamado o método createCamera( ).

self.camera = self.sceneManager.createCamera( "playerCam" )

criação da câmera

É obrigatório informar o nome da câmera, normalmente adota-se por padrão "playerCam" ou "defaultCam", retornando o objeto câmera. Uma vez criada, ela deverá ser ajustada para ter uma visualização adequada do ambiente, indicando aonde que se localizará no espaço, informando o vetor de posição espacial (x, y, z) no método self.camera.setPosition( ). Depois deve-se indicar para qual lado que ela irá observar chamando o método self.camera.lookAt( ), passando também um vetor de posição espacial. Além disto, deverá indicar o ponto que o mundo irá começar a ser renderizado a partir de sua posição, através do atributo self.camera.nearClipDistance. E como parte da configuração inicial da câmera, deverá ser configurado também o viewport, que é o ponto que realiza a conexão da câmera com a janela de visualização. E além disto é possível configurar a cor de fundo do viewport pelo atributo backgroundColour( ).

33

Ilustração 5: Câmera Frustrum, os elementos principais que compõe a câmera. Fonte: Pro Ogre3D Programming, 2007, Cap. 4, p. 67

Câmera Frustrum, é a pirâmide de visualização da câmera, seccionada no topo. O plano XY é o nearClipDistance, será a partir dele que o mundo começará a ser renderizado, e o plano X'Y', é o farClipDistance, que será até onde o mundo será renderizado. Figura extraída do livro Pro Ogre3D Programming. O código abaixo deverá ser implementado logo após a criação da câmera:

self.camera.setPosition( (0, 50, 200) ) self.camera.lookAt((0, -300, -500)) self.camera.nearClipDistance = 5 self.viewport = self.janela.addViewport( self.camera ) self.viewport.backgroundColour = (0, 0, 0)

Configuração dos parâmetros da câmera

Implementação da chamada ao método configuracaoVisual( ) dentro do método run( ). Esta chamada tem de ser feita após ao setupOgre( ).

self.configuracaoVisual( )

Chamada ao método configuracaoVisual( )

Neste ponto a configuração visual esta completa, com o SceneManager, Câmera e Viewport definidos. Porém ainda falta realizar a configuração dos recursos, que se localizam

34 dentro do resources.cfg. Isto serve para informar aonde que o Ogre deverá procurar os arquivos que deverão ser carregados, e deverá ser implementado na classe principal.

def configuraRecursos( self ): """ Metodo para realizar a configuracao dos recursos, a partir do arquivo resources.cfg, os caminhos que o Ogre devera procurar por arquivos. """ print 'Configurando recursos...' f = open( 'resources.cfg' ,'r' ).read( ).splitlines( ) # abrindo o arquivo em linhas for l in f: # para cada linha... l = l.strip( ) if len( l ) > 0: # verificando se e nao e uma linha vazia if not l[ 0 ] == '#': if "#" in l: l = l.split( '#' )[ 0 ].strip( ) if '[' in l: # verificando se e uma secao section = l.strip( '[]' ) print "\t" + section else: # se nao for comentario ou secao, entao e uma linha de configuracao key, path = l.split( '=' )   # entao adicionando ao Ogre como configuracao de recurso ogre.ResourceGroupManager.getSingleton( ) .addResourceLocation( path, key, section ) print 'Recursos configurados.'

Método para carregar os recursos a partir do arquivo resources.cfg.

Este método é um parser simples de arquivo de configuração do Ogre, ele realiza a leitura do arquivo, localiza as seções de configuração, as chaves e os caminhos. Todos estes recursos são informados ao Ogre através do ResourceGroupManager, que é um Singleton que tem como fim gerenciar os grupos de recursos e realizar notificações de carregamento e descarregamento dos recursos no grupo. Ele utiliza o método addResourceLocation, que recebe três parâmetros: nome - que é o nome do recurso, pode ser um diretório, um arquivo zip, uma URL; tipoRecurso - qual tipo do recurso que será adicionado, pode ser FileSystem ou Zip; grupoRecurso - informa para qual grupo de recurso que deverá ser carregado; A chamada deverá feita dentro do método run( ), após a configuração visual. E

• • •

35 após a definição dos recursos, deverá em seguida inicializar todos os grupos de recursos utilizando o método initialiseAllResourceGroups( ), conforme código abaixo.

self.configuraRecursos( ) ogre.ResourceGroupManager.getSingleton().initialiseAllResourceGroups( )

Execução do método configuraRecursos( ) e iniciando os grupos de recursos.

Com o Ogre iniciado, toda estrutura básica de configuração do ambiente realizada, agora deverá partir para a fase final do processo de inicialização do Ogre. Esta fase é a qual dará início ao funcionamento da aplicação, que é informar ao Ogre que inicie o processo de renderização. Existe duas formas de realizar este procedimento. A primeira forma é realizar a seguinte chamada (que deverá ser implementada após o código anterior):

self.root.startRendering( )

Iniciando o loop principal de renderização.

O método root.startRendering( ), inicia o processo automático de renderização da cena e não irá finalizar enquanto não receber uma ordem de parada. O Ogre realizará uma chamada callback a todas as classes FrameListeners registradas através do método addFrameListener( ), para cada frame renderizado. Os FrameListeners serão vistos a seguir. E partir destas classes, este processo poderá ser parado. A segunda forma é criar um loop infinito realizando a chamada de renderização de um frame manualmente, conforme a seguir:

while self.root.renderOneFrame(): <código>

Uma outra froma de realizar o loop de renderização.

Desta forma o desenvolvedor poderá realizar um controle mais personalizado de como que o Ogre deverá comportar. E deste ponto uma questão é levantada: qual método de renderização deverá ser utilizado? A resposta encontra-se na documentação da API do Ogre:

36
Usuários da biblioteca do OGRE não são obrigados a usar o método loop de renderização automático . Isto se dá por uma conveniência e é muito útil para aplicações que precisam de alta taxa de quadros, como jogos. Para aplicações que não necessitam de atualizar constantemente os quadros (por exemplo, um editor), é melhor atualizar manualmente os quadros quando requerido, fazendo uma chamada ao método RenderTarget::update, ou se você quiser executar seu próprio loop de renderização, pode atualizar todos os quadros por demanda, usando Root::renderOneFrame. Isto libera a CPU para fazer outras coisas entre as atualizações, caso a taxa de quadros seja menos importante. (Ogre3D, 2007, Tradução Nossa)

A resposta esta em qual fim que a aplicação terá, e para esta demonstração será utilizado o loop de renderização automática, pois há a necessidade de uma alta taxa de quadros. Após todas as implementações até neste ponto, existirá uma aplicação muito simples, que abstrai todo o processo básico de inicialização do Ogre. Ao executar este código, deverá aparecer uma janela de configuração do Ogre aguardando para iniciar a aplicação. Confirmando em "Ok", uma janela preta deverá aparecer sem acontecer nada, e não será possível fechá-la, pois como foi iniciado o processo de loop infinito do Ogre, a aplicação deverá ser fechada de maneira forçada. Uma vez quando o método startRendering é chamado, ele não irá parar nunca por si só, um código registrado através do frameListener deverá ser chamado para fazer com que o loop principal seja finalizado. Os FrameListener serão vistos a seguir.

37

Ilustração 6: Janela de renderização do Ogre. Fonte: Fonte Nossa.

Pode-se concluir, que o processo de inicialização do Ogre se consiste de diversas etapas básicas, como inicialização do Ogre, configuração de recursos, inicialização do renderizador, abrir a janela de renderização, criar o SceneManager, criar uma câmera, definir um viewport, criar cenários, registrar frameListeners e iniciar o processo de renderização. Para uma aplicação de grande porte, estas etapas deverão ser separadas preferencialmente em métodos distintos, de forma de realizar o isolamento destas fases. Este será o código básico para a demonstração, devendo retornar nele diversas vezes para adicionar mais funcionalidades de forma que possa a prosseguir o com desenvolvimento da demonstração.

2.2

Frame Listener: Princípios Gerais

FrameListener a forma da aplicação poder interagir com com o loop de

38 renderização do Ogre. Cada vez que o Ogre executa um loop, ele chama o método frameStarted( ) da classe FrameListener ao início da renderização do frame, e frameEnded( ) ao término da renderização. Com estes métodos pode-se fazer o que bem entender com o Ogre, e eles devem retornar um valor booleano, sendo que ao retornar falso, o loop automático será finalizado. Para receber estas notificações de eventos de renderização do Ogre, o método addFrameListener deverá ser chamado, passando como parâmetro a instância de uma classe FrameListener que irá executar as operações. Será criado abaixo, uma classe básica para ser o FrameListener desta demonstração. Nesta classe será implementada gradativamente novos códigos afim de produzir uma aplicação funcional.

class BasicoListener( ogre.FrameListener ): def frameStarted( self, frameEvent ): return True;

Declaração da classe BasicoListener.

E antes de chamar o método startRendering( ), deverá ser criado um método que será responsável por iniciar e alocar todos os FrameListener da aplicação. Este método se chamará configuraFrameListener( ), e a sua chamada deverá ser realizada imediatamente antes de executar o comando ogre.startRendering( ).

def configuraFrameListener( self ): """ Metodo para realizar a configuracao e alocacao dos frameListener da aplicacao. """ self.basicoListener = BasicoListener( ) self.root.addFrameListener( self.basicoListener )

Implementação do método configuraFrameListener( ).

Primeiramente foi criado uma classe listener básica que é um FrameListener. Esta classe será responsável por gerenciar uma determinada atividade para o FrameListener, dentro dela poderá conter todo o código necessário para realizar o seu propósito. Foi também adicionado o método frameStarted que irá ser chamado quando o quadro começar a ser renderizado. Apenas para servir de demonstração, este método irá retornar falso, para fazer com que o loop de renderização seja finalizado imediatamente. Note que existe um parâmetro

39 chamado frameEvent. Este parâmetro representa a classe FrameEvent, que possui o atributo timeSinceLastEvent, que informa o tempo em segundos decorrido desde o último evento (que pode ser o tempo entre o frame inicial e o frame final ou o frame final e o frame inicial) e o atributo timeSinceLastFrame, que informa o tempo em segundos decorrido desde o último evento semelhante. Em seguida deverá ser feita a configuração do FrameListener, para que este possa responder às chamadas de evento do Ogre. Deverá existir uma instância para a classe listener, no caso, BasicoListener e então adicionar esta instância com o método addFrameListener. Note que é possível adicionar quantos listener que for necessário. Como resultado, ao executar a aplicação, ela iniciará e só poderá ser fechada de forma forçada, pois o frameStarted retorna sempre verdadeiro e fez com que o loop de renderização fique executando indefinidamente. À seguir será implementado uma forma que a renderização possa ser encerrada com a tecla ESC.

A aplicação precisa fazer acontecer, para que os resultados possam ser visualizados na tela. Estes processos irão ocorrer dentro dos FrameListeners, e esta é a sua importância. Neste ponto, os FrameListeners foram vistos superficialmente, de forma que possam ser compreendidos, pois adiante serão vistos mais funcionalidades referente a eles, como o unbuffered e buffered input, e um conhecimento básico sobre listeners será necessário.

2.3

Gerenciamento de Dispositivos de Entrada: Abordagem superficial ao OIS

O Ogre possui um sistema básico para trabalhar com o teclado, que é um procedimento bastante simples e rudimentar. Porém o sistema de leitura buffered do Ogre não é consistente e existem diversas limitações quanto ao seu uso, por exemplo, não é possível utilizar joysticks. O motivo básico para isto é devido ao fato do Ogre ser exclusivamente um motor gráfico, e não um sistema composto. Isto não é motivo de preocupação, pois existem outras formas de lidar com este problema, e será visto adiante o OIS (Object Oriented Input System). Neste projeto não será visto o sistema de entrada padrão do Ogre. No seu lugar será utilizado diretamente o OIS.

40 Neste momento o OIS será abordado superficialmente, apenas para atender o que diz a inicialização e configuração dos dispositivos de entrada. Mais à frente será feita uma cobertura mais abrangente sobre o OIS. Nesta demonstração será feita uma classe que será responsável por gerenciar e responder aos eventos vindos dos dispositivos de entrada, como teclado, mouse e joystick. Primeiramente deverá ser feito import da biblioteca do OIS:

import ogre.io.OIS as OIS

Importação do OIS.

Para então implementar a classe a seguir:

class InputListener( ogre.FrameListener ): def __init__( self, window ): ogre.FrameListener.__init__(self) janelaHnd = window.getCustomAttributeInt( "WINDOW" ) self.InputManager = OIS.createPythonInputSystem( [ ( "WINDOW", str( janelaHnd ) ) ] ) self.Keyboard = self.InputManager.createInputObjectKeyboard( OIS.OISKeyboard, False ) def __del__( self ): print 'liberando recursos do InputListener' del self.InputManager del self.Keyboard def frameStarted( self, frameEvent ): self.Keyboard.capture( ) if( self.Keyboard.isKeyDown( OIS.KC_ESCAPE ) ): return False return True

Implementação da classe InputListener.

Esta classe ficará como responsável por gerenciar o recurso dos dispositivos de entrada, como teclado, mouse e joystick. Ela esta dividida em três partes, inicialização, liberação de recursos e processamento dos eventos (em frameStarted). Será explicado linha a linha o funcionamento da classe. O construtor recebe como parâmetro o window, o objeto que contém a janela de renderização na qual os eventos do teclado serão lidos. Como a classe herda do ogre.FrameListener, logo, antes de realizar qualquer coisa, tem de chamar o

41 construtor da classe pai. Em seguida, deve-se buscar no objeto window, o handle, que é um código que identifica a janela. Então é feita a inicialização do OIS, passando como parâmetro uma lista que irá conter WINDOW e o handle da janela, de forma a escutar os dispositivos vindo dela. Em seguida vem a parte que cria o objeto que irá ser responsável por processar os eventos do teclado. O primeiro parâmetro é a referência para classe que irá ser o tipo do dispositivo a ser lido, e o segundo parâmetro é o buffered, que informa ao OIS se a leitura deverá ser buffered ou unbuffered. Ao criar esta classe, teremos o objeto Keyboard, que irá representar o teclado. Em __del__ simplesmente deve-se liberar os recursos que utilizados, que é o InputManager e o Keyboard. Em FrameStarted, vem o processo de leitura do teclado, executado através do método self.Keyboard.capture( ), que realiza a captura do buffer do teclado, isto é obrigatório, pois se não o fizer, o teclado não será lido. Após isto, faz-se uma verificação para saber se uma tecla esta pressionada, através do método isKeyDown, informando qual tecla deseja-se saber seu status, no caso, a tecla ESCAPE. Se ela estiver pressionada, então irá retornar Falso e encerrar a aplicação. Caso contrário, o frameStarted irá retornar verdadeiro e a aplicação continuará a ser executada. Porém antes de executar a aplicação, deve-se colocar o InputListener em um FrameListener. O código abaixo deverá ser implementado dentro do método configuraFrameListener( ).

self.inputListener = InputListener( self.janela ) self.root.addFrameListener( self.inputListener )

Instanciando a classe InputListener e atribuindo a instância como frameListener.

Com a classe criada e com o FrameListener configurado, agora a aplicação poderá ser executada. Será novamente uma janela preta, porém com a diferença que agora ela pode ser encerrada pressionando a tecla ESCAPE.

Neste ponto foi visto como funciona um sistema básico de entrada, apenas para podermos fazer com que nossa aplicação seja encerrada pressionando a tecla ESCAPE, mas para isto foi necessário realizar uma configuração básica do OIS para que a aplicação passe a responder pelos eventos do teclado. Na classe InputListener criada, de forma progressiva,

42 iremos implementar o tratamento das teclas da aplicação. Esta foi uma abordagem superficial do OIS, o qual irá ser visto mais à fundo, e será coberto mais aplicações para o teclado e para o mouse.

2.4

Criando Cenários

O cenário é um conjunto de informações, como a estrutura de malhas que o compõe, os seus atores, luzes, câmeras, entre vários outros elementos. Para que ele possa ser criado, deverá ser todo modelado em um software de modelagem 3D, que neste projeto, será utilizado o Blender. Neste processo deverão ser modelados todos os objetos e suas malhas deverão ser exportadas para arquivos distintos utilizando o script Ogre Meshes Exporter, e o cenário deverá ser exportado no final utilizando o script Blender dotScene Exporter. O processo de criação do cenário é um trabalho de escape artístico, onde que pessoas com conhecimento em modelagem e texturização irão criar cuidadosamente o cenário para que possa ficar atrativo e funcional, e isto não será coberto neste projeto. Será explicado mais à frente como utilizar estes scripts para exportar objetos e cenários, no entanto será demonstrado apenas os procedimentos para carregar o cenário dentro do Ogre, partindo de um dotScene e modelos já existentes. Até o momento, existe apenas uma aplicação que aparece uma tela preta, a qual pode ser fechada pressionando a tecla ESCAPE, porém não existe nenhum objeto carregado para dentro do sistema. E será isto que este item irá cobrir, o carregamento de objetos e a confecção de um cenário dentro do Ogre, onde que preencherá a aplicação com um conteúdo visual. Para falar sobre este assunto, é necessário dizer como é criado um cenário e como ele deverá ser carregado para dentro do Ogre, além de falar sobre como funciona a estrutura SceneManager, cuja sua criação foi vista anteriormente.

2.5

A Estrutura do SceneManager

43

O SceneManager é o gerenciador de todos os elementos visuais, tudo que aparecerá na tela, o SceneManager irá manter o seu controle. Porém os objetos não podem ser colocados diretamente no SceneManager, existe uma subdivisão a qual os objetos deverão ser anexados, que é o SceneNode. É esta classe que manterá o controle de localização dos objetos a ele anexados. Ao criar um objeto, ele não será exibido na tela enquanto não estiver anexado a um SceneNode. É possível também anexar ao SceneNode outros SceneNodes, criando assim uma árvore, uma estrutura hierárquica, de SceneNodes. É extremamente importante lembrar que a posição dos objetos a ele anexados, são relativas ao SceneNode em questão, e não ao mundo. Exite mais um conceito a observar, que são as Entidades. Entidade é um tipo de objeto que pode ser renderizado em uma cena, e que é representada por uma estrutura de malha 3D, porém luz, partículas, câmeras e billboards (renderizações de imagens 2D), não são entidades, pois não podem ser representadas por uma malha 3D. Com o entendimento do funcionamento da estrutura de objetos no Ogre com o SceneManager e SceneNode, e pode então prosseguir com o carregamento de cenários. Basicamente carregar um cenário, é carregar os objetos, criar as luzes, câmeras, partículas, e tudo mais que compõe o cenário. Este carregamento pode ser feito manualmente, criando dentro do código todo o processo de montagem do cenário, carregando objeto a objeto e os anexando no sceneNodes, porém isto é uma solução engessada e custosa ao desenvolvedor, pois sua manutenção se tornaria extremamente difícil (para não dizer inviável). Existe uma outra forma para realizar esta tarefa, que é carregando um arquivo XML com informações de cenário. Este arquivo é o dotScene, nele possui as informações hierárquicas dos objetos (entidades e não entidades), com todos os seus atributos. Para implementar esta facilidade, será usado a classe DotScene que esta no arquivo dotscene.py. Esta classe é um parser de XML que realiza a leitura do arquivo dotscene e vai montando o cenário no SceneManager passado. Ele cria automaticamente todos os SceneNodes e neles anexa os objetos em si. Antes que seja implementado esta classe, algumas modificações deverão ser feitas na classe principal para que os objetos possas ser carregados. Primeiramente deve-se realizar a utilização da classe DotScene do arquivo dotScene.py, com a linha abaixo, que deverá se localizar logo no começo do arquivo, junto

44 com os demais imports.

from dotscene import DotScene

Importando o dotScene.

Para realizar o carregamento do cenário com a classe DotScene, deve-se preocupar em manter o isolamento das responsabilidades, e criar um método que será responsável por manipular elementos do cenários. Este método se chamará criaCenario( ) e deverá ser implementado conforme o código seguinte.

def criaCenario( self ): """ Metodo onde ficarao os codigos que serao responsaveis para criar conteudo referente ao cenario """ self.dotScene = DotScene( "Scene.xml", self.sceneManager, self.root ) ent = self.sceneManager.getEntity( "plane_1" ) ent.setMaterialName("Examples/GrassFloor") ent.setCastShadows( False ) ent = self.sceneManager.getEntity( "plane_2" ) ent.setMaterialName("Examples/Rockwall") ent.setCastShadows( False ) ent = self.sceneManager.getEntity( "athene" ) ent.setMaterialName("Examples/Athene/NormalMapped")

Implementação do método criaCenario( ).

Na primeira linha é feito o carregamento de um cenário já existente usando o DotScene. O construtor do DotScene recebe os seguintes parâmetros:
• •

fileName - arquivo XML que irá ser o dotScene; sceneManager - o SceneManager criado anteriormente, será neste SceneManager que o cenário será carregado; rootNode - a instância root do Ogre criado anteriormente;

•

As demais linhas são definição de materiais para as entidades. Ao criar uma

45 entidade, ela deverá receber um nome único que a identifique, para que possa ser recuperada posteriormente utilizando o método SceneManager.getEntity( ), passando o nome da entidade em questão. Uma vez que a entidade foi recuperada para um objeto, suas propriedades poderão ser acessadas para realizar qualquer operação necessária. Neste caso, é feita uma atribuição do material que a entidade terá. Normalmente uma entidade já possui suas informações de qual material a utilizar, dentro do próprio arquivo, mas existem casos que esta informação do material não encontra-se presente, logo, é necessário fazer uma atribuição manual do material a ser utilizado. O Ogre localiza tanto os materiais quanto os objetos para ser carregado, dentro dos caminhos especificados no arquivo resources.cfg, se o arquivo a ser utilizado estiver dentro dos caminhos especificados, o Ogre conseguirá localizá-lo. Se a aplicação for executada, será visto um cenário conforme a imagem abaixo.

Ilustração 7: Renderização após ter carregado o dotScene de demonstração para avaliar se tudo feito até no momento esta funcionando. Fonte: Fonte Nossa.

46 2.5.1 SkyPlane, SkyBox e SkyDome

Como parte do processo de criação do cenário, existe a elaboração de um cenário de fundo que serve para complementar o cenário principal que esta sendo criado, seu uso tem utilidade primária para ambientes externos, onde é necessário visualizar o céu. Estes cenários de fundo podem se comportar de três maneiras distintas, dependendo do propósito da aplicação.

2.5.1.1

SkyBox

Basicamente é um cubo gigantesco que engloba todo o cenário. Para que seja criado, deverá ser chamado o método sceneManager.setSkyBox, que recebe os seguintes parâmetros:
enableSkyBox - indica se deseja habilitar (True) ou desabilitar (False) o SkyBox; material - especifica qual material que deverá ser usado como SkyBox; distânciaDaCamera - indica o quão longe da câmera que o SkyBox irá ficar, porém isto somente terá efeito se usado em conjunto com o parâmetro a seguir; drawFirst - informa se o SkyBox será desenhando antes da cena (True) ou se deverá ser desenhado depois da cena (False)

A linha abaixo irá criar o SkyBox e deverá se localizar dentro do método criaCenario( ), após o carregamento do cenário:

self.sceneManager.setSkyBox(True, "Examples/CloudyNoonSkyBox", 1000, True)

Configurando skyBox.

Ao executar a aplicação, terá um cenário de fundo configurado, conforme imagem abaixo:

47

Ilustração 8: Renderização do cenário com SkyBox configurado Fonte: Fonte Nossa.

E por fim, será visto a utilização do parâmetro drawFirst. Basicamente, define o que deverá ser desenhado primeiro, o cenário ou SkyBox. Se o cenário for desenhado primeiro, o SkyBox será desenhado por cima do cenário, podendo ocultar parte dele. Caso contrário, independente, o cenário será visualizado integralmente até à distância máxima de visualização da câmera. Abaixo será visto a diferença da utilização do parâmetro drawFirst. Apenas para fins de teste, se reduzir a distância de visualização do SkyBox para 150 unidades e coloque drawFirst como False, e o cenário será cortado, conforme imagem abaixo.

48

Ilustração 9: Renderização do cenário com SkyBox com a distância reduzida para 150 unidades e drawFrist como Falso. Fonte: Fonte Nossa.

O que nota-se é que o SkyBox irá se sobressair sobre o cenário, limitando o campo de visão. Isto poderá ser útil, dependendo do propósito utilizado, pois com uma distância de visualização do cenário menor, menos objetos poderão ser renderizados e poderá ter um ganho de desempenho

2.5.1.2

SkyDome

SkyDome é bem semelhante ao SkyBox, a diferença é que a textura é projetada de maneira esférica, porém a base do cubo não é texturizada, logo, para este tipo de céu, é necessário a utilização de um chão, um terreno. A implementação do SkyDome é feita utilizando o comando sceneManager.setSkyDome. A implementação anterior do SkyBox

49 deverá ser removida e colocar esta linha no lugar.

self.sceneManager.setSkyDome(True, "Examples/CloudySky",5,8,500, True);

Configurando skyDome.

Existem seis parâmetros que devem ser configurado. Os dois primeiros e os dois últimos trabalham de forma idêntica ao SkyBox. Porém o terceiro e o quarto parâmetro é exclusivo do SkyDome:
curvatura - define o fator de curvatura da textura. A API do Ogre sugere utilizar valores entre 2 e 65, onde valores altos permite um efeito mais suave, e valores baixos são menos curvados possibilitando um melhor efeito de distância; tiling - informa quantas vezes que a textura poderá ser repetida no SkyDome

Ao executar a aplicação será visto a cena a seguir:

50

Ilustração 10: Renderização do cenário com SkyDome configurado Fonte: Fonte Nossa.

Note que à medida que se aproxima do horizonte, o efeito de distorção esférica da textura torna-se péssima. A limitação do uso do SkyDome é somente para cenários em que a visão se limitará até o horizonte, e nunca abaixo dele. Deve-se experimentar alterar os valores de curvatura e repetição para ver os efeitos que podem ser alcançados.

2.5.1.3

SkyPlane

SkyPlane se difere dos SkyBox e SkyDome, devido ao fato de se comportar como uma superfície plana. A sua criação envolve em criar dentro do Ogre uma superfície plana e então a criação do SkyPlane com o sceneManager.setSkyPlane, o uso do SkyPlane é para cenários onde que não ocorrerá a visualização do horizonte, e somente o céu será visto. Porém

51 o SkyPlane não será utilizado neste projeto, então não será abordado o seu uso. O cenário esta carregado mas não esta pronto ainda, existem outros aspectos que devem ser abordados, como iluminação e sombreamento. Tudo esta como se estivesse uma grande luz constante iluminando todo o ambiente, o que não é o desejado. Além disto, o script que esta sendo utilizado para carregar o cenário, deverá sofrer mais modificações, de forma que possa realizar mais atividades relacionadas à montagem de cenário. Mais adiante será visto como utilizar os scripts de exportação de objetos e cenários no Blender, inclusive a forma correta de realizar a texturização. Existe também a configuração do céu, que poderá ser feita com o SkyBox, SkyDome ou SkyPlane, onde que cada uma destas técnicas tem um objetivo específico, sendo que no SkyBox é ideal para locais de visualização para todas as direções, o SkyDome para somente o céu e o horizonte, e o SkyPlane para somente o céu, sem horizonte ou horizonte limitado. A seguir será visto a configuração de iluminação e sombreamento.

2.5.2

Iluminação e Sombras

Uma das partes mais importantes no carregamento do cenário é a definição de iluminação e sombras. Esta parte é complexa e delicada, pois existe muitos detalhes e aspectos que devem ser observados, pois as luzes e as sombras possuem muitos parâmetros que devem ser configurados de acordo com a aplicação. Estes são recursos que demandam processamento do hardware disponível e devem ser utilizados com parcimônia, caso contrário a aplicação poderá torna-se lenta.

2.5.2.1

Luzes

As luzes são objetos que o Ogre criam que tem como propósito apenas produzir luz. Como dito anteriormente, a luz não é uma entidade, e não possui nenhuma representação

52 visual (não é possível ver o ponto de emissão), pois somente é vista quando ilumina alguma entidade. O resultado final é uma combinação com as propriedades da luz e as propriedades do material do objeto que irá refleti-la. Antes de prosseguir no código sobre luz, deve-se procurar entender como que o Ogre trabalha com iluminação e quais são os parâmetros que os afetam. O Ogre possui três tipos de luzes:
• •

Pontual, é um ponto que emite luz para todos os lados, em uma esfera; Spotlight, age como uma lanterna, possui o formato de um cone, onde a ponta é a sua origem e ela vai abrindo para um círculo, iluminando somente o que se encontra dentro deste cone de luz, possuindo duas áreas de iluminação, uma mais forte, no centro, e uma mais fraca, na borda; Direcional, simula uma luz distante, que partir de uma direção, ilumina todas as entidades que se encontram na cena. Ideal para simular a luz do Sol ou da Lua.

•

Estes são somente os tipos básicos da luz, além disto, a luz possui mais atributos como:
•

diffuseColour (cor difusa), a cor que a luz afetará a textura do objeto, ou seja, é a cor principal da luz; specularColor (cor de reflexão), a cor que será refletida da superfície do material; attenuation, controla a forma que a luz irá reduzir à medida que afastar do ponto de emissão;

•

•

As luzes são adicionadas na cena como um objeto qualquer, utilizando o método SceneManager.createLight( ), e elas são dinâmicas, isto significa que após criadas, podem ter seus atributos alterados de forma que possa alcançar os efeitos de iluminação desejados. Quando adicionada a um SceneNode, elas podem alterar sua posição e direção, tornando-se móveis. Ao criar uma luz, o Ogre adota por padrão a cor difusa branca, sem atenuação e com uma distância de 1000 unidades. Da forma que o projeto será feito, estes elementos poderão ser carregados de dentro do arquivo dotScene, mas para fins didáticos, as luzes serão criadas dentro do código para iluminar o cenário que foi carregado anteriormente, de forma que possa entender como que é feita a configuração dos parâmetros de iluminação.

53

2.5.2.1.a

Luz Ambiente

Antes de trabalhar com luzes, deve-se verificar um detalhe no ambiente. Note que todos os objetos do cenário estão iluminados por igual, sem sombreamento e nem nada, como se algo estivesse iluminando os objetos por igual, de todas as direções. Isto se deve à luz ambiente que esta configurada por padrão no SceneManager. AmbientLight é um tipo de iluminação que não é proveniente de nenhuma fonte de luz, é a forma que o Ogre mantém o SceneManager iluminado, de forma uniforme e constante, sem a necessidade de criar uma luz externa. O ambientLight pode ser configurado em qualquer momento da aplicação, e como é um parâmetro de aparência do cenário, o desenvolvedor que irá saber a melhor forma de configurá-lo. Neste caso, o ambientLight será configurado dentro do método criaCenario( ), preferencialmente antes de carregar o cenário, conforme abaixo:

self.sceneManager.ambientLight = (0, 0, 0)

Configurando a luz ambiente.

O ambientLight recebe um vetor com a informação de cores, Vermelho, Verde e Azul. Onde (0, 0, 0) é preto e (1, 1, 1) é branco. Deixando (0, 0, 0) seria como se nenhuma luz estivesse iluminando o cenário, o deixando totalmente na escuridão. Uma sugestão de iluminação ambiente é configurar como (0.1, 0.1, 0.1), deixando o cenário na penumbra, com apenas 10% de iluminação ambiente.

54

Ilustração 11: Luz ambiente configurada em 10% Fonte: Fonte Nossa.

2.5.2.1.b

Luz Pontual (LT_POINT)

A luz pontual é o tipo mais simples de luz, como dito anteriormente, é um ponto que emite luz para todas as direções. O código abaixo criará uma luz pontual, e deverá ficar dentro do criaCenario( ).

# criando uma luz luzPontual = self.sceneManager.createLight("luzPontual") # especificando que o tipo e pontual luzPontual.setType( ogre.Light.LT_POINT ) # indicando um ponto no espaco que o emissor se localizará luzPontual.setPosition( 0, 50, 100 ) # indicando uma cor, tonalidade de laranja luzPontual.setDiffuseColour( 0.70, 0.55, 0.20 )

55

# indicando a cor de reflexao luzPontual.setSpecularColour( 1.0, 1.0, 1.0 )

Criando um sistema de iluminação pontual.

Este código irá criar uma luz amarelada ao centro do cenário, e ao renderizar, os objetos ficarão iluminados. Conforme imagem abaixo:

Ilustração 12: Iluminação através de uma luz pontual Fonte: Fonte Nossa.

2.5.2.1.c

Luz Spotlight (LT_SPOTLIGHT)

56 Spotlight é um tipo especial de luz que se comporta como a luz de uma lanterna, e abre para o formato de um cone, iluminando somente o que fazer intercessão com o cone. O spotlight possui dois ângulos de iluminação, onde que o primeiro ângulo é do circulo interno, onde a luz é mais forte e o segundo ângulo é do circulo externo, onde ela é mais fraca (simulando uma lanterna, onde a areá central é iluminada mais forte, e à medida que se aproxima da periferia, fica mais fraco e então escuro). A definição dos parâmetros deste tipo de iluminação é mais complexo, pois existem três fatores determinantes para o Spotlight, que é a posição que se localiza, a direção para a qual iluminará e o ângulo do cone da luz. O código anterior deverá ser removido ou comentado para adicionar o código à seguir.

# criando uma luz básica luzSpot = self.sceneManager.createLight("luzSpot") # definindo que o tipo é spotlight luzSpot.setType(ogre.Light.LT_SPOTLIGHT) # indicando uma posição no espaço onde ficará o emissor luzSpot.setPosition(0,625,300) # indicando os ângulos interno e externo do cone, respectivamente luzSpot.setSpotlightRange(ogre.Degree(d=10), ogre.Degree(d=10)) # apenas para demo, fazendo a posição oposta à posicao da luz... dirV = -luzSpot.getPosition() # ... normalizando o vetor de posicao ... dirV.normalise() # ... definindo a direção para a qual iluminará luzSpot.setDirection(dirV) # cor da luz, tonalidade de verde luzSpot.setDiffuseColour(0.50, 1.0, 0.50)

Criando um sistema de iluminação por spotlight.

Existem alguns atributos especiais que configuram mais características deste tipo de luz, que são:
•

direction - recebe um vetor posicional que define um ponto no espaço que ira ficar direcionado; setSpotLightRange - define os ângulo interno e o ângulo externo dos círculos de iluminação do cone;

•

57

Ilustração 13: Iluminação com Spotlight Fonte: Fonte Nossa.

2.5.2.1.d

Luz Direcional (LT_DIRECTIONAL)

A luz direcional se comporta como uma luz que emana de um ponto para uma determinada direção, simulando uma luz distante, com a luz do sol ou a luz da lua. Uma característica da luz direcional, pelo fato de simular uma luz muito distante, não é necessário especificar sua posição, basta definir a direção que ela irá iluminar, abrangendo todo o cenário. Adicione este código para criar uma luz direcional (os códigos anteriores de iluminação deverão ser comentados):

58
# criando um emissor de luz luzDirecional = self.sceneManager.createLight("luzDirecional") # indicando que o tipo é direcional luzDirecional.setType(ogre.Light.LT_DIRECTIONAL) # especificando qual direcao que iluminará luzDirecional.setDirection(100, -70, -80) # definindo a cor, tonalidade de amarelo luzDirecional.setDiffuseColour(0.70, 0.70, 0.30)

Criando um sistema de iluminação por luz direcional.

Ilustração 14: Iluminação através de luz direcional Fonte: Fonte Nossa.

2.5.2.2

Sombras

Até o momento as luzes foram criadas, mas nenhuma sombra foi vista. Para aumentar o realismo da visualização do cenário, a utilização de sombras é extremamente

59 importante no processo de renderização da cena. Porém isto irá requerer cautela do desenvolvedor, pois é um recurso complexo, e neste documento as sombras serão abordadas superficialmente, mas de forma que possa retirar um bom entendimento delas.
Infelizmente, as sombras ainda é um dos aspectos mais desafiadores da renderização 3D, e ainda existem muitas pesquisas sobre elas. Enquanto existem muitas técnicas para renderizar as sombras, nenhuma é perfeita, e todas vem com vantagens e desvantagens. (Ogre3D, 2007, Tradução Nossa)

De forma que o efeito de sombra seja alcançado, o Ogre disponibiliza diversas técnicas de sombra a qual deverá ser analisada de forma que sirva com o cenário que esteja trabalhando. A geração de sombra se define em dois tipos básicos:

2.5.2.2.a

Stencil Shadow

As sombras são criadas com o contorno da silhueta do objeto que esta recebendo a luz, gerando assim uma sombra nítida e geométrica, com mais perfeição. Ela permite o Self Shadowing (que é a entidade projetando sombra nela própria) com a utilização de hardwares que não forneçam suporte a este tipo de técnica, considerando que é totalmente processada na CPU e não no dispositivo de vídeo, alcançando um efeito mais realista. Uma característica dela é a borda da sombra ser nítida, não sendo possível suavizá-las, ou seja, não é possível utilizar Soft Shadows com esta técnica. Devido ao fato de utilizar uma técnica geométrica para a produção das sombras, Stencil Shadow deve ser utilizado com grande cautela, pois quanto mais polígonos o objeto gerador de sombra possuir, mais custoso ao sistema ficará a sua geração. E como as aplicações hoje em dia demandam uma qualidade gráfica maior, a utilização do Stencil Shadow poderá ser um gargalo no desempenho da aplicação, ocorrendo um grande overhead na CPU. Este trecho foi extraído do manual do Ogre sugerindo alguns cuidados quanto ao uso do Stencil Shadows:

60
•

Tente evitar que os objetos fiquem muito próximos (ou até mesmo dentro) das luzes - isto poderá parecer bonito, mas em alguns casos poderão surgir artefatos (objetos geométricos na cena) em equipamentos que não são capazes de executar vertex programs. Esteja ciente que os volumes da sombra (preenchimento) não respeitam a 'solidez' dos objetos que passarão através, e se estes objetos não projetar sombras neles próprios (que poderá esconder o efeito), então o resultados será que você verá as sombras do outro lado que deveria ter sido ocultada pelo objeto. Faça uso do SceneManager::setShadowFarDistance para limitar o número de volumes das sombras que são construídas. Faça uso de LOD (Level Of Detail) para reduzir a complexidade do volume da sombra mais distante. Tente evitar sombras compridas (nascente e poente) - elas podem exacerbar outros problemas como volume clipping, fillrate, e podem demandar mais objetos em uma distância maior para construir o seu volume. (Ogre3D, 2007, Tradução Nossa)

•

•

•

•

2.5.2.2.b

Texture-Based Shadow

Sombras baseadas em textura são produzidas através do ponto de vista da luz no objeto que produzirá a sombra, sob uma textura a qual é feita a projeção durante o momento da renderização. A utilização desta técnica traz algumas vantagens em relação ao uso de Stencil Shadows, como o overhead dos detalhes da malha do objeto não causa tanto impacto na utilização da CPU, além do mais, este tipo de textura normalmente é processado diretamente na GPU, liberando a CPU deste trabalho e obtendo um ganho de desempenho considerável. Outro aspecto é que este tipo sombra é muito mais personalizável, pois é possível utilizar shaders para objetos bons efeitos. A desvantagem é que, pelo fato de utilizar uma textura para produzí-la, e uma textura é uma imagem, logo, ela possui um tamanho fixo, e isto pode se tornar desagradável quando ela esticar muito, pois a pixelação (serrilhado provocado pela baixa resolução) será evidente. Existem diversas formas de combater isto, e uma delas é utilizar texturas de resolução mais alta, consumindo mais memória, o que hoje em

61 dia não é tão ruim, devido ao fatos das placas de vídeo mais modernas estarem vindo com muita memória. Além das técnicas básicas de sombras, existem algumas características de como que elas se comportarão, o qual pode ser combinada com as técnicas de produção. Estas características são:

2.5.2.2.c

Sombras Modulativas

Basicamente as sombras modulativas funcionam escurecendo uma cena já renderizada com uma cor específica. Esta cor pode ser configurada utilizando o método SceneManager.setShadowColour, que por padrão é cinza escuro. Apesar de ser uma forma de sombra imprecisa (as áreas sombreadas tem a mesma intensidade da sombra, independente da quantidade de luz que aquela área esteja recebendo), porém bons efeitos podem ser alcançados utilizando esta forma sem sobrecarregar a CPU.

2.5.2.2.d

Sombras de Máscara Aditiva

As sombras de máscara aditiva, trabalha renderizando a cena diversas vezes, para cada ponto de luz, que é obtido um resultado (uma sombra) a qual é combinada com a passagem anterior, através da adição dos resultados. Neste processo toda a contribuição das luzes são acumuladas de forma correta na cena e cada ponto de luz foi prevenido de afetar áreas que não podem afetar por causa dos emissores de sombra (objetos). Esta técnica é eficaz e produz resultados bem realísticos, mas para isto existe um custo, pois para cada luz é realizada uma passagem de renderização. Os tipos suportados de sombras são: ogre.SHADOWTYPE_STENCIL_ADDITIVE ogre.SHADOWTYPE_STENCIL_MODULATIVE

62 ogre.SHADOWTYPE_TEXTURE_ADDITIVE ogre.SHADOWTYPE_TEXTURE_MODULATIVE

Para que as sombras passem a funcionar, existem algumas configurações que deverão ser observadas. Os objetos podem ou não projetar sombras e também podem ou não receber sombras. No caso de Stencil Shadows, as sombras são projetadas em todos os objetos, mas no caso de Texture Shadows, as sombras serão projetadas em superfícies que não projetam sombras. Observando o arquivo Scenes.xml, ambos planos estão configurados para não projetar sombras, de forma para que Texture Shadows possa ser visualizada. Na imagem abaixo é visto que as sombras são projetadas apenas no piso, mas não nos demais objetos que fazem parte dela. Para informar que um objeto não produza sombras, deve-se utilizar o método entidade.setCastShadows( ), que recebe como parâmetro um valor verdadeiro ou falso. E no caso de não receber sombras, existe o método entidade.setReceiveShadows( ), que também recebe um valor verdadeiro ou falso. A utilização de sombras é feita informado ao Ogre qual será a técnica a ser utilizada. Será implementado primeiro Shadow Texture, utilizando o método sceneManager.setShadowTechnique( ). Afim de evitar confusões com as técnicas, o Ogre permite apenas uma técnica de sombra por SceneManager. Esta linha deverá ficar dentro do método configuracaoVisual( ), após a criação do SceneManager, uma vez que a configuração de sombras é para o SceneManager. Além de definir a técnica, também é definido o tamanho da memória que irá consumir, que será tamanho*tamanho*3 bytes, lembrando que é válido somente para Texture Shadows. Maior o tamanho, maior será a resolução textura da sombra, mas deverá ter cautela para não sobrecarregar a memória de vídeo.

# definindo a técnica de sombra Shadow Texture self.sceneManager.setShadowTechnique( ogre.SHADOWTYPE_TEXTURE_ADDITIVE ) # aumentando o tamanho da memória a ser utilizada self.sceneManager.setShadowTextureSize( 2048 )

Configurando Texture Shadows.

63

Ilustração 15: Texture Shadows ativado. As sombras são menos precisas. Fonte: Fonte Nossa.

Para

utilizar

Stencil

Shadow,

a

técnica

deve

ser

alterada

para

SHADOWTYPE_STENCIL_ADDITIVE:

self.sceneManager.setShadowTechnique( ogre.SHADOWTYPE_STENCIL_ADDITIVE )

Utilizando técnica de sombra Stencil Shadows Additive.

Ao renderizar, as sombras serão vistas, de uma forma diferente da Texture Shadow. As sombras são mais nítidas e permite o selfshadowing.

64

Ilustração 16: Stencil Shadows ativado. É possível ver que a estátua e as casas projetam sombras neles próprios. Fonte: Fonte Nossa.

Como foi visto, iluminação e sombras é um assunto muito extenso e complexo para abordar, como são efeitos que tem função primária na renderização da cena, logo são variáveis e dinâmicos, e não adianta sugerir receitas de valores e formas para sua utilização. Tudo irá depender de muitos parâmetros, como o tamanho do ambiente, da quantidade de luzes, da quantidade de objetos que existirão na cena, da complexidade destes objetos entre muitos outros. Qual a melhor técnica a ser utilizada para sombras? Qual é a melhor forma de iluminar a cena? São perguntas que somente o desenvolvedor saberá, pois dependem de muitos aspectos para que sejam corretamente respondidas. Sobre iluminação, o seu resultado é uma combinação dos parâmetros da luz com os parâmetros do material que será incidido. Ensaios e combinações deverão ser feitas de forma que se alcance um bom efeito tornando o cenário atrativo, realístico e, inclusive, funcional. A utilização de sombras não deverá ser indiscriminado, o desenvolvedor deverá

65 ter boas maneiras quanto ao seu uso, pois bons efeitos podem ser alcançados, mas poderá ser custoso tanto para a CPU quanto para a GPU. Tenha em mente que a aplicação poderá ser executada em sistemas com poucos recursos, então oferecer ao usuário a configuração de sombras seria uma grande gentileza, considerando que existem hardwares que não é capaz de processar Shadow Textures e a aplicação simplesmente não irá executar, e talvez colocar opção de desligar sombras seria de grande ajuda. Caso o usuário possua um equipamento moderno, onde grande quantidade de recursos estão disponíveis, ele pode aumentar os valores para alcançar uma renderização mais perfeita.

66

3

OIS E DINÂMICA

3.1

OIS – Object Oriented Input System

No capítulo 2 foi visto uma implementação superficial do OIS, para fazer com que a aplicação saia pressionando a tecla ESCAPE. Isto foi feito implementando um FrameListener, e capturando os eventos do teclado, para então no frameStarted, verificar se houve alguma chamada à tecla em questão. O OIS fornece algumas facilidades que melhoram o gerenciamento destes eventos, que são o OIS.KeyListener, OIS.MouseListener e OIS.JoyStickListener. Estas classes realizam o trabalho de escutar os dispositivos e realizar uma chamada de callback às classes nelas registradas. Será visto à seguir implementação de um novo FrameListener utilizando as classes KeyListener e MouseListener. Desta forma, o OIS irá notificar a classe quando houver alguma mudança de estado proveniente destes dispositivos.

3.1.1

Classes OIS Listeners

3.1.1.1

Classe KeyListener

Esta classe é responsável por escutar o teclado e realizar uma chamada de callback aos métodos keyPressed( ) e keyReleased( ). Estes métodos deverão ser sobrescritos na classe que irá estender o OIS.KeyListener, e possuem um parâmetro chamado arg da classe OIS.KeyEvent, que tem dois atributos:
• •

key: KeyCode do evento text: texto do caracter

67

3.1.1.2

Classe MouseListener

Responsável por escutar os eventos vindo do mouse, realizando uma chamada de callback aos métodos mouseMoved( ), mousePressed( ), mouseReleased( ). Estes métodos deverão também ser sobrescritos na classe que irá estende-la. Eles possui um parâmetro arg, que é do tipo OIS.MouseEvent. Nesta classe existe um método chamado getEvent( ), que retorna um objeto da classe OIS.MouseState, que possui os atributos:
• • • •

X: Componente do eixo X Y: Componente do eixo Y Z: Componente do eixo Z buttons: representa todos os botões

Os métodos mousePressed( ) e mouseReleased( ), possui mais um parâmetro, que é o id, que informa qual botão do mouse que foi pressionado, que são: 0 – botão esquerdo 1 – botão direito 2 – botão central

3.1.2

Implementação da classe InputManager

Anteriormente foi implementado uma classe simples, que realiza a verificação da tecla pressionada, para disparar um código inserido nesta verificação. Porém trabalhando desta forma, fará com que a classe esteja altamente acoplada a outras classes e objetos, reduzindo a sua flexibilidade e fazendo com que a manutenção seja complexa. Para solucionar este problema, a classe InputManager seguirá o Design Pattern Observer, que irá realizara

68 notificação de todas as classes observadoras, executando um método específico, que deverá ser implementado nestas classes. Desta forma, a classe InputManager (sujeito) ficará desacoplada das classes observadores, fazendo com que o programa fique flexível, podendo os observadores registar e remover o registro no InputManager em qualquer momento da aplicação, em tempo de execução. Esta implementação será feita criando a classe InputManager que herdará da classe Ogre.FrameListener, pois ela deverá a cada quadro, realizar a captura dos dados dos dispositivos. Dentro desta classe serão criadas as subclasses KeyListener e MouseListener, para responder às atualizações provenientes dos dispositivos, para então notificar as classes observadores. Neste caso, esta classe deverá ser escrita em um novo arquivo, a fim de não sobrecarregar o arquivo TechDemo.py com classes. O nome deste arquivo será InputManager.py. Antes de criar esta classe, a implementação da classe InputListener, bem como qualquer chamada a ela, deverá ser removida, de forma para não conflitar com esta nova classe. Esta implementação irá requerer duas bibliotecas, o Ogre e o OIS. Logo, sua importação se faz necessária.

import ogre.io.OIS as OIS import ogre.renderer.OGRE as ogre

Importação dos pacotes necessários para a classe InputManager.

Abaixo segue a definição da classe InputManager. Alguns elementos serão semelhante à classe InputListener, considerando que ambas deverão realizar a operação de captura dos dispositivos de entrada. Nesta classe serão definido os tipos chamados de CallTypes, que irá definir qual o tipo de chamada que os observadores irão se registrar. Considerando que existirão classes que se registrarão, por exemplo, eventos de KeyPresses e outras para MouseMoved.

class InputManager( ogre.FrameListener ): # Registrando os CT (CallTypes) CT_KeyPressed = 1 CT_KeyReleased = 2 CT_MouseMoved = 3 CT_MousePressed = 4

69
CT_MouseReleased = 5

Implementação da classe InputManager.

Em seguida será declarado o método construtor da classe, que irá iniciar os dispositivos de entrada e declarar a lista de observadores (self.observers), onde será registrados uma lista com as instâncias das classes que devem ser notificada pelo determinado tipo de evento, especificado acima com os CallTypes.

def __init__( self, window ): ogre.FrameListener.__init__( self ) # Inicializando o InputManager janelaHnd = window.getCustomAttributeInt( "WINDOW" ) self.PIS= OIS.createPythonInputSystem( [ ( "WINDOW", str( janelaHnd ) ) ] ) # inicializando o teclado e o mouse, ambos como buffered self.IOKeyboard = self.PIS.createInputObjectKeyboard( OIS.OISKeyboard, True ) self.IOMouse = self.PIS.createInputObjectMouse( OIS.OISMouse, True ) # Lista geral de observadores self.observers = { } # Registrando as listas de observadores self.observers[self.CT_KeyPressed] = [ ] # KeyPressed self.observers[self.CT_KeyReleased] = [ ] # KeyReleased self.observers[self.CT_MouseMoved] = [ ] # MouseMove self.observers[self.CT_MousePressed] = [ ] # MousePressed self.observers[self.CT_MouseReleased] = [ ] # MouseReleased

Definição do construtor do InputManager.

Neste ponto é uma característica deste novo modelo de gerenciamento do dispositivo de entrada. As classes MouseListener e KeyListener, que serão definidas à seguir, receberão como parâmetro em seu construtor a lista de observadores, de forma que quando houver uma mudança de estado, estes observadores serão notificado através da chamada a um método específico, que será visto à seguir.

# criando os objetos para as classes MouseListener e KeyListener self.mouseListener = self.MouseListener( self.observers ) self.keyListener = self.KeyListener( self.observers )

Instanciando os objetos MouseListener e KeyListener.

70

Para que sejam notificadas da mudança de estado do dispositivo, é necessário incluí-las na chamada de callback dos dispositivos, através do método setEventCallback( ), que recebe como parâmetro a instância da classe que implementará os métodos de callback.

# realizando a atribuição para a chamada de callback self.IOKeyboard.setEventCallback( self.keyListener ) self.IOMouse.setEventCallback( self.mouseListener )

Atribuição dos callbacks para o OIS.

Por fim, como recursos foram alocados a objetos, é recomendado que estes sejam removidos no destructor __del__( ).

def __del__( self ): del self.IOKeyboard del self.IOMouse del self.PIS

Implementação do desconstrutor do InputManager.

Com o método construtor implementado, deverão ser implementados três métodos que realizarão as operações fundamentais da classe InputManager. Esta classe no padrão de projeto Observer, é a classe sujeito (subject), logo, deverá implementar um método para registrar e outro para remover as classes observadoras, que serão os métodos registerObserver( ) e unregisterObserver( ). Ambos métodos receberão como parâmetro o tipo de chamada e a instância da classe observadora. O método registerObserver( ) irá adicionar a instância em questão na lista de observadores. A classe unregisterInstance( ) irá removê-la da lista.

""" Metodo para registrar observadores """ def registerObserver( self, callType, instance ): self.observers[ callType ].append( instance ) """ Metodo para tirar o registro de observadores """ def unregisterObsever( self, callType, instance ): self.observers[ callType ].remove( instance )

71
Implementando os registradores e desregistradores do InputManager.

A classe InputManager herda da classe ogre.FrameListener, devendo implementar o método frameStarted para realizar a captura dos dispositivos, através do método capture( ).

""" FrameStarted - capturar os dados dos dispositivos de entrada """ def frameStarted( self, evt ): self.IOKeyboard.capture( ) self.IOMouse.capture( ) return True

Realizando a captura dos dispositivos de entrada.

Com isto, a implementação da classe InputListener esta feita. Deve-se agora realizar a implementação de suas subclasses, KeyListener e MouseListener. Como informado acima, a classe KeyListener herdará da classe OIS.KeyListener, e deverá implementar os métodos keyPressed( ) e keyReleased( ). Nestes métodos deverá ser executado uma iteração na lista de observadores para notificá-los do novo estado, executando um método que estará na classe observadora. Para o evento KeyPressed, será notificado através do método updateKeyPressed( ) e para o evento KeyReleased, será notificado através do método updateKeyReleased( ). Em ambos os métodos, deverá ser repassado o parâmetro o arg, para que o observador possa recuperar a informação da tecla pressionada. As subclasses são implementadas no mesmo nível de edentação dos métodos da classe pai e para acessá-la é utilizado o self.

class KeyListener( OIS.KeyListener ): def __init__( self, observers ): OIS.KeyListener.__init__( self ) self.observers = observers def keyPressed( self, arg ): for i in self.observers[ InputManager.CT_KeyPressed ]: i.updateKeyPressed( arg ) return True def keyReleased( self, arg ): for i in self.observers[ InputManager.CT_KeyReleased ]: i.updateKeyReleased( arg ) return True

72
Implementação da classe KeyListener.

A implementação da classe MouseListener será semelhante à classe anterior, com a diferença que irá herdar da classe OIS.MouseListener e deverá implementar os métodos MouseMoved( ), MousePressed( ), e MouseReleased( ). Da mesma forma como foi realizada a iteração através dos observadores, será feito também nesta classe. Para MouseMoved( ) será updateMouseMoved( ), para MousePressed( ) será updateMousePressed( ) e para MouseReleased( ) será updateMouseReleased( ). Todas repassarão os parâmetros recebidos para os métodos de atualização dos observadores.

class MouseListener( OIS.MouseListener ): def __init__( self, observers ): OIS.MouseListener.__init__( self ) self.observers = observers def mouseMoved( self, arg ): for i in self.observers[ InputManager.CT_MouseMoved ]: i.updateMouseMoved( arg ) return True def mousePressed( self, arg, id ): for i in self.observers[ InputManager.CT_MousePressed ]: i.updateMousePressed( arg, id ) return True def mouseReleased( self, arg, id ): for i in self.observers[ InputManager.CT_MouseReleased ]: i.updateMouseReleased( arg, id ) return True

Implementação da classe MouseListener.

Uma vez com a classe InputManager implementada no arquivo InputManager.py, a classe principal (no arquivo TechDemo.py) deverá realizar sua importação, utilizando a linha abaixo:

from InputManager import *

Importando o InputManager dentro do TechDemo.

Esta classe realiza a manipulação de dispositivos e depende da criação da janela

73 da aplicação para que possa realizar a captura. Então deverá ser instanciada imediatamente após à chamada ao método self.configuracaoVisual( ), no método run( ).

self.inputManager = InputManager( self.janela )

Instanciando o InputManager.

A classe InputManager é um FrameListener, então deverá ser registrado no método configuraFrameListener( ), com addFrameListener( ). Uma observação importante, por se tratar de uma classe de gerenciamento de dispositivos, a primeira fase na iteração principal de um jogo, é realizar a escuta dos dispositivos de entrada, para que as demais classes que necessitem de uma situação destes dispositivos, sejam notificadas antes que o sistema atualize o restante dos dados, logo, deverá ser a primeira classe a ser adicionada na lista de FrameListeners.

self.root.addFrameListener( self.inputManager )

Adicionando a instância do InputManager no FrameListener.

Neste ponto a classe InputManager encontra-se em funcionamento na aplicação, porém sua implementação removeu o recurso que foi realizando antes, que é abandonar a aplicação pressionando a tecla ESCAPE. Agora isto não será de responsabilidade da classe InputManager, uma outra classe deverá realizar este trabalho. Será elegido a classe BasicoListener para realizar esta tarefa. Desta forma, ela passará a ser uma classe observadora de eventos do dispositivo de entrada. Sendo assim, deverá ser configurado para ser notificadas da atualização de estado dos dispositivos. A primeira modificação será a implementação do método updateKeyPressed( ) na classe BasicoListener, que será executado quando alguma tecla for pressionada. Este método recebe como parâmetro o objeto que possui a tecla pressionada. Caso a tecla pressionada seja ESCAPE, então será atribuído a uma variável chamada self.abandonar, o valor Verdadeiro, para que abandone o sistema. Assim, deverá ser criado no construtor da classe a variável self.abandonar recebendo o valor falso, para fazer com que o sistema continue em execução:

def __init__( self ):

74
ogre.FrameListener.__init__(self) self.abandonar = False

Modificando a classe BasicoListener para o novo processo de abandonar a aplicação.

E então implementar o método updateKeyPressed( ).

def updateKeyPressed( self, arg ): if arg.key == OIS.KC_ESCAPE: self.abandonar = True

Implementando o método updateKeyPressed( ), na classe BasicoListener, para informar quando alguma tecla for pressionada.

Em seguida deverá alterar o método frameStarted( ) para que abandone o sistema caso self.abandonar seja verdadeiro.

def frameStarted( self, frameEvent ): return not self.abandonar

Implementação do método frameStarted( ) na classe BasicoListener.

Com a classe BasicoListener devidamente configurada, agora deverá se registar como um observador dos eventos provenientes dos dispositivos de entrada, mas especificamente dos eventos de KeyPressed. Este registro deverá ser feito preferencialmente antes de ser adicionada na lista ). de FrameListeners, chamada será utilizando-se o no método método inputManager.registerObservar( Esta implementada

configuraFrameListener( ), através do comando à seguir.

self.inputManager.registerObserver( self.inputManager.CT_KeyPressed, self.basicoListener )

Registrando a instância da classe BasicoListener como observador do InputManager.

Assim sempre quando alguma tecla for pressionada, a classe BasicoListener será notificada e disparará o código nela programado. Ao executar a aplicação, a tecla de ESCAPE poderá ser pressionada, que a aplicação sairá normalmente.

75 O OIS foi visto de uma maneira completa e abrangente. Devido aos bons modos de desenvolvimento, foi necessário implementar o padrão de projeto Observer, pois esta classe poderá sofrer forte acoplamento com outras classes, fazendo com que sua manutenção seja difícil, tornando-se inflexível. O motivo de sua melhoria neste ponto, é devido ao fato dos próximos itens dependerem de um sistema de gerenciamento de dispositivos de entrada, como a classe Ator e o CEGUI, de forma que a classe InputManager não necessitará de nenhuma modificação.

3.2

Dinâmica com Newton

A implementação de um sistema de dinâmica na aplicação, fará com que ela responda à simulação de eventos físicos, onde objetos poderão se colidir em um ambiente sólido que responde à física real. Por padrão, o Python-Ogre aceita um outro sistema de dinâmica, o ODE (Open Dynamics Engine) que é também um sistema de dinâmica maduro e estável. O motivo da adoção do Newton foi em visto que à sua simplicidade de implementar a simulação física. No Ogre existe um projeto chamado OgreNewt, esta biblioteca realiza o encapsulamento do Newton, integrando-o ao Ogre, para quando o mundo for atualizado, esta classe irá fazer a atualização das posições de todos os sceneNodes, poupando o desenvolvedor de ter de atualizar manualmente todos os sceneNodes. E o OgreNewt será utilizado por este projeto. De forma a permitir que o desenvolvedor interaja com a dinâmica, o Newton trabalha com um sistema de callbacks, que quando houver alguma atualização do mundo, alguma colisão, o Newton chamará o callback configurado no corpo rígido, de forma a executar o código que lá se encontra. Nestes callbacks podem ser aplicados as forças que farão o objeto se deslocar. Para que dar início ao tutorial, é necessário explicar sobre o princípio de funcionamento do Newton. Basicamente, ele possui cinco elementos básicos que são utilizados para descrever a física:

76 World (Mundo) World descreve o ambiente em que os objetos irão ser afetados pela física. A sua criação é obrigatória, pois todos os objetos que serão criados, irão requerer o mundo para o qual deverão responder. Rigid Body (Corpo Rígido) Representa o corpo que deverá ser simulado pelo sistema, e que irá interagir com os demais corpos criado na cena. Como é uma representação do mundo real, os corpos possuem massa, tamanho e forma. Collision (Colisão) É um objeto que representa uma forma para realizar a colisão. Os corpos rígidos requerem um objeto Collision para definir seu formato físico, pois a forma que o objeto pode ter, pode ser diferente da forma que é utilizado para realizar a colisão. Por se tratar de uma forma, o Collision pode ter os seguintes formatos: Formas Primitivas: Cubos, Elipses, Cilindros, Capsulas, Cones e Cilindros Chanfrados; Convex Hull: é um formato irregular que "embrulha" o objeto a ser simulado, de forma semelhante a um papel que embrulha um objeto, de maneira a ocupar o menor espaço possível em torno do objeto, resultando em um formato primitivo que se assemelha ao objeto em questão. Tree Collisions: é um objeto especial que serve para representar um corpo que será imóvel, como os prédios de uma cidade ou um terreno, desta forma ele possui massa infinita e nada pode afetá-lo. O seu uso é praticamente mandatório quando se tratar de utilizar física em um cenário que necessite de um chão, um terreno. Joint (Junção) Une dois objetos de forma que interagem um com outro, como um braço e um antebraço, que são unidos pelo cotovelo. O Newton fornece diversos tipos de junções

77 como dobradiças, Ball and Socket, engrenagem, entre outros. Material Define como que os objetos irão se comportar quando se colidir, onde pode ser ajustado a fricção, por exemplo. Além disto, pode-se criar o chamado MaterialPair, que descreve quando dois tipos de materiais se colidem, assim podendo definir eventos para disparar sons e efeitos visuais, por exemplo.

3.2.1

Classe Mundo

Com base no conceito de funcionamento do Newton, será visto a criação de um sistema básico de colisão entre objetos. Primeiro passo é realizar a importação da biblioteca do OgreNewt:

import ogre.physics.OgreNewt as OgreNewt

Importando o pacote OgreNewt.

Em seguida é a criação do objeto World. Será criado um método que irá ser responsável pela definição do sistema de dinâmica, que se chamará setupNewton( ), conforme código abaixo.

def setupNewton( self ): # criando o objeto World self.World = OgreNewt.World( ) # definindo o tamanho do mundo, onde a dinâmica irá atuar self.World.setWorldSize( -500, 500 ) # criando a variável bodies, que irá armazenar todos os corpos rígidos self.bodies = []

Método setupNewton( ), para realizar a configuração básica do Newton.

Este método realiza a criação do objeto World, configura o tamanho do mundo

78 com o método world.setWorldSize( ) que recebe os parâmetros:
• •

maxInf: ponto máximo inferior do mundo; maxSup: ponto máximo superior do mundo;

Também é definido uma variável chamada bodies. Esta variável irá armazenar todos os corpos rígidos que forem criados, para manter o controle de todos eles. Se não for implementado, a dinâmica não funcionará. O método setupNewton( ) deverá ser chamado imediatamente após à chamada do setupOgre( ), dentro do método run( ):

def run( self ): """ Metodo que inicia a simulacao. Neste ponto serao ativados todos os sistema e tambem serao configurados todos os recursos """ if not self.setupOgre( ): return False self.setupNewton( )

Realizando a chamada ao método setupNewton( ) no método run( ).

3.2.2

Materiais

Uma das características de um corpo rígido é o material do qual ele é feito. Isto serve para informar ao Newton, através do par de materiais, a configuração de atrito e elasticidade. A configuração dos materiais é feito criando-se os ID dos materiais e depois fazendo a combinação com estes materiais. Esta combinação irá possuir os atributos dinâmicos do material, quando estes estiverem se colidindo. Com base nisto serão criados dois materiais, pedra, que será o piso, e borracha, que será aplicado posteriormente ao ator. Estes materiais são definidos com o método OgreNewt.MaterialID( ), que recebe world como parâmetro e retorna um objeto do tipo Material. Este código deverá se localizar dentro do método criaCorposRigidos( ), e antes de criar qualquer corpo rígido.

79
# configuracao dos materiais que serao utilizados pelos corpos self.matPedra = OgreNewt.MaterialID( self.World ) self.matBorracha = OgreNewt.MaterialID( self.World )

Criando materiais.

Com estes materiais definidos, então é feita a combinação do par de material, com o método OgreNewt.MaterialPair( ). Este método recebe como parâmetro world, o primeiro materiais e o segundo material do par, retornando um objeto do tipo MaterialPair. Este objeto irá possuir a configuração da dinâmica, como atrito e elasticidade. O atrito é configurado com o método MaterialPair.setDefaultFriction( ), recebendo como parâmetro o atrito estático e o atrito dinâmico. Abaixo segue a definição do atrito dinâmico (ou cinético) e atrito estático. Atrito Dinâmico:
Chama-se de força de atrito dinâmico a força que surge entre as as superfícies que apresentam movimento relativo. A força de atrito dinâmico se opõe a este deslizamento entre as superfícies, não necessariamente oposta ao movimento do corpo. Por exemplo: Quando um caixote está deslizando sobre uma superfície horizontal para a direita, a força de atrito dinâmico estará aplicada na superfície de contato do caixote e a superfície de apoio paralelamente a superfície e apontando para a esquerda. (WIKIPEDIA, 2007)

Atrito Estático:
Chamamos de força de atrito estático a força que se opõe a deslizamento entre as superfícies. Por exemplo, podemos citar o deslizamento de uma caixa sobre uma superfície ou também o atrito entre o pneu de um carro quando este não está deslizando sobre a superfície. Quando se tenta empurrar uma caixa em repouso em relação ao solo, nota-se que dependendo da força que é aplicada sobre a caixa, esta não sai do lugar. Assim, pode-se concluir que há uma força que atua contra o movimento. Ela é denominada força de atrito estático. Há que se ter cuidado para não relacionar a força de atrito estático com um corpo necessariamente parado. (WIKIPEDIA, 2007)

Neste projeto será usado um coeficiente de atrito estático e cinético que atenda a demanda do deslocamento do ator. Existem também a elasticidade, que é definida através do método MaterialPair.setDefaultElasticity( ), que recebe como parâmetro o coeficiente de elasticidade. A elasticidade será a reação da força aplicada pelo corpo na colisão que será retornada a ele, fazendo-se afastar do objeto colidido, como uma bola de tênis versus uma bola de ferro ao caírem no chão de concreto. A elasticidade da bola de tênis é muito maior que

80 a elasticidade da bola de ferro (que deve ter seu coeficiente aproximado de zero). Com o par de material criado, este deve ser armazenado numa lista para mante-lo na memória.

par = OgreNewt.MaterialPair(self.World, self.matBorracha, self.matPedra) par.setDefaultElasticity( 0 ) par.setDefaultFriction(0.1, 0.1) self.parMateriais['borracha/pedra'] = par del par

Configuração do par de material.

E por fim, os materiais são atribuídos aos corpos através do método body.setMaterialID( ), informando o objeto material criado anteriormente.

3.2.3

Corpos Rígidos

Após a criação do objeto world e dos materiais, deverão ser criados os demais objetos que irão interagir entre si na física. Existem várias formas de alcançar isto, normalmente esta informações podem ficar dentro do Scene.xml, onde que o parser dotscene.py poderá criar toda a configuração da dinâmica. Para fins de demonstração, esta configuração será feita manualmente, demonstrando como que é feito a criação dos corpos rígidos. Os objetos que participarão da dinâmica, poderão interagir com um mundo, fixo, imóvel, como um terreno, casas ou prédios. Estes objetos serão do tipo TreeCollision, ou seja, serão imóveis e possuirão massa infinita. No cenário em questão, será válido para o chão e para as casas. Para facilitar este trabalho, deverá ser implementado o método abaixo, que irá criar um TreeCollision na classe principal para qualquer sceneNode, bastando apenas informar o nome do sceneNode o qual receberá os corpos rígidos.

def criaCorpoTreeCollision( self, nomeSceneNode, nomeEntidade, materialID ): # buscando a entidade ... ent = self.sceneManager.getEntity( nomeEntidade ) # buscando o SceneNode baseado no nome. sceneNode = self.sceneManager.getSceneNode( nomeSceneNode )

81
# criando a forma da colisão colisao = OgreNewt.TreeCollision( self.World, sceneNode, True ) # criando o corpo rígido baseado na forma corpoRigido = OgreNewt.Body( self.World, colisao ) # removendo a forma, pois ela não será mais utilizada del colisao corpoRigido.setPositionOrientation( sceneNode.getPosition( ), sceneNode.getOrientation( ) ) # anexando o corpo rígido ao sceneNode corpoRigido.attachToNode( sceneNode ) #definindo o material a ser utilizado corpoRigido.setMaterialGroupID( materialID ) # retornando o corpo rígido para ser armazenado return corpoRigido

Implementação do método criaCorpoTreeCollision( ).

O corpo rígido tem uma forma, esta forma é descrita pelo objeto colisão e então esta forma é atribuída através do método OgreNewt.Body( ) que cria o corpo rígido com o formato em questão. Por exemplo, se existe 100 objetos num cenário que devem ter o mesmo formato para a colisão, então é definido o objeto colisão apenas uma vez, e então atribuído para a criação de 100 corpos rígidos. Uma vez que o objeto colisão não será mais utilizado, recomenda-se liberar a memória excluindo este objeto. Uma vez com o método implementado, deverá ser feita a sua chamada, mas para isto deverá ser criado um método chamado criaCorposRigidos( ), cuja chamada deverá se localizar dentro do método run( ), após a chamada do método criaCenario( ), visto que para configurar os corpos rígidos, preferencialmente, deverá ser depois que os objetos do cenário ja estiverem criados. Implementação da chamada ao método criaCorposRigidos( ):

self.criaCenario( ) self.criaCorposRigidos( ) self.root.startRendering( )

Implementando chamada ao método criaCorposRigidos( ).

Implementação do método criaCorposRigidos( ):

82

def criaCorposRigidos( self ): """ Método responsável para criar toda a configuração dos corpos rígidos que deverão participar da dinâmica. ""

Implementação do método criaCorposRigidos( ).

Dentro do método criaCorposRigidos( ), deverá ser feita a criação dos corpos TreeCollision, executando a chamada ao método criaCorpoTreeCollision( ). No arquivo Scene.xml existem diversos objetos e sceneNode nomeados que deverão ser corpos rígidos TreeCollision. Estes objetos são: plane_1 e plane_2, que são os dois chãos (o de pedra e o de grama) e tudorhouse_1 a tudorhouse_7 que são as casas. O código abaixo fará a configuração para todos eles.

# criando os corpos self.bodies.append( self.matPedra ) ) self.bodies.append( self.matPedra ) ) self.bodies.append( self.matPedra ) )

rigidos para os pisos self.criaCorpoTreeCollision( "plane_1", "plane_1", self.criaCorpoTreeCollision( "plane_2", "plane_2", self.criaCorpoTreeCollision( "athene", "athene",

# criando os corpos rigidos para as casas, de 1 a 7 for i in range(7): nome = "tudorhouse_" + str( i + 1 ) self.bodies.append( self.criaCorpoTreeCollision( nome, nome, self.matPedra ) )

Criando os corpos rígidos dos objetos necessários no cenário.

Com o cenário definido, deve-se criar os corpos rígidos que irão interagir neste cenário. Estes objetos poderão colidir e sofrer colisões com outros objetos e com o cenários. Para isto é necessário que exista um objeto de colisão de forma primária (cubo, esfera, cone, etc) ou Convex Hull. Para estes objetos, variáveis como massa, inércia e gravidade terão efeitos sobre eles, logo, deverão ser configurados. À princípio será utilizado o Convex Hull para o formato, devido à facilidade de configurar a forma. Como feito anteriormente, deverá ser criado um método chamado criaCorpoConvexHull( ), que ficará responsável por criar corpos rígidos do tipo ConvexHull.

83 A criação destes corpos rígidos é mais complexo quanto ao TreeCollision, existem mais parâmetros que deve ser considerados como posição, orientação, massa e inércia.

def criaCorpoConvexHull( self, nomeSceneNode, nomeEntidade, massaUnitaria, materialID ): # buscando a entidade ... ent = self.sceneManager.getEntity( nomeEntidade ) # ... e buscando o sceneNode ... entNode = self.sceneManager.getSceneNode( nomeSceneNode ) # pegando o tamanho, para realizar calculo de massa e inercia tam = entNode.getScale( ) # definindo a massa do objeto mass = tam.x * tam.y * tam.z * massaUnitaria # calculando a inercia do objeto baseado em um cilindro inertia = OgreNewt.CalcCylinderSolid( mass, tam.z, tam.y ) # Normalizando as normais, para colisao ent.setNormaliseNormals(True) # Criando o formato ConvexHull col = OgreNewt.ConvexHull( self.World, entNode ) # Criando o corpo rigido bod = OgreNewt.Body( self.World, col ) del col # removendo o formato convexHull # anexando o corpo rigido ao SceneNode bod.attachToNode( entNode ) # atribuindo a massa e a inercia bod.setMassMatrix( mass, inertia ) # aplicando a força de gravidade bod.setStandardForceCallback() # definindo a posicao e orientacao do corpo rigido bod.setPositionOrientation( entNode.getPosition( ) ,entNode.getOrientation( ) ) #definindo o material a ser utilizado bod.setMaterialGroupID( materialID ) # retornando o corpo rigido return bod

Implementação do método criaCorpoConvexHull( ).

O que este método faz é buscar o SceneNode e a Entidade que deverá ter corpo rígido. Em seguida deve-se buscar o tamanho do objeto para que se possa calcular sua massa e inércia aproximadas. Também deve-se buscar a posição do objeto para que possa definir a posição do corpo rígido (de forma se a mesma do objeto). A massa é calculado pelas

84 dimensões do objeto multiplicado pela massa unitária, deve-se considerar a unidade da massa em quilogramas. A inércia do objeto é baseada na massa e tamanho, e para facilitar, o OgreNewt possui algumas funções para calcular a inercia de um primitivo qualquer. Neste caso será usado o cálculo de inercia de um cilindro, o que para alguns casos pode não ser realista, mas será bastante aproximado (como se o objeto estivesse dentro de um cilindro com a sua altura e seu raio em um eixo, no caso, eixo z). Outro passo a tomar é a normalização das normais, para evitar qualquer erro de colisão devido à normais incorretas do objeto. Em seguida deve-se criar a forma, no caso, ConvexHull, e então criar o corpo rígido com base neste formato e anexar o corpo rígido ao SceneNode. E por último deverá ser feita a configuração específica para este tipo de corpo, que é a definição da massa e inércia, a aplicação da força de gravidade e a definição da posição e orientação do corpo rígido baseado na posição e orientação do objeto original. Este método deverá ser chamado de dentro do método criaCorposRigidos( ). Em Scene.xml, existem 16 barris que deverão ser corpos rígidos chamado barrel_1 a barrel_16. A implementação deverá ser conforme abaixo:

# criando os corpos rigidos para os barris de 1 a 16 for i in range(16): self.bodies.append( self.criaCorpoConvexHull( "barrel_" + str( i + 1), "barrel_" + str( i + 1 ), 10, self.matPedra ) )

Realizando a configuração para os corpos rígidos móveis.

Além da configuração das entidades que irão interagir num sistema dinâmico, é fundamental a definição de um FrameListener que irá atualizar o sistema dinâmico, realizando todos os cálculos necessários para determinar a nova posição dos objetos. Esta atualização é feita chamando o método World.update( ), que recebe como parâmetro o tempo real decorrido desde a ultima atualização. A classe à seguir é um FrameListener que será responsável pela atualização da dinâmica. Nele será feito o cálculo de FPS (Frames Per Second) para passar o tempo decorrido para o World.update( ).

class NewtonUpdateListener (Ogre.FrameListener ): def __init__( self, World , fps_atualizacao): Ogre.FrameListener.__init__(self) self.World = World self.fps_desejado = fps_atualizacao self.atualizacao = (1.0 / self.fps_desejado)

85
self.decorrido = 0.0; def frameStarted(self, evt): Ogre.FrameListener.frameStarted(self, evt) self.decorrido += evt.timeSinceLastFrame if ((self.decorrido > self.atualizacao) and (self.decorrido < (1.0)) ): while (self.decorrido > self.atualizacao): # atualizando a simulacao self.World.update( self.atualizacao ) self.decorrido -= self.atualizacao else: if (self.decorrido < self.atualizacao): # não passou-se tempo suficiente desde o último tempo decorrido pass else: # atualizando a simulacao self.World.update( self.decorrido ) # reiniciando o tempo decorrido, para não ficar eternamente atraz self.decorrido = 0.0 return True

Implementação da classe NewtonUpdateListener.

Com o FrameListener implementado, seu objeto deverá ser criado, informando o objeto World e FPS desejado para a atualização, e então adicionado à lista de FrameListener do Ogre, no método configuraFrameListener( ), conforme código abaixo:

self.newtonUpdateListener = NewtonUpdateListener( self.World, 60 ) self.root.addFrameListener( self.newtonUpdateListener )

Colocando a instância da classe NewtonUpdateListener como frameListener.

Neste ponto o OgreNewt está implementado e funcionando. Ao executar a aplicação, os barris irão cair e se acomodar, demonstrando que o OgreNewt foi implementado e esta funcionando perfeitamente.

86

Ilustração 17: Tambores se ajustando com a física. Fonte: Fonte Nossa.

Neste tópico foi visto a implementação de um sistema básico de dinâmica com OgreNewt, utilizando TreeCollision para os objetos que ficarão imóveis e ConvexHull para os corpos móveis. O ConvexHull é uma forma mais complexa, podendo sobrecarregar o sistema, porém é a maneira mais simples de implementar. Para objetos mais simples, recomenda-se os outros formatos primitivos, ganhando um desempenho considerável. A implementação do OgreNewt abre caminho para o desenvolvimento de novas funcionalidades ao projeto, como atores, logo, o Newton poderá ser visto novamente mais adiante para implementar tais funcionalidades.

3.3

Classe Ator

Uma simulação ou um jogo é feito criando-se um ambiente virtual, que será o lugar onde as interações com este mundo serão realizadas. De forma a interagir, seja por intervenção do usuário, ou por um sistema de inteligência artificial, é criado uma classe chamada Ator. Esta classe representa qualquer elemento que realizará esta interação no ambiente. Para ter noção do que um ator pode ser, deve-se saber primeiramente seu significado (HOUAISS, 2001, p. 337) "Ator /ô/ s.m (1532 JBarR 39) (...) ETIM lat. actor,õris 'o que faz mover, o que representa, orador, o que executa, dirige'; (...)". Ou seja, o ator será o executor das ações dentro da simulação. Devido à extensão desta classe, será visto neste projeto um ator simples, que será controlado pelo usuário, para realizar a interação no

87 ambiente criado anteriormente.

3.3.1

Implementação

Esta classe será implementada em um arquivo separado, que se chamará ator.py. Neste arquivo será declarada apenas uma classe, que será chamada de Ator. Nesta classe será visto mais implementações de dinâmica, para fazer com que o personagem se mova pelo cenário, e também será visto o assunto de animações do Ogre3D, para trazer a ilusão de que o ator esta em movimento. Será realizada primeiro a implementação da dinâmica e em seguida será feita a implementação da animação.

3.3.2

Dinâmica

Existem várias formas de realizar o ator se mover. Uma forma conhecida é da cápsula e esfera, onde que a cápsula representa o corpo do ator e a esfera representa os pés. Fazendo-se a esfera rolar, o ator irá andar. Esta forma não será feita devida a sua complexidade em relação a este projeto, mas será feito algo semelhante. Será utilizado apenas uma esfera que será arrastada por uma força. Ao mover, será carregada uma animação de movimento do personagem, e quando parar a animação será substituída por outra, que é a animação Idle, parada. Esta classe irá ser um frameListener, devido ao fato de utilizar animação, que necessita ser atualizada com o tempo passado a cada quadro. Abaixo segue a implementação inicial.

class Ator( ogre.FrameListener ): def __init__( self, world, entidade, nodoDinamico ): ogre.FrameListener.__init__(self)

Implementação da classe ator.

88 Em seu construtor, existem três parâmetros sendo que world representa o mundo dinâmico, entidade representa a entidade que será o ator e o nodoDinamico, que é o nodo onde ficarão os corpos rígidos. Devido ao fato de utilizar dois nodos para representar o ator, o nodo que representa a física terá suas informações de posição e orientação passadas ao nodo da entidade, de forma o nodo da entidade ficar sempre na mesma posição do nodo dos corpos rígidos. O código seguinte prossegue dentro do construtor, que é a declaração das variáveis que serão utilizadas nesta classe. A função de cada variável será explicada no decorrer deste item.

# Declarações das variaveis globais self.anguloDirecao = 0 self.rot = ogre.Vector3.UNIT_X self.world = world self.entidade = entidade self.nodoEntidade = self.entidade.getParentNode( ) self.nodoDinamico = nodoDinamico self.corpos = { } self.raio = 0 self.nome = "ator" self.massa = 100 self.velocidade = 4.0 self.movimento = 0

Definições das variáveis da classe.

Será criado um método desta classe que irá criar as definições da dinâmica do ator, que se chamará criaCaracter( ), recebendo como parâmetro um material do newton.

def criaCaracter( self, material ): # Método para criar os atributos físicos do caracter

Definição do método criaCaracter( ).

O corpo físico do ator será uma esfera que se localizará em seus pés. Esta esfera irá possuir um tamanho específico, de forma que não fique grande demais e nem pequena demais. Sua forma será baseada no tamanho do BoundBox do ator. BoundBox é uma forma geométrica que define o tamanho máximo e mínimo dos limites de uma entidade no espaço.

## DEFININDO O TAMANHO DO RAIO PARA A ESFERA aabb = self.entidade.getBoundingBox( ) min = aabb.getMinimum( ) * self.nodoEntidade.getScale( )

89
max = aabb.getMaximum( ) * self.nodoEntidade.getScale( ) tamanho = ogre.Vector3( abs( max.x - min.x ), abs( max.y - min.y ), abs( max.z - min.z ) )

Determinando o tamanho do raio para a esfera.

De forma a determinar o tamanho da esfera, busca-se o AABB (Axis Aligned Bouding Box) da entidade, e determina o seu tamanho mínimo e máximo baseado na escala da entidade. O comprimento que esta entidade ocupa é o ponto máximo menos o ponto mínimo em um mesmo eixo. Assim é feito para os três eixos, e armazenado em um vetor o tamanho desta entidade. Este tamanhos servirá de base para a confecção da esfera. Para saber qual será o raio da esfera, será utilizado o maior eixo, afim de envolver a entidade em seu maior eixo no plano XZ.

# Ajustando o tamanho do raio da esfera if size.x > size.z: self.raio = size.z / 2.0 else: self.raio = size.x / 2.0

Ajustando o tamanho do raio da esfera.

Com o tamanho do raio definido, então é criado o corpo rígido da esfera. No Newton isto é feito utilizando o método OgreNewt.Ellipsoid( ), que recebe como parâmetro o mundo dinâmico e um vetor com o tamanho da elipse no eixo X, Y e Z. Com isto é feito o calculo da inercia com o método OgreNewt.calcSphereSolid( ), recebendo os parâmetros massa e o raio da esfera, e por fim é criado o corpo rígido, que no caso foi chamado de corpoPe. No final a forma é removida com a linha del col, uma vez que ela não será mais utilizada na aplicação, liberando a memória. Além disto é feita a atribuição de massa e inercia ao corpo. Também é feita a definição do material que será utilizado no corpo, com o proposito de simular a fricção e elasticidade.

# criando a esfera de movimento col = OgreNewt.Ellipsoid( self.world, ogre.Vector3( self.raio, self.raio, self.raio ) ) inercia = OgreNewt.CalcSphereSolid( self.massa, self.raio ) corpoPe = OgreNewt.Body( self.world, col ) del col

90
# definindo a massa e inercia da esfera corpoPe.setMassMatrix(self.massa, inercia ) # atribuinte o grupo de material corpoPe.setMaterialGroupID( material )

Definindo a configuração dos parâmetros dinâmicos da esfera.

O corpo rígido encontra-se definido, então deverão ser feitas as operações de orientação e posicionamento. Esta esfera deverá localizar-se na mesma posição e orientação que a entidade se encontra. Além disto, deverá ser feito um ajuste de posição da entidade em relação a esfera. Sem este ajuste, a entidade iria localizar-se à distância de um raio da esfera acima do chão. Isto porque a posição do nodo se encontra no centro da esfera, e não no seu ponto inferior, logo, este ajuste é feito subtraindo a altura da entidade (eixo y) em um raio, colocando a entidade na altura exata, encostada no chão.

# ajustando a esfera para a mesma posicao da entidade corpoPe.setPositionOrientation( self.nodoEntidade.getPosition, self.nodoEntidade.getOrientation( ) ) # ajuste de altura da entidade, para ficar no chao self.nodoEntidade.setPosition( self.nodoEntidade.getPosition( ) ogre.Vector3(0, self.raio, 0) )

Sincronizando a posição da esfera para mesma posição da entidade.

O passo seguinte, é a definição de uma junção chamada UpVector( ). Esta junção é um vetor que força que o corpo fique orientado em um determinado eixo. Para esta esfera será criado um UpVector( ) no eixo Y e um no eixo X, desta forma a esfera ficará alinhada no eixo Y e X, e nunca tombara para nenhum dos lados, considerando que ela será arrastada.

# Junções para manter a esfera ereta e apontando para frente upVectorX = OgreNewt.UpVector( self.world, corpoPe, ogre.Vector3.UNIT_X ) upVectorY = OgreNewt.UpVector( self.world, corpoPe, ogre.Vector3.UNIT_Y )

Definindo as junções da esfera.

O Newton trabalha com um sistema de callback, sendo que quando houver alguma mudança de estado dinâmico do objeto, ou simplesmente quando o mundo sofrer uma atualização, serão chamados os métodos de callbacks configurados nos objetos. Porém para

91 fins de otimização, o Newton pode colocar alguns corpos para "dormir", quando não houver mudança de estado do corpo, apenas "acordando" o corpo quando ele sofrer uma colisão e ter seu estado alterado. Desta forma, para evitar que o Newton ponha a esfera para "dormir", deve-se utilizar o método body.setAutoFreeze( 0 ), e então o corpo permanecerá acordado mesmo quando não houver mudança do seu estado, permitindo assim o controle da esfera pelo usuário.

# informando ao newton para nao deixar a esfera "dormir" corpoPe.setAutoFreeze( 0 )

Informando ao Newton para não deixar a esfera “dormir”.

Para permitir o controle do ator, deve-se implementar o callback que realizará a aplicação das forças no corpo, de forma a fazer a esfera se mover. Isto é feito criando-se um atributo no corpo rígido com uma instância para o método que deverá ser chamado quando o callback for realizado. Em seguida deve-se executar o método setCustomForceAndTorqueCallback( ) informando o corpo rígido que possuirá o método de callback definido e uma string com o nome do método de callback. O método atorControleCallback( ) será implementado em seguida.

# definindo o método de callback corpoPe.atorControleCallback = self.atorControleCallback corpoPe.setCustomForceAndTorqueCallback(corpoPe, "atorControleCallback")

Definindo o método de forceCallback.

E por fim o corpo rígido é anexado ao nodoDinamico, que será responsável apenas por realizar a colisão, para ter suas informações passadas para o nodo da entidade. Sempre quando são criados objetos que serão corpos rígidos ou junções, estes devem ser armazenados em uma lista para que continuem a existir no sistema.

# anexando a esfera para o nodo dinamico corpoPe.attachToNode( self.nodoDinamico ) #armazenando os objetos físicos self.corpos['corpoPe'] = corpoPe self.corpos['UpVectorY'] = upVectorY self.corpos['UpVectorX'] = upVectorX

92
Anexando a esfera para o nodo dinâmico.

Assim a implementação da criação do corpo rígido do ator esta feito. O passo seguinte será a definição do método de callback atorControleCallback( ), que irá realizar o cálculo de direção do ator e aplicar as forças para fazê-lo mover. Antes de implementa-lo, serão definidos alguns métodos para realizar o controle de movimentação, como andar para frente e virar para os lados. No que se diz de mover-se para frente, será apenas um indicador para informar ao método atorControleCallback( ) que o personagem encontra-se em movimento. Isto fará com que o método saiba que o corpo deverá encontrar-se em movimento, para então aplicar as forças e faze-lo mover. Isto será alcançado simplesmente informando à variável self.movimento o valor 1, e para fazer com que o ator não se mova mais, self.movimento será 0. Isto funcionará da forma que multiplicando o vetor de força por 1, conservará o vetor, multiplicando por 0, irá zerar o vetor e nenhum movimento será feito. Abaixo será a implementação do método andaParaFrente( ).

def andaParaFrente( self ): # metodo para andar para frente # indicando que se encontra em movimento self.movimento = 1

Implementação do método andaParaFrente( ).

Além do método de andar para frente, serão implementados os método virarParaDireita( ) e virarParaEsquerda( ). Para fazer o personagem virar para os lados, será utilizada a variável self.anguloDirecao, que irá armazenar o ângulo que será usado para o personagem que irá se orientar. Para virar para direita, acrescenta-se um valor angular, que no caso será utilizado 5 graus, e para virar à esquerda irá subtrair 5 graus. Estes método realizarão somente estes cálculos. O restante do calculo se localizará no método atorControleCallback( ). Por padrão, no construtor da classe, anguloDirecao será 0.

def virarParaEsquerda( self ): # metodo para virar à esquerda self.anguloDirecao -= 5 def virarParaDireita( self ): # metodo para virar à direita self.anguloDirecao += 5

93
Implementações dos métodos virarParaEsquerda( ) e virarParaDireita( ).

Por fim, no que diz aos método de controle de movimento do ator, será implementado o método para fazer com que o ator não se mova, chamado pararDeAndar( ). Este método irá atribuir à variável self.movimento o valor 0, fazendo-o parar de se mover.

def pararDeAndar(self): # metodo para parar de andar self.movimento = 0

Implementação do método pararDeAndar( ).

Com os métodos de controle de movimento implementados, será definido então o método atorControleCallback( ), que é o núcleo do funcionamento do ator. Será nele que ficará todo o código para fazer com que o corpo se comporte fisicamente. Uma vez implementado, o método setStandardForceCallback( ) que define a gravidade é cancelado, então deve-se aplicar a força da gravidade manualmente. Por se tratar de um método callback do Newton, serão utilizados alguns métodos que somente podem ser utilizado dentro de um callback, como addForce( ), addTorque( ), setOmega( ), setVelocity( ), entre outros.

def atorControleCallback( self, body ): # método de callback, que será chamado a cada atualização do mundo

Implementação do método atorControleCallback( ).

Para fazer o corpo se mover, serão necessárias diversas operações como limitação do angulo de direção, transformação do ângulo em um vetor de rotação no plano xz, aplicar a força de gravidade no corpo, definir a velocidade de movimento, sincronizar o sceneNode da entidade com o sceneNode dinâmico e orientar a direção da esfera. O funcionamento do deslocamento é aplicar uma força de arrasto na esfera e ao mesmo tempo aplicar uma nova orientação à junção UpVectorX, de forma que sempre a junção de direção e o vetor de força apontem para a mesma direção, conforme imagem abaixo.

94

Ilustração 18: Vetores e constraints aplicados à esfera. Fonte: Fonte Nossa.

Esta operação será alcançada da seguinte forma. Primeiro deve-se verificar os limites do ângulo de rotação, não pode ser maior que 360 e nem menor que 0. Caso seja maior que 360, então completou uma volta e o ângulo será 0, sendo menor que 0, então passa a ser 360.

# ajustando os valores do angulo de direcao if self.anguloDirecao > 360.0: self.anguloDirecao = 0 elif self.anguloDirecao < 0: self.anguloDirecao = 360

Ajuste dos valores do ângulo de direção.

Após esta filtragem do ângulo, deve-se calcular o vetor de rotação, que será usado para determinar a velocidade e a orientação da esfera. Isto é feito convertendo-se o ângulo para radianos, de forma a calcular o seno e cosseno deste ângulo. O movimento de virar será

95 no plano xz, então estes valores serão aplicados no eixo x, o seno, e no eixo z -cosseno, tendo como resultado um vetor de direção no plano desejado.

# convertendo o angulo em radianos d = ogre.Degree( self.anguloDirecao ).valueRadians( ) # calculando a posicao que a forca devera ser aplicada self.dirVect = ogre.Vector3( Sin( d ), 0.0, -Cos( d ) )

Definição do vetor de direção.

O passo seguinte é aplicar a força de gravidade. Isto é feito utilizando-se o método body.addForce( ), recebe como parâmetro o vetor do tipo Ogre.Vector3 que indica um valor nos eixos xyz. A gravidade é aplicada no eixo y, para fazer o objeto cair, o valor que será informado deverá estar na unidade de m/s^2. Lembrando que a gravidade da Terra é de 9.8 m/s^2, logo, será aplicado ao eixo y o valor -9.8 multiplicado pela massa do corpo.

# aplicando a força de gravidade body.addForce( (0, -9.8 * self.massa, 0) )

Aplicação da força de gravidade.

Com a gravidade aplicada, será feito o processo de atribuir ao corpo uma força para se mover. Este é um ponto que irá sofrer muitas críticas. Não é recomendado a utilização dos métodos setVelocity( ) e setOmega( ). Estes métodos atribuem uma velocidade linear e velocidade rotacional (torque) ao corpo, podendo alcançar os efeitos físicos desejáveis, mas funcionando contra o princípio dinâmico, onde que a velocidade é a conseqüência da força aplicada ao corpo. Porém para fins didáticos e para fazer com que o corpo se mova da forma desejada, sem dispender muito tempo na pesquisa de um modelo de movimentação adequado, será utilizado este movimento mais simples. O vetor final de velocidade será o dirVect, que possui valores insignificantes, os quais serão amplificados, multiplicado pela constante velocidade e pela variável movimento, caso seja 1, o termo será conservado e o dirVect possuirá um vetor de velocidade, sendo 0, os termos serão zerados e nenhuma velocidade será aplicada. A velocidade é aplicada no corpo buscando-se a velocidade atual do corpo, com o método body.getVelocity( ), que irá retornar um vetor com a velocidade nos três eixos. Em seguida esta multiplicação é feita nos eixos x e z, para então no eixo y conservar a velocidade que o corpo se encontra, que é a velocidade

96 gravitacional. E por fim a velocidade é atribuída através do método body.setVelocity( ), que recebe como parâmetro um vetor com a velocidade do corpo nos três eixos.

# buscando a velocidade do corpo vel = body.getVelocity( ) # calculando o vetor de deslocamento resultante.x = self.dirVect.x * self.velocidade * self.movimento resultante.z = self.dirVect.z * self.velocidade * self.movimento # conservando a velocidade da gravidade resultante.y = vel.y # ajustando a velocidade final da esfera body.setVelocity( resultante )

Ajustando a velocidade do corpo.

Agora deve-se atualizar a posição e orientação do sceneNode da entidade com o sceneNode do corpo rígido, sincronizando as posições, fazendo com que a parte visível do ator se mova também. Na sincronização da posição, é feito um ajuste da altura da entidade, pois o ponto no espaço do sceneNode do corpo rígido encontra-se no centro da esfera. Então para colocar a entidade no chão é feita a subtração da altura de um raio da posição do nodo do corpo rígido.

# atualizando a posicao e orientacao do personagem com a posicao e orientacao da esfera self.nodoEntidade.setPosition( self.nodoDinamico.getPosition( ) ogre.Vector3(0, self.raio, 0) ) self.nodoEntidade.setOrientation( self.nodoDinamico.getOrientation( ) )

Sincronizando a posição e orientação do personagem com a posição e orientação da esfera.

E por fim, é feita a orientação do sceneNode da esfera mudando-se o ponto que o UpVectorX irá apontar. Isto é alcançado utilizando-se o método setPin( ), informando o vetor de direção dirVect, fazendo o corpo rígido apontar para esta nova direção.

# fazendo a esfera mudar de orientacao upVectorX= self.corpos['UpVectorX'] upVectorX.setPin( self.dirVect )

Fazendo a esfera mudar de orientação.

97 Com isto conclui-se a implementação dinâmica do ator. Porém a aplicação não pode ser executada ainda, pois o ator não se encontra inscrito como observador dos dispositivos de entrada, o que se será visto no final da implementação desta classe. Será visto à seguir a implementação da animação, para fazer com que o ator não seja um personagem imóvel.

3.3.3

Animação

Com a dinâmica aplicada ao ator, deve-se fazer com que ele execute animações quando alguma ação é atribuída a ele, como andar. Se fosse possível executar a aplicação neste ponto, o ator iria se mover de um lado a outro do cenário, de forma estática. Este item irá cobrir animação com o Ogre3D, para complementar a movimentação do ator. A forma que o Ogre manipula as animações de esqueleto, é chamada de skinning. Para fazer com que a entidade seja animada, ela deverá possuir informações de sua animação em um arquivo separado com o mesmo nome do arquivo de malha, porém com a extensão .skeleton. Neste arquivo encontram-se as informações de todas as possíveis animações que a entidade possui. O processo de animação no Ogre é muito simples de implantar. A animação deverá ser carregada e configurada, e para executá-la, basta informar o tempo decorrido do processamento dos quadros para que o Ogre possa sincronizar o tempo da animação com o tempo de execução dos quadros. Nesta classe ator, serão usados três animações. Uma que é o ator andando, chamada 'Walk', outra que será executada quando estiver parado, 'Idle'. E uma que será ele atirando, 'Shoot'. Estas três animações deverão ser carregadas e armazenadas em uma lista na memoria de forma que possa ser recuperada depois para sua execução. Esta lista será declarada no construtor, juntamente com uma variável que irá armazenar a animação atual, que deverá ser executada.

self.animacaoAtual = None self.animacoes = { }

98
Definições das variáveis básicas de animação.

O passo seguinte é carregar todas as animações do ator, isto é feito utilizando o método entity.getAnimationState( ), que recebe como parâmetro uma string com o nome da animação. Este método retorna um objeto do tipo animationState, que contém a animação para ser executada. Com a animação carregada ela deverá ser configurada, para todas elas será informado que entre em repetição, através do método animationState.setLoop( ), passando como parâmetro Verdadeiro. E para fazê-la entrar em execução, deve ser informado através do método setEnabled( ) passando como parâmetro o valor Verdadeiro, de forma a fazer que a animação seja executada. E por fim, cada estado de animação será armazenado na lista self.animacoes, para que possa buscar a animação quando necessário. Ao final do carregamento da animação é atribuído uma animação inicial ao ator, 'Idle'.

# Carregando os estados das animações que serão utilizados for nomeAnim in ("Idle", "Walk", "Shoot"): # buscando a animação configurada dentro da entidade animationState = self.entidade.getAnimationState( nomeAnim ) # Informando para auto repetir animationState.setLoop( True ) # Informando reproduzir animationState.setEnabled( True ) # Informando para auto repetir self.animacoes[ nomeAnim ] = animationState # definindo automaticamente a animacao atual self.animacaoAtual = self.animacoes["Idle"]

Carregando as animações do ator.

Como a classe ator herda da classe ogre.FrameListener, então deverá ser implementado o método frameStarted( ), que será responsável por informar o tempo decorrido desde o ultimo quadro para sincronizar a animação com o tempo atual. Isto será feito sempre com a animação atual, independente qual ela seja.

def frameStarted( self, frameEvent ): self.animacaoAtual.addTime( frameEvent.timeSinceLastFrame ); return True;

Implementação do método frameStarted( ), para sincronizar a animação.

99 Neste ponto a animação encontra-se implementada. O que resta a fazer é definir qual será a animação que será executada. Existem três animações, uma andando, outra parado e outra atirando. A animação de andar, será atribuída quando o ator estiver movendo-se para frente. O método andarParaFrente( ), irá atribuir à animação atual a animação 'Walk', que encontra-se carregada na lista, conforme código abaixo.

# atribuindo a animacao de andar self.animacaoAtual = self.animacoes["Walk"]

Atribuindo a animação de andar, no método andarParaFrente( ).

No método pararAndar( ), irá atribuir a animação 'Idle', de forma que quando não estiver fazendo nada, o ator fique 'olhando em volta'. Este código será implementado no método pararAndar( ).

self.animacaoAtual = self.animacoes["Idle"]

Atribuindo a animação idle, no método pararAndar( ).

E por fim será atribuído a animação de atirar. Esta animação será implementado no método atirar( ), que à princípio apenas irá atribuir a animação 'Shoot' ao ator. Neste método poderá ser implementado todo o código que irá fazer que a entidade dispare um projétil. Isto não será implementado neste projeto pois irá fugir do foco principal.

def atirar( self ): # método para atirar self.animacaoAtual = self.animacoes["Shoot"]

Implementação do método atirar( ).

Com isto conclui-se a implementação de animações no Ator. Foi visto a criação do processo de animação básica utilizando o esqueleto definido dentro do personagem carregado, de forma a abstrair o seu funcionamento básico.

100 3.3.4 Configuração do Teclado

A classe Ator será controlada pelo usuário, então de forma a permitir esta intervenção, esta classe deverá ser um observador do InputManager, e para definir quais ações que deverão ser executadas quando alguma tecla específica for pressionada, deverá ser implementado o método updateKeyPressed( ) e updateKeyReleased( ). A interação com o usuário será feita através das teclas "W" para andar para frente, "A" para virar à esquerda, "D" para virar à direita e "Q" para atirar. Pode parecer estranho, mas a verificação se a tecla foi pressionada será feita no método frameStarted( ). Isto se deve que este método será sempre executado, mesmo quando nada estiver acontecendo no dispositivo de entrada, em contra partida ao método updateKeyPressed( ), que será executado somente quando alguma tecla for pressionada ou quando foi liberada, mas não será chamado enquanto estiver com alguma tecla pressionada. Este tipo de característica poderá ser implementado na classe InputManager( ), para alertar sempre quando alguma tecla estiver pressionada. Para contornar isto, o código seguinte será implementado no frameStarted( ), antes de executar a animação programada.

if self.teclaAtual == OIS.KC_W: self.andarParaFrente( ) elif self.teclaAtual == OIS.KC_A: self.virarParaEsquerda( ) elif self.teclaAtual == OIS.KC_D: self.virarParaDireita( ) elif self.teclaAtual == OIS.KC_Q: self.atirar( ) else: self.pararAndar( )

Decisão do que o ator fará quando uma determinada tecla for pressionada.

Os métodos updateKeyPressed( ) irá atualizar a variável self.teclaAtual com a tecla pressionada, e o método updateKeyReleased( ) irá informar que a variável self.teclaAtual não possui nenhum valor, atribuindo None a ela. Esta variável deverá ser declarada no construtor da classe com o valor None, pois à principio nenhuma ação será feita pelo usuário.

self.teclaAtual = None

Atribuindo o valor None à variável teclaAtual.

101

E então implementando os métodos updateKeyPressed( ) e updateKeyReleased( ).

def updateKeyPressed( self, arg ): self.teclaAtual = arg.key def updateKeyReleased( self, arg ): self.teclaAtual = None

Implementações dos métodos updateKeyPressed( ) e updateKeyReleased( ).

A última implementação nesta classe, será fazer a câmera sempre observar o ator, onde quer que ele esteja. Isto será feito criando-se um método chamado trackMe( ), este método irá receber como parâmetro a câmera criada no sistema, para fazê-la observar o ator. Este método irá atribuir a câmera a uma variável interna, chamada camera. Além disto, devese declarar a variável câmera no método __init__( ), com o valor None, de forma que quando o ator inicie, não ocorra erro pelo fato da variável não existir.

def trackMe( self, camera ): self.camera = camera

Implementação do método trackMe( ).

Em seguida deve-se implementar no método frameStarted( ), o código para fazer que a câmera aponte sempre ao ator. Como visto no capítulo II, a câmera possui um método chamado lookAt( ), este método faz com que a câmera observe o ponto específico no espaço. Então, ele será implementado no método frameStarted( ) para sempre que houver uma atualização de quadro, ela irá observar para a entidade. Porém se a variável valer None, nada deverá ser feito.

if self.camera != None: self.camera.lookAt( self.nodoEntidade.getPosition( ) )

Fazendo a câmera olhar para a o ator.

A classe Ator encontra-se implementada, agora deve-se configurá-la na classe TechDemo, no arquivo TechDemo.py, de forma a fazê-la funcionar. Será criado nesta classe mais um método, que será chamado de configuraAtores( ), que irá realizar a criação e

102 configuração dos atores envolvidos na aplicação. Primeiramente deve-se realizar a importação da classe Ator, com o import a seguir.

from ator import Ator

Importação da classe Ator na classe TechDemo.

Dentro do método configuraAtores( ) a classe ator será instanciada, passando os parâmetros world, qual a entidade que será o ator e qual é o nodoDinamico. Uma observação especial ao nodo dinâmico. Este nodo será responsável por armazenar o corpo rígido, para então sincronizar com o nodo o qual a entidade selecionada para representar o ator se localiza. Então será criado um nodo com o método createChildSceneNode( ). Com a instância ator definido, deverá em seguida executar o método para criar o personagem, ator.criaCaracter( ), que recebe como parâmetro o material para o corpo rígido, que será o material borracha. Ela também deverá se registrar como observadora à classe InputManager, de forma a ser notificada aos eventos do teclado.

def configuraAtores( self ): """ método para configurar os atores na aplicação """ # criando o nodo dinâmico dynamicsNode = self.sceneManager.getRootSceneNode().createChildSceneNode( "d" ) # instanciando a classe ator self.ator = Ator(self.World, self.sceneManager.getEntity( "robot" ), dynamicsNode ) # criando o caracter informando o materal borracha self.ator.criaCaracter( self.matBorracha ) # fazendo a camera sempre observar o ator self.ator.trackMe( self.camera ) # registrando como observador para KeyPressed e KeyReleased self.inputManager.registerObserver( self.inputManager.CT_KeyPressed, self.ator ) self.inputManager.registerObserver( self.inputManager.CT_KeyReleased, self.ator )

Implementação do método configuraAtores( ).

A chamada ao método configuraAtores( ) deverá ser feita imediatamente após à chamada ao método criaCorposRigidos( ), pois dependerá dos materiais que foram criados

103 neste método.

self.configuraAtores( )

Realizando a chamada ao método configuraAtores( ).

Neste item foi visto a implementação de uma classe Ator, simples, para demonstrar mais dinâmica e animações, para complementar o assunto do Newton e do Ogre3D. Esta classe é extremamente simples, mas funcional. O sistema de movimento da esfera pode não ser o ideal, mas demonstra como que é feita a aplicação das forças de movimento. Também foi implementado um processo de animação simples, mas para abstrair o funcionamento básico do sistema de animação do Ogre3D. Executando a aplicação o personagem poderá se movimentar pelo cenário colidindo com os objetos que foram criados anteriormente

104

4

INTERFACES E ÁUDIO

4.1

Interfaces - CEGUI

No desenvolvimento de Jogos e Simulações, além de toda estrutura gráfica e dinâmica, é necessário a elaboração de uma GUI3 para interagir com o usuário. Estas interfaces permitem a criação de janelas e de seus componentes, como botões, caixa de texto, lista de seleção, entre outros, como qualquer outra interface gráfica. Como o Ogre fornece apenas o ambiente gráfico, sem nenhum tipo de suporte a GUI, esta lacuna fica vaga, sendo de responsabilidade do desenvolvedor implementar esta interface. Então para evitar de escrever a interface do princípio, esta lacuna é preenchida pelo CEGUI (Crazy Eddie's GUI), liberando o desenvolvedor deste trabalho, pois o foco é desenvolver o jogo, e não o sistema GUI. Vale lembrar que o CEGUI não faz parte do Ogre, é um projeto à parte desenvolvido por um outro grupo, cujo foco é fornecer uma GUI onde não existe uma GUI disponível, no caso, Ogre. Porém o CEGUI é recomendado como o sistema de interface padrão com o Ogre, onde existem dezenas de projetos que a utilizam. O CEGUI realiza a implementação de uma GUI dentro do ambiente gráfico provido pelo Ogre, criando uma interface altamente personalizável, podendo definir skins4 entre outros atributos. Também com o CEGUI é possível capturar o mouse para que possa exibir o cursor na tela, o que até o momento não foi visto. A vantagem de se utilizar o CEGUI é o fato de ser facilmente implementado na aplicação, requerendo poucas linhas de código para colocá-lo em funcionamento. Suas telas são baseadas em um arquivo XML, bastando apenas especificar qual arquivo a ser carregado, que a interface estará carregada, podendo exibir na tela no momento que o desenvolvedor desejar. Também é possível criar os componentes em tempo de execução, atendendo assim qualquer tipo de demanda que possa ser necessária para a configuração da interface.

3 4

GUI – Graphical User Interface, é qualquer interface gráfica que será exibida para o usuário. Normalmente se consite de janelas, botões, áreas de texto, imagem, possuindo integração com o mouse. Skin é a personalização da aparência da interface, onde é afetada toda a aparência dos controles, fazendo com que a interface seja atraente.

105 1 4.1.1 Estrutura XML do CEGUI

A estrutura XML do CEGUI se constitui da definição de uma tag raiz chamada <GUILayout>, onde irá abrigar todos os demais componentes. A definição dos componentes na qual a interface será constituída, ficarão dentro das tags <Window>, especificando o tipo do componente a ser utilizado no atributo Type, e seu nome no atributo Name, juntamente com suas tags de propriedades referente ao tipo do componente em questão. Estas tags além de definir o componente, também é uma tag container, que é uma tag que poderá ter outros componentes dentro dela, como um quadro que possui botões dentro dele. A elaboração desta interface é feita definindo Window do tipo DefaultGUISheet, que é a área na qual a interface será montada. Dentro desta tag, é definido um Window do tipo FrameWindow, que é a estrutura visual da janela. A partir deste ponto são montados os componentes que constituirão a janela. A nomenclatura utilizada para as tags Window é baseada pela profundidade na hierarquia das tags Window. Se o frameWindow se chama “app/Janela”, então o botão de “Sair”, se chamará “app/Janela/btnSair”, e assim por diante. Os componentes são disponibilizados por um scheme (esquema), que deverá ser carregado ao iniciar o CEGUI. Na estrutura fornecida pelo Python-Ogre, o conjunto de componentes fornecidos é o TaharezLook o qual fornece os seguintes componentes:
TaharezLook/Button TaharezLook/RadioButton TaharezLook/Checkbox TaharezLook/Editbox TaharezLook/Titlebar TaharezLook/FrameWindow TaharezLook/ProgressBar TaharezLook/AlternateProgressBar TaharezLook/VUMeter (barra de progresso) TaharezLook/SliderThumb TaharezLook/Slider TaharezLook/HorizontalScrollbarThumb TaharezLook/HorizontalScrollbar TaharezLook/VerticalScrollbarThumb TaharezLook/VerticalScrollbar TaharezLook/Listbox TaharezLook/ComboDropList TaharezLook/ComboEditbox TaharezLook/Combobox TaharezLook/Spinner TaharezLook/StaticShared TaharezLook/StaticImage TaharezLook/StaticText TaharezLook/ListHeaderSegment TaharezLook/ListHeader TaharezLook/MultiLineEditbox TaharezLook/Tooltip TaharezLook/ScrollablePane TaharezLook/TabButton TaharezLook/TabContentPane TaharezLook/TabButtonPane TaharezLook/TabControl

106
TaharezLook/MenuItem TaharezLook/PopupMenu TaharezLook/Menubar TaharezLook/LargeVerticalScrollbarThumb TaharezLook/LargeVerticalScrollbar TaharezLook/ImageButton Lista de widgets suportados pelo TaharezLook.

Ao definir uma tag Window, é importante definir dentro dela as tags Property, que são as propriedades do componente especificado no atributo Type. Não é objetivo deste projeto exibir a relação das propriedades que cada componente possui, pois é uma lista extensa e foge de seu propósito. Porém existe três propriedades fundamentais à tag Window, que são:
•

UnifiedPosition: define a posição do componente na área. O valor informado é relativo ao componente pai, e varia de 0 a 1, onde que 1 é 100%, 0.5 é 50% e 0 é 0% da posição do espaço; UnifiedSize: define o tamanho do componente de forma relativa ao espaço do componente pai; Text: informa o nome do texto do componente.

•

•

Abaixo segue um exemplo de uma estrutura XML, que irá criar uma janela básica, em branco, ocupando toda a tela:

<?xml version="1.0" ?> <GUILayout> <Window Type="DefaultGUISheet" Name="raiz"> <Window Type="TaharezLook/FrameWindow" Name="TechDemo/MainWindow"> <Property Name="UnifiedSize" Value="{{1,0},{1,0}}" /> <Property Name="UnifiedPosition" Value="{{0,0},{0,0}}" /> <Property Name="DragMovingEnabled" Value="false" /> <Property Name="RollUpEnabled" Value="false" /> <Property Name="CloseButtonEnabled" Value="true" /> <Property Name="Text" Value="TechDemo" /> </Window> </Window> </GUILayout>

Exemplo de um arquivo XML com o layout de uma interface.

4.1.2

Implementação

107

A Implementação do CEGUI será feita criando-se uma classe chamada CEGUIManager, que irá gerenciar o CEGUI, evitando de distribuir os códigos por todo o sistema, tornando sua manutenção mais complicada, propiciando erros na aplicação. Ela deverá ser definida dentro um novo arquivo, chamado CEGUIManager.py, facilitando a manutenção do código. Esta classe irá requerer a importação de três bibliotecas, Ogre, OIS (será utilizado seus atributos para os códigos das teclas) e o CEGUI.

import ogre.renderer.OGRE as ogre import ogre.io.OIS as OIS import ogre.gui.CEGUI as CEGUI

Importações dos pacotes básicos na classe CEGUIManager.

Em seguida será implementada a classe CEGUIManager juntamente com seu método construtor.

class CEGUIManager: def __init__( self, janela, sceneManager ): # Criando o renderizador self.GUIRenderer = CEGUI.OgreCEGUIRenderer(janela, ogre.RENDER_QUEUE_OVERLAY, False, 0, sceneManager) # Criando o sistema self.GUISystem = CEGUI.System(self.GUIRenderer) # configurando o CEGUI self._setupCEGUI( )

Implementação da classe CEGUIManager.

Para realizar a implementação do CEGUI no sistema, deve-se criar dois objetos, um é o renderizador, que será o canvas onde os componentes serão desenhados. Esta classe faz a união entre o Ogre e o CEGUI, e o outro objeto é o sistema CEGUI. O objeto renderizador é definido através do método CEGUI.OgreCEGUIRenderer( ), que recebe os seguintes parâmetros:
• •

window: janela alvo que a interface será renderizada; queue_id: especifica em qual momento a GUI deverá aparecer dentro do processo de renderização do Ogre;

108
•

post_queue: verdadeiro para a GUI ser renderizada após o queue_id, falso para ser renderizada antes da queue_id; max_quads: obsoleto. Informar sempre 0; sceneManager: aponta para um sceneManager alvo para o qual a GUI será renderizada;

• •

Com renderizador do CEGUI criado, deve-se então criar o objeto system, com o método CEGUI.System( ). Este método recebe como parâmetro o renderizador criado anteriormente. Neste ponto o CEGUI estará iniciado, porém existem outras configurações que devem ser realizadas para coloca-lo em funcionamento. Estas configurações ficarão em um novo método privado, que será chamado _setupCEGUI( ). Neste método será configurado qual esquema que será utilizado pelo CEGUI, lembrando que o Python-Ogre fornece apenas o esquema TaharezLook. Deverá ser feita a configuração do cursor do mouse, a configuração da fonte que será usada para os textos da interface e carregar as interfaces que serão utilizadas pela aplicação. A implementação deste método deverá ser conforme abaixo:

##metodo privado que irá configurar os parâmetros do CEGUI def _setupCEGUI( self ): # carregando o esquema a ser utilizado pelo CEGUI CEGUI.SchemeManager.getSingleton( ).loadScheme( "TaharezLookSkin.scheme" ) # definindo um cursor para o mouse self.GUISystem.setDefaultMouseCursor( "TaharezLook", # configurando uma fonte padrao para a interface self.GUISystem.setDefaultFont( "BlueHighway-12" ) # carregando duas interfaces para ser utilizadas self.GUI_COMPOSITOR = CEGUI.WindowManager.getSingleton( ) .loadWindowLayout("CompositorDemo.layout") self.GUI_SHADOWS = CEGUI.WindowManager.getSingleton( ) .loadWindowLayout( "shadows.layout") "MouseArrow" )

Método de configuração do CEGUI.

Na primeira linha é feito o carregamento do esquema a ser usado pelo CEGUI, o TaharezLook. Para que este arquivo possa ser carregado, o caminho deste recurso deverá estar apontado dentro do arquivo resources.cfg. Na linha seguinte, é feita a definição do cursor do

109 mouse. Este método recebe dois parâmetros:
•

imageSetName: nome do imageSet que irá conter o mapeamento de onde será buscado a imagem; imageName: nome da imagem a ser utilizado como cursor do mouse.

•

No diretório media/gui/, possui um arquivo chamado TaharezLook.imageset, que realiza o mapeamento para todas as imagens que serão usadas na interface. O parâmetro imageName utilizado(“MouseArrow”), irá se encontrar neste arquivo. Na terceira linha é feita a configuração fonte padrão a ser utilizada na interface, com o método GUISystem.setDefaultFont( ). Este método recebe um parâmetro, que é o nome da fonte, a qual é configurada através de um arquivo XML, que define seus atributos, como nome, tamanho, resolução, entre outros. O caminho do diretório que contém este arquivo deverá estar disponível no dentro de resources.cfg. Segue abaixo uma amostra do arquivo de configuração da fonte. O nome da fonte a ser utilizada, esta definida no atributo Name.

<?xml version="1.0" ?> <Font Name="BlueHighway-12" Filename="bluehigh.ttf" Type="FreeType" Size="12" NativeHorzRes="800" NativeVertRes="600" AutoScaled="true"/>

Exemplo da configuração da fonte.

Neste método é feito o carregamento dos layouts que serão utilizados pela aplicação, em uma lista que estará disponível no sistema. O carregamento destas interfaces é feita utilizando o método loadWindowLayout( ), que recebe como parâmetro o arquivo XML que possui o layout para montar a interface. Estes objetos são as telas carregadas, as quais ficarão disponíveis no sistema de forma que possam ser chamadas quando houver necessidade. Para exibir uma interface, deverá ser utilizado o método self.GUIRenderer.setGUISheet( ), que recebe como parâmetro o objeto que possui a interface carregada. A implementação deste método será visto mais adiante. O passo seguinte na implementação, é fazer com que o CEGUI passe a responder aos eventos do teclado e mouse. Como CEGUI não trabalha com gerenciamento dos dispositivos de entrada, será necessário realizar uma injeção destes eventos, de forma que

110 possa responde-los. Utilizando-se do benefício do padrão de projeto Observer na classe OISManager, esta classe irá realizar a implementação de todos os métodos de notificação para realizar a injeção destes eventos, que são: updateMouseMoved( ); updateMousePressed( ); updateMouseReleased( ); updateKeyReleased( ); updateKeyPressed( ), A injeção de eventos é feita utilizando os métodos: injectMouseMove( ); injectMouseButtonDown( ); injectMouseButtonUp( ); injectkeyDown( ); injectkeyUp( ); injectChar( ); Para o caso da injeção de eventos dos botões do mouse, é necessário realizar uma conversão dos botões do OIS para o CEGUI. Isto é feito utilizando-se o método _convertOISMouseButtonToCegui( ), emprestado do arquivo CEGUI_Framework.py, da demonstração do Python-Ogre. O código abaixo realiza a implementação dos métodos observadores, realizando a injeção dos eventos no CEGUI.

• • • • •

• • • • • •

## METODOS PARA REALIZAR A INJEÇÃO DE EVENTOS NO CEGUI ## ## método observador do evento de MouseMoved def updateMouseMoved( self, arg ): CEGUI.System.getSingleton( ).injectMouseMove( arg.get_state( ).X.rel, arg.get_state( ).Y.rel ) ## método observador do evento de MousePressed def updateMousePressed( self, arg, id ): CEGUI.System.getSingleton( ).injectMouseButtonDown( self._convertOISMouseButtonToCegui( id ) )

111

## método observador do evento de MouseReleased def updateMouseReleased( self, arg, id ): CEGUI.System.getSingleton( ).injectMouseButtonUp( self._convertOISMouseButtonToCegui( id ) ) ## método observador do evento de KeyPressed def updateKeyPressed( self, arg ): CEGUI.System.getSingleton( ).injectKeyDown( arg.key ) CEGUI.System.getSingleton( ).injectChar( arg.text ) ## método observador do evento de KeyReleased def updateKeyReleased( self, arg ): CEGUI.System.getSingleton( ).injectKeyUp( arg.key ) ## extraido do CEGUI_Framework.py, que acompanha os demos do Python-Ogre def _convertOISMouseButtonToCegui( self, buttonID ): if buttonID == 0: return CEGUI.LeftButton elif buttonID == 1: return CEGUI.RightButton elif buttonID == 2: return CEGUI.MiddleButton elif buttonID == 3: return CEGUI.X1Button else: return CEGUI.LeftButton

Implementação dos métodos de injeção de eventos no CEGUI.

Esta classe irá também fornecer o acesso à interface gráfica, que será feito quando pressionar a tecla F1, exibindo uma determinada tela, pressionando F2, irá aparecer outra tela. Para decidir qual interface exibir, primeiro deve ser implementado um método, que permita alterar a interface tanto de dentro da classe, como de fora da classe também. Este método se chamará exibeGUI( ), que irá executar GUISystem.setGUISheet( ), que realiza a exibição de uma interface na tela.

## Exibe uma GUI def exibeGUI( self, GUI ): self.GUISystem.setGUISheet( GUI )

Implementação do método exibeGUI( ).

Para realizar esta troca, o código seguinte deverá se localizar dentro do método updateKeyPressed( ), antes de realizar a injeção de eventos.

112
if arg.key == OIS.KC_F1: self.exibeGUI( self.GUI_COMPOSITOR ) elif arg.key == OIS.KC_F2: self.exibeGUI( self.GUI_SHADOWS )

Implementação da mudança da GUI, no método updateKeyPressed( ).

De forma a fazer com que esta classe funcione, no arquivo TechDemo.py, deverá ser feito o registro de observador à classe OISManager,

def registraCEGUIObservadores( self ): self.inputManager.registerObserver( self.CEGUIManager ) self.inputManager.registerObserver( self.CEGUIManager ) self.inputManager.registerObserver( self.CEGUIManager ) self.inputManager.registerObserver( self.CEGUIManager ) self.inputManager.registerObserver( self.CEGUIManager )

InputManager.CT_KeyPressed, InputManager.CT_KeyReleased, InputManager.CT_MouseMoved, InputManager.CT_MousePressed, InputManager.CT_MouseReleased,

def cancelaCEGUIObservadores( self ): self.inputManager.unregisterObserver( self.CEGUIManager ) self.inputManager.unregisterObserver( self.CEGUIManager ) self.inputManager.unregisterObserver( self.CEGUIManager ) self.inputManager.unregisterObserver( self.CEGUIManager ) self.inputManager.unregisterObserver( self.CEGUIManager )

InputManager.CT_KeyPressed, InputManager.CT_KeyReleased, InputManager.CT_MouseMoved, InputManager.CT_MousePressed, InputManager.CT_MouseReleased,

Métodos registraCEGUIObservadores( ) e cancelaCEGUIObservadores( ).

Com a classe CEGUIManager implementada, deve-se criar uma instância para que possa ser ativada no sistema. Pelo fato de requerer como parâmetro a janela de renderização e um sceneManager, a criação deste objeto deverá ficar imediatamente após a chamada do método self.criaCenario( ), dentro do método run( ). E em seguida deve-se executar o método registraCEGUIObservadores( ) de forma a fazer que responda aos eventos provenientes dos dispositivos de entrada.

self.CEGUIManager = CEGUIManager( self.janela, self.sceneManager ) self.registraCEGUIObservadores( )

Instanciando a classe CEGUIManager e registrando seus eventos.

113

Ao executar a aplicação, pressionando a tecla F1 e F2, irá trocar entre as telas carregadas, conforme imagens abaixo.

Ilustração 19: Tela com a tecla F1 pressionada. Fonte: Fonte Nossa.

114

Ilustração 20: Tela com a tecla F2 pressionada. Fonte: Fonte Nossa.

4.1.3

Respondendo aos eventos do CEGUI

Com o CEGUI implementado, deve-se capturar os disparos dos eventos proveniente da interface. Isto funciona através do padrão de projeto Observer, que registra o método para um determinado evento de um objeto da interface, que quando disparado, o método registrado será executado, semelhante a um sistema de callback. Para registrar estes eventos, deverá ser criado um método que ficará responsável por agrupar todos os registros. Para fazer este registro, deve-se primeiro buscar o objeto o qual deseja-se utilizar para registrar o evento. Isto é feito utilizando-se o método WindowManager.getWindow( ), que recebe como parâmetro uma string com o nome do objeto. Em seguida, utiliza-se o método subscribeEvent( ), para registrar o método para um evento específico. Este método recebe três parâmetros: evento, que indica para qual evento que o método será registrado, self e uma string com o nome do método a ser registrado.

115 Será implementado a funcionalidade para sair da aplicação, pressionando o botão “Exit Demo”. Para isto, será feito um método chamado configuraEventos( ), na classe CEGUIManager. Neste método serão implementados os registros aos eventos do CEGUI. Sua chamada deverá ficar no construtor da classe CEGUIManager, como último método a ser executado.

def configuraEventos( self ): wmgr = CEGUI.WindowManager.getSingleton() wmgr.getWindow( "ExitDemoBtn").subscribeEvent( CEGUI.PushButton.EventClicked, self, "abandonarAplicacao")

Registrando o método abandonarAplicacao a um evento no CEGUI.

Uma vez com registro feito, deve-se implementar o método registrado, abandonarAplicacao( ). Para fazer com que esta classe faça com que a aplicação saia, será atribuído uma instância do método BasicoListener.requestShutdown( ) para uma variável nesta classe, para que não ocorra acoplamento entre o CEGUIManager e o BasicoListener. Esta variável se chamará self.metodoSair e deverá ser atribuído com o valor None no construtor desta classe. A atribuição desta instância deverá ser feita após a chamada do método configuraFrameListener( ), na classe TechDemo, pois é nesta classe que seu objeto é criado.

self.configuraFrameListener( ) self.CEGUIManager.metodoSair = self.basicoListener.requestShutdown

Atribuindo a instância do método basicoListener.requestShutdown( ) à variável CEGUIManager.metodoSair.

Ao executar a aplicação, pressioando a tecla F1, irá aparecer a interface que possui o botão “Exit Demo”, ao clicar a aplicação deverá ser abandonada. Com isto conclui-se os registros aos eventos dos componentes do CEGUI. Esta foi uma abordagem bastante simples, mas didática, demonstrando como que pode ser alcançado.

116 4.2 Áudio com OgreAL

OgreAL é uma biblioteca que realiza o encapsulamento do OpenAL, de forma a facilitar a integração com o Ogre3D. O OpenAL fornece toda a plataforma de áudio necessária para a aplicação, permitindo trabalhar com sons que se movem em um espaço tridimensional. Neste tópico será abordado a implementação básica do OgreAL, demonstrando como que poderá ser implementado na aplicação.

4.2.1

Implementação

Para implementar o OgreAL, deve-se importar sua biblioteca, no pacote ogre.sound.OgreAL. O passo seguinte é instanciar o OgreAL, que irá iniciar o sistema de áudio. Isto será feito no método configuraAudio( ), na classe TechDemo. À princípio será feito apenas a instanciação da classe, porém futuras implementações de configuração do dispositivo de áudio, deverão ser feitas neste método. A chamada a este método deverá ficar no método run( ), preferencialmente após à chamada ao método configuracaoVisual( ).

def configuraAudio( self ): self.soundManager = OgreAL.SoundManager( )

Método configuraAudio( ). Instanciando a classe OgreAL.SoundManager( ).

Uma vez iniciado, o sistema de áudio esta pronto para o uso. Para fazer o uso de um som, deverá ser criado uma instância para o objeto som, que irá conter um arquivo de som carregado. Este objeto poderá ser anexado a um sceneNode, ganhando mobilidade. Neste demonstração, será colocado um ponto de som no ator, que quando andar, poderá notar o sistema de posicionamento do som. O código seguinte ficará no método configuraAtores( ), que irá carregar um arquivo de audio e anexá-lo ao sceneNode dynamicsNode.

117
sound = self.soundManager.createSound("drumloop", "drumloop.wav", True) dynamicsNode.attachObject( sound ) sound.play( )

Carregando um arquivo de som e anexando-o a um sceneNode.

Para fazer o som tocar, utiliza-se o método play( ). A qualquer momento o som pode ser parado, com o método stop( ) ou então pausado com o método pause( ). Ao executar a aplicação, quando andar com o personagem, verá que o som irá se deslocar para direção que o personagem se encontra.

118

CONCLUSÃO
O que foi visto é apenas uma pequena parte no desenvolvimento de jogos, o qual envolve diversas outras áreas, como artes gráficas, modelagem de objetos, produção de áudio, roteiros, scripts. Porém esta parte é o núcleo desta produção, que irá fundir estas áreas em um produto. Não foi possível abordar todos os assuntos referente aos sistemas demonstrados, como neblina, partículas, billboarding, junções dinâmicas entre outros, mas fica sendo um degrau na escadaria do aprendizado.Como foi visto, existe na Internet uma grande quantidade de recursos e documentações disponíveis para ser consultada. Espera-se que este trabalho tenha cumprido seus objetivos, principalmente no que se diz a respeito de despertar o interesse sobre o desenvolvimento de jogos, mostrando através de um tutorial o desenvolvimento de uma demonstração de um jogo. Para saber mais, recomenda-se fazer uma visita às páginas informadas na referência bibliográfica, que existe uma enorme quantidade de recursos como tutoriais, fóruns de discussões, exemplos de código entre outros. Pode-se considerar esta demonstração uma base para o desenvolvimento de jogos e simulações mais complexos. E para quem leu e executou a demonstração desta monografia, um desafio é lançado, procurar aprimorar o que foi passado, implementando novas funcionalidades, expandindo a demonstração para algo mais concreto, pois conhecimento gera conhecimento.

119

ANEXOS
Anexo 1 – Dotscene.py

#!/usr/bin/python """ this file parses .scene node (dotscene) files and creates them in OGRE with user data Doesn't do any fancy stuff (skydome, XODE, lightmapping, etc) but you can use this as a base for adding those features.) cpp: http://www.ogre3d.org/wiki/index.php/DotScene_Loader_with_User_Data_Class """ from xml.dom import minidom, Node import string import ogre.renderer.OGRE as ogre

class DotScene: def __init__ (self, fileName, sceneManager, rootNode): self.fileName = fileName self.sceneManager = sceneManager self.ogreRoot = rootNode nodes = self.findNodes(minidom.parse(self.fileName).documentElement,'nodes') self.root = nodes[0].childNodes self.parseDotScene() # allows self['nodeName'] to reference xml node in '<nodes>' def __getitem__ (self,name): return self.findNodes(self.root,name) def parseDotScene (self): # TODO: check DTD to make sure you get all nodes/attributes # TODO: Use the userData for sound/physics i = 0 for node in self.root: i = i + 1 if node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'node': # create new scene node #, ogre.Vector3(0, 0, 0), ogre.Quaternion.IDENTITY newNode = self.sceneManager.getRootSceneNode().createChildSceneNode( str(node.attributes['name'].nodeValue) )

#position it pos = self.findNodes(node, 'position')[0].attributes newNode.position = (float(pos['x'].nodeValue), float(pos['y'].nodeValue), float(pos['z'].nodeValue)) # rotate it

120
rot = self.findNodes(node, 'rotation')[0].attributes newNode.orientation = ogre.Quaternion(float(rot['qw'].nodeValue), float(rot['qx'].nodeValue), float(rot['qy'].nodeValue), float(rot['qz'].nodeValue)) # scale it scale = self.findNodes(node, 'scale')[0].attributes newNode.scale = (float(scale['x'].nodeValue), float(scale['y'].nodeValue), float(scale['z'].nodeValue)) # is it a light? try: thingy = self.findNodes(node, 'light')[0].attributes name = str(thingy['name'].nodeValue) attachMe = self.sceneManager.createLight(name) ltypes={'point':ogre.Light.LT_POINT,'directional':ogre.Light .LT_DIRECTIONAL,'spot':ogre.Light.LT_SPOTLIGHT,'radPoint':ogre.Light.LT_POINT} try: attachMe.type = ltypes[thingy['type'].nodeValue] except IndexError: pass lightNode = self.findNodes(node, 'light')[0] try: tempnode = self.findNodes(lightNode, 'colourSpecular')[0] attachMe.specularColour = (float(tempnode.attributes['r'].nodeValue), float(tempnode.attributes['g'].nodeValue), float(tempnode.attributes['b'].nodeValue), 1.0) except IndexError: pass try: tempnode = self.findNodes(lightNode, 'colourDiffuse')[0] attachMe.diffuseColour = (float(tempnode.attributes['r'].nodeValue), float(tempnode.attributes['g'].nodeValue), float(tempnode.attributes['b'].nodeValue), 1.0) except IndexError: pass if thingy['type'].nodeValue == 'spot': dirV = -newNode.getPosition() dirV.normalise() newNode.setDirection(dirV) print attachMe print 'added light: "%s"' % name except IndexError: pass # is it an entity? try: thingy = self.findNodes(node, 'entity')[0].attributes castShadow = 'true'; name = str(thingy['name'].nodeValue) mesh = str(thingy['meshFile'].nodeValue) try:

121
castShadows = str(thingy['castShadows'].nodeValue) except: castShadows = 'true' print 'adding entity: "%s" %s' % (name, mesh) attachMe = self.sceneManager.createEntity(name,mesh) attachMe.setNormaliseNormals(True) attachMe.castShadows = (castShadows == 'true') print 'added. ' except IndexError: pass # is it a camera? # TODO: there are other attributes I need in here try: thingy = self.findNodes(node, 'camera')[0].attributes name = str(thingy['name'].nodeValue) fov = float(thingy['fov'].nodeValue) projectionType= str(thingy['projectionType'].nodeValue) attachMe = self.sceneManager.createCamera(name) #attachMe.FOVy = fov cameraNode = self.findNodes(node, 'camera')[0] try: tempnode = self.findNodes(cameraNode, 'clipping')[0] attachMe.nearClipDistance = float(tempnode.attributes['near'].nodeValue) attachMe.farClipDistance = float(tempnode.attributes['far'].nodeValue) except IndexError, e: print str(e) pass print 'added camera: "%s" fov: %f type: %s clipping: %f,%f' % (name, fov, projectionType,attachMe.nearClipDistance,attachMe.farClipDistance) except IndexError: pass # attach it to the scene try: newNode.attachObject(attachMe) except: print "could not attach:",node.nodeName def findNodes (self,root, name): out=minidom.NodeList() if root.hasChildNodes: nodes = root.childNodes for node in nodes: if node.nodeType == Node.ELEMENT_NODE and node.nodeName == name: out.append(node) return out

Código fonte do dotscene.py. Extraído da demonstração do Python-Ogre.

122 Anexo 2 - resources.cfg

[Bootstrap] Zip=../media/packs/OgreCore.zip # Resource locations to be added to the default path [General] FileSystem=../media FileSystem=../media/fonts FileSystem=../media/sounds FileSystem=../media/Audio FileSystem=../media/materials/programs FileSystem=../media/materials/scripts FileSystem=../media/materials/textures FileSystem=../media/models FileSystem=../media/overlays FileSystem=../media/particle FileSystem=../media/primitives FileSystem=../media/ode FileSystem=../media/gui Zip=../media/packs/cubemap.zip Zip=../media/packs/cubemapsJS.zip Zip=../media/packs/dragon.zip Zip=../media/packs/fresneldemo.zip Zip=../media/packs/ogretestmap.zip Zip=../media/packs/skybox.zip

Conteúdo do arquivo resources.cfg

Anexo 3 - plugins.cfg

# Defines plugins to load ## User this for Windows # Define plugin folder PluginFolder=../../plugins Plugin=RenderSystem_GL.dll Plugin=RenderSystem_Direct3D9.dll Plugin=Plugin_ParticleFX.dll Plugin=Plugin_BSPSceneManager.dll Plugin=Plugin_OctreeSceneManager.dll Plugin=Plugin_CgProgramManager.dll

Conteúdo do arquivo plugins.cfg

123 Anexo 4 – Scene.xml
<scene formatVersion="0.2.0"> <nodes> <node name="plane_1"> <position x="0.0" y="0.0" z="0.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="30.0" y="1.0" z="30.0"/> <entity name="plane_1" meshFile="plane.mesh"/> </node> <node name="plane_2"> <position x="0.0" y="0.1" z="0.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="15.0" y="1.0" z="15.0"/> <entity name="plane_2" meshFile="plane.mesh"/> </node> <node name="tudorhouse_1"> <position x="-50.0" y="28.0" z="100.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="0.050" y="0.050" z="0.050"/> <entity name="tudorhouse_1" meshFile="tudorhouse.mesh"/> </node> <node name="tudorhouse_2"> <position x="-50.0" y="28.0" z="0.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="0.050" y="0.050" z="0.050"/> <entity name="tudorhouse_2" meshFile="tudorhouse.mesh"/> </node> <node name="tudorhouse_3"> <position x="-100.0" y="28.0" z="0.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="0.050" y="0.050" z="0.050"/> <entity name="tudorhouse_3" meshFile="tudorhouse.mesh"/> </node> <node name="tudorhouse_4"> <position x="-150.0" y="28.0" z="0.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="0.050" y="0.050" z="0.050"/> <entity name="tudorhouse_4" meshFile="tudorhouse.mesh"/> </node> <node name="tudorhouse_5"> <position x="50.0" y="28.0" z="40.0"/> <rotation qx="0.0" qy="0.71" qz="0.000000" qw="0.71"/> <scale x="0.050" y="0.050" z="0.050"/> <entity name="tudorhouse_5" meshFile="tudorhouse.mesh"/> </node> <node name="tudorhouse_6"> <position x="50.0" y="28.0" z="0.0"/> <rotation qx="0.0" qy="0.71" qz="0.000000" qw="0.71"/> <scale x="0.050" y="0.050" z="0.050"/> <entity name="tudorhouse_6" meshFile="tudorhouse.mesh"/> </node> <node name="tudorhouse_7"> <position x="50.0" y="28.0" z="-40.0"/> <rotation qx="0.0" qy="0.71" qz="0.000000" qw="0.71"/> <scale x="0.050" y="0.050" z="0.050"/> <entity name="tudorhouse_7" meshFile="tudorhouse.mesh"/> </node> <node name="athene"> <position x="0.0" y="23.0" z="50.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="0.3000" y="0.3000" z="0.300"/> <entity name="athene" meshFile="athene.mesh"/>

124
</node> <node name="robot"> <position x="0.0" y="0.0" z="100.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="0.2000" y="0.2000" z="0.200"/> <entity name="robot" meshFile="robot.mesh"/> </node> <node name="barrel_1"> <position x="30.0" y="5.0" z="100.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_1" meshFile="barrel.mesh"/> </node> <node name="barrel_2"> <position x="35.0" y="5.0" z="100.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_2" meshFile="barrel.mesh"/> </node> <node name="barrel_3"> <position x="40.0" y="5.0" z="100.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_3" meshFile="barrel.mesh"/> </node> <node name="barrel_4"> <position x="45.0" y="5.0" z="100.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_4" meshFile="barrel.mesh"/> </node> <node name="barrel_5"> <position x="30.0" y="5.0" z="90.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_5" meshFile="barrel.mesh"/> </node> <node name="barrel_6"> <position x="35.0" y="5.0" z="90.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_6" meshFile="barrel.mesh"/> </node> <node name="barrel_7"> <position x="40.0" y="5.0" z="90.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_7" meshFile="barrel.mesh"/> </node> <node name="barrel_8"> <position x="45.0" y="5.0" z="90.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_8" meshFile="barrel.mesh"/> </node> <node name="barrel_9"> <position x="30.0" y="33.0" z="100.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_9" meshFile="barrel.mesh"/> </node> <node name="barrel_10"> <position x="35.0" y="33.0" z="100.0"/>

125
<rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_10" meshFile="barrel.mesh"/> </node> <node name="barrel_11"> <position x="40.0" y="33.0" z="100.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_11" meshFile="barrel.mesh"/> </node> <node name="barrel_12"> <position x="45.0" y="63.0" z="100.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_12" meshFile="barrel.mesh"/> </node> <node name="barrel_13"> <position x="30.0" y="33.0" z="90.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_13" meshFile="barrel.mesh"/> </node> <node name="barrel_14"> <position x="35.0" y="33.0" z="90.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_14" meshFile="barrel.mesh"/> </node> <node name="barrel_15"> <position x="40.0" y="33.0" z="90.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_15" meshFile="barrel.mesh"/> </node> <node name="barrel_16"> <position x="45.0" y="33.0" z="90.0"/> <rotation qx="0.0" qy="0.000000" qz="0.000000" qw="0.0"/> <scale x="1.0" y="1.0" z="1.0"/> <entity name="barrel_16" meshFile="barrel.mesh"/> </node> </nodes> <externals> <item type="material"> <file name="Example.material"/> </item> </externals> <environment> <colourAmbient r="0.000000" g="0.000000" b="0.000000"/> <colourBackground r="0.056563" g="0.220815" b="0.400000"/> </environment> </scene>

126

REFERÊNCIA BIBLIOGRÁFICA
BLENDER. Blender Homepage. Disponível em: <http://www.blender.org/>. Acesso em: 14 Jun 2007. CEGUI. Tutorials - CEGUI Wiki. Disponível em: <http://www.cegui.org.uk/wiki/index.php/Tutorials>. Acesso em: 14 Jun 2007. CEGUI. FAQ - CEGUI Wiki. Disponível em: <http://www.cegui.org.uk/wiki/index.php/FAQ >. Acesso em: 14 Jun 2007. FREEMAN, Eric; FREEMAN, Elisabeth. Use a Cabeça! Padrões de Projetos. Rio de Janeiro: Editora Alta Books, 2005. 496 p. HOUAISS, Instituto Antônio. Dicionário Houaiss da Língua Portuguesa. 1 ed. Rio de Janeiro: Editora Objetiva, 2001. 2922p. JUNKER, Gregory. Pro Ogre3D Programming. 1 ed. New York. Apress, 2006. 288p. MICROSOFT. DirectX 9.0c End-User Runtime. Disponível em: <http://www.microsoft.com/downloads/details.aspx?FamilyID=0A9B6820-BFBB-47999908-D418CDEAC197&displaylang=en>. Acesso em: 14 Jun 2007. Newton Game Dynamics. Unofficial Newton Game Dynamics WIKI. Disponível em: <http://www.python.org/psf/license/>. Acesso em: 14 Jun 2007. OGRE 3D. About. Disponível em: <http://www.ogre3d.org/index.php?option=com_content&task=view&id=19&Itemid=105>. Acesso em: 14 Jun 2007. OGRE 3D. Ogre Tutorials - Ogre Wiki. Disponível em: <http://www.ogre3d.org/wiki/index.php/Ogre_Tutorials>. Acesso em: 14 Jun 2007. OGRE 3D. Ogre API Reference. Disponível em: <http://www.ogre3d.org/docs/api/html/index.html>. Acesso em: 14 Jun 2007. OGRE 3D. Ogre Manual - Shadows. Disponível em: <http://www.ogre3d.org/docs/manual/manual_67.html#SEC286>. Acesso em: 14 Jun 2007. OGRE 3D. OgreOde Walking Character - Ogre Wiki. Disponível em: <http://www.ogre3d.org/wiki/index.php/OgreOde_Walking_Character>. Acesso em: 14 Jun 2007. OGRE 3D. Blender dotScene Exporter - Ogre Wiki. Disponível em:

127 <http://www.ogre3d.org/wiki/index.php/Blender_dotScene_Exporter>. Acesso em: 14 Jun 2007. OGRE 3D. DotSceneFormat - Ogre Wiki. Disponível em: <http://www.ogre3d.org/wiki/index.php/DotSceneFormat>. Acesso em: 14 Jun 2007. OGRE 3D. OGRE Meshes Exporter - Ogre Wiki. Disponível em: <http://www.ogre3d.org/wiki/index.php/OGRE_Meshes_Exporter> OGRE MESHES EXPORTER. Documentação. Disponível em: <http://ogre.cvs.sourceforge.net/*checkout*/ogre/ogrenew/Tools/BlenderExport/ogrehelp/ogr emeshesexporter.html> Acesso em: 14 Jun 2007. OGRE 3D. Using OIS - Ogre Wiki. Disponível em: <http://www.ogre3d.org/wiki/index.php/Using_OIS>. Acesso em> 14 Jun 2007. OPENAL. Open AL Homepage. Disponível em: <http://www.openal.org/>. Acesso em: 14 Jun 2007. Python-Ogre. PyOgre - Ogre Wiki. Disponível em: <http://www.ogre3d.org/wiki/index.php/PyOgre>. Acesso em: 14 Jun 2007. Python-Ogre. Python-Ogre Homepage. Disponível em: <http://www.python-ogre.org/>. Acesso em: 14 Jun 2007. Python. About Python. Disponível em: <http://www.python.org/about/>. Acesso em: 14 Jun 2007. Python. Python License. Disponível em: <http://www.python.org/psf/license/>. Acesso em: 14 Jun 2007. RAPIDSVN. RapidSVN Homepage. Disponível em: <http://rapidsvn.tigris.org/>. Acesso em: 14 Jun 2007. SPE. SPE IDE - Stani's Python Editor. Disponível em: <http://pythonide.blogspot.com/>. Acesso em: 14 Jun 2007. WX PYTHON. wxPython Homepage. Disponível em: <http://www.wxpython.org/>. Acesso em: 14 Jun 2007. WIKIPEDIA. Python IDE - List of Integrated Development Enviroments for Python Programming Language. Disponível em: <http://en.wikipedia.org/wiki/List_of_integrated_development_environments_for_Python>. Acesso em: 14 Jun 2007.


				
DOCUMENT INFO
Shared By:
Stats:
views:3722
posted:10/30/2008
language:Latin
pages:127
Description: Monografia sobre desenvolvimento de jogos com ogre3D
Rafael Riedel Rafael Riedel
About