Avançar para o conteúdo principal

OpenGL Parte II

No artigo anterior criámos a base do nosso pequeno jogo que agora vamos expandir. O objetivo de hoje é ter uma função que lê os modelos exportados do Blender, ou de outro programa de modelação 3D, em formato Wavefront (extensão OBJ) e apresentar esse modelo no controlo SharpGL.

Neste nosso pequeno projeto os objetos têm de ser sempre formados por triângulos por isso quando exportamos no Blender temos de garantir que todas as faces são "trianguladas".

Fica aqui uma imagem com as opções de exportação a utilizar:


O ficheiro com o modelo tem linhas de vários tipos:
Vértices:
P.Ex: v 0.940139 -0.495046 0.940139
Coordenadas das texturas:
P.Ex: vt 0.000000 0.424059
Normais:
P.Ex: vn -0.000000 -1.000000 -0.000001
Faces ou triângulos:
P.Ex: f 1/1/1 2/2/1 3/3/1

Basicamente com estes três tipos de linhas e com uma imagem podemos moldar qualquer objeto.

Os vértices, como é fácil de perceber representam os pontos de cada triângulo (x,y,z), as coordenadas das texturas permitem aplicar a textura ao modelo corretamente (U,V), as normais vão permitir ao OpenGL calcular com melhor qualidade a iluminação do nosso jogo e por fim as faces que indicam quais os vértices que são unidos para formar os triângulos.

Agora criamos uma classe que vai conter as seguintes listas:

  • Lista de pontos - são os vértices do nosso modelo
  • Lista de triângulos - baseada na classe triangulo criada no artigo anterior
  • Lista de pontos 2d - classe igual à dos pontos mas só com 2 coordenadas uma vez que as coordenadas das texturas são 2D
  • Lista de normais - mais uma lista de pontos 3D


Para além destas listas precisamos de uma textura, que não é mais do que uma variável do tipo Texture, para a qual necessitamos de incluir o namespace SharpGL.SceneGraph.Assets.

Comecemos por criar a classe Ponto2D com o seguinte código:
class Ponto2D
    {
        float x, y;

        public Ponto2D()
        {
            x = 0;
            y = 0;
        }
        public Ponto2D(float px, float py)
        {
            x = px;
            y = py;
        }
        public void setX(float x)
        {
            this.x = x;
        }
        public void setY(float y)
        {
            this.y = y;
        }
        public float getX()
        {
            return x;
        }
        public float getY()
        {
            return y;
        }
        public void moveXY(float _x, float _y)
        {
            x += _x;
            y += _y;
        }
        public void moveXY(Ponto a)
        {
            x += a.getX();
            y += a.getY();
        }
    }

Agora a classe Modelo já descrita:
    class Modelo
    {
        List<Ponto> lPontos = new List<Ponto>();
        List<Triangulo> lTriangulos = new List<Triangulo>();
        List<Ponto2D> lCoordText = new List<Ponto2D>();
        List<Ponto> lNormais = new List<Ponto>();
        Texture textura;
        
    }

De seguida vamos acrescentar alguns métodos, começando pela função que vai permitir carregar a textura:
        public void CarregaTextura(OpenGL gl, string nome)
        {
            if (File.Exists(nome) == false) return;
            textura = new Texture();
            textura.Create(gl, nome);
        }

A próxima função vai carregar o modelo do ficheiro e preencher as listas, mas primeiro algumas alterações à classe triângulo, criada no artigo anterior. Temos de adicionar as coordenadas das texturas, uma coordenada para cada vértice do triângulo, adicionamos as normais, mais uma vez uma para cada vértice, e por fim uma variável que nos permite saber se temos normais definidas ou não.
Assim a classe triângulo fica com o seguinte aspeto:
    class Triangulo : Objeto
    {
        Ponto p1, p2, p3;
        Ponto2D tx1, tx2, tx3;
        Ponto n1, n2, n3;
        bool hasNormals = false;

Como nem sempre os modelos são exportados com normais definimos um construtor da classe que recebe as coordenadas das texturas e adicionamos uma função para definir as normais independente do construtor.
        public Triangulo(Ponto p1, Ponto p2, Ponto p3, Ponto2D tx1, Ponto2D tx2, Ponto2D tx3)
        {
            this.p1 = p1;
            this.p2 = p2;
            this.p3 = p3;
            this.tx1 = tx1;
            this.tx2 = tx2;
            this.tx3 = tx3;
            n1 = new Ponto(0, 0, 0);
            n2 = new Ponto(0, 0, 0);
            n3 = new Ponto(0, 0, 0);
            origem = new Ponto(0, 0, 0);
            rotacao = new Ponto(0, 0, 0);
            n1 = new Ponto(0, 0, 1);
            n2 = new Ponto(0, 0, 1);
            n3 = new Ponto(0, 0, 1);
            largura = 1;
            hasNormals = false;
        }
        public void defineNormais(Ponto n1, Ponto n2, Ponto n3)
        {
            this.n1 = n1;
            this.n2 = n2;
            this.n3 = n3;
            hasNormals = true;
        }

Agora já podemos tratar da classe Modelo e da função que carrega o modelo:
        public Modelo(string nome)
        {
            //ler o ficheiro
            StreamReader ficheiro = new StreamReader(nome);

            string linha;
            string[] campos;
            float x, y, z;
            while (ficheiro.Peek() != -1)
            {
                linha = "";
                linha = ficheiro.ReadLine();
                linha = linha.Trim();
                if (linha != null && linha[0] != '#')
                {
                    linha = linha.Replace(".", ",");   //troca . por ,
                    campos = linha.Split(' ');  //separa nos espaços
                    //vertices
                    if (linha[0] == 'v' && linha[1] != 't' && linha[1] != 'n')
                    {
                        x = float.Parse(campos[1]);
                        y = float.Parse(campos[2]);
                        z = float.Parse(campos[3]);
                        lPontos.Add(new Ponto(x, y, z));
                    }

                    //coord texturas
                    if (linha[0] == 'v' && linha[1] == 't')
                    {
                        float t1 = float.Parse(campos[1]);
                        float t2 = float.Parse(campos[2]);
                        lCoordText.Add(new Ponto2D(t1, t2));
                    }
                    //normais
                    if (linha[0] == 'v' && linha[1] == 'n')
                    {
                        float t1 = float.Parse(campos[1]);
                        float t2 = float.Parse(campos[2]);
                        float t3 = float.Parse(campos[3]);
                        lNormais.Add(new Ponto(t1, t2, t3));
                    }
                    //faces
                    if (linha[0] == 'f')
                    {
                        if (lCoordText.Count > 0)
                        {
                            string[] temp = campos[1].Split('/');
                            int t1 = int.Parse(temp[0]) - 1;
                            int t4 = int.Parse(temp[1]) - 1;
                            int n1 = 0;
                            if (lNormais.Count > 0)
                                n1 = int.Parse(temp[2]) - 1;

                            temp = campos[2].Split('/');
                            int t2 = int.Parse(temp[0]) - 1;
                            int t5 = int.Parse(temp[1]) - 1;
                            int n2 = 0;
                            if (lNormais.Count > 0)
                                n2 = int.Parse(temp[2]) - 1;

                            temp = campos[3].Split('/');
                            int t3 = int.Parse(temp[0]) - 1;
                            int t6 = int.Parse(temp[1]) - 1;
                            int n3 = 0;
                            if (lNormais.Count > 0)
                                n3 = int.Parse(temp[2]) - 1;
                            lTriangulos.Add(new Triangulo(lPontos[t1], lPontos[t2], lPontos[t3], lCoordText[t4], lCoordText[t5], lCoordText[t6]));
                            if (lNormais.Count > 0)
                                lTriangulos[lTriangulos.Count - 1].defineNormais(lNormais[n1], lNormais[n2], lNormais[n3]);
                        }
                        else if (lNormais.Count > 0)
                        {
                            string[] temp = campos[1].Split('/');
                            int t1 = int.Parse(temp[0]) - 1;
                            int t4 = int.Parse(temp[2]) - 1;

                            temp = campos[2].Split('/');
                            int t2 = int.Parse(temp[0]) - 1;
                            int t5 = int.Parse(temp[2]) - 1;

                            temp = campos[3].Split('/');
                            int t3 = int.Parse(temp[0]) - 1;
                            int t6 = int.Parse(temp[2]) - 1;
                            lTriangulos.Add(new Triangulo(lPontos[t1], lPontos[t2], lPontos[t3]));
                            if (lNormais.Count > 0)
                                lTriangulos[lTriangulos.Count - 1].defineNormais(lNormais[t1], lNormais[t2], lNormais[t3]);
                        }
                        else
                        {
                            int t1 = int.Parse(campos[1]) - 1;
                            int t2 = int.Parse(campos[2]) - 1;
                            int t3 = int.Parse(campos[3]) - 1;
                            lTriangulos.Add(new Triangulo(lPontos[t1], lPontos[t2], lPontos[t3]));
                        }
                    }
                }
            }

            ficheiro.Close();
            //textura
            textura = null;
        }

Para testar estas funções vamos adicionar um modelo e uma textura ao nosso projeto, de preferência dentro de pastas criadas especificamente para esse efeito.
Com esta textura

Vamos ter isto


Para isso precisamos de uma função de desenhar nova na classe triângulo, pois a versão anterior só desenhava as arestas dos triângulos, o código passa a ser:
        public void desenhar(OpenGL gl, Texture textura)
        {
            gl.PushMatrix();
            gl.Translate(origem.getX(), origem.getY(), origem.getZ());
            gl.Rotate(rotacao.getX(), rotacao.getY(), rotacao.getZ());
            gl.Enable(OpenGL.GL_TEXTURE_2D);
            textura.Bind(gl);
            gl.Enable(OpenGL.GL_REPEAT);
            gl.Enable(OpenGL.GL_LIGHTING);
            gl.Begin(OpenGL.GL_POLYGON);
            gl.Enable(OpenGL.GL_COLOR_MATERIAL);

            gl.TexCoord(tx1.getX(), tx1.getY());
            if (hasNormals) gl.Normal(n1.getX(), n1.getY(), n1.getZ());
            gl.Vertex(p1.getX(), p1.getY(), p1.getZ());
            gl.TexCoord(tx2.getX(), tx2.getY());
            if (hasNormals) gl.Normal(n2.getX(), n2.getY(), n2.getZ());
            gl.Vertex(p2.getX(), p2.getY(), p2.getZ());
            gl.TexCoord(tx3.getX(), tx3.getY());
            if (hasNormals) gl.Normal(n3.getX(), n3.getY(), n3.getZ());
            gl.Vertex(p3.getX(), p3.getY(), p3.getZ());
            gl.End();
            gl.Disable(OpenGL.GL_TEXTURE_2D);
            gl.Disable(OpenGL.GL_LIGHTING);
            gl.Disable(OpenGL.GL_REPEAT);
            gl.Disable(OpenGL.GL_AUTO_NORMAL);
            gl.Disable(OpenGL.GL_COLOR_MATERIAL);
            gl.PopMatrix();
        }

Com este código cada triângulo vai ter normais, textura e tudo que é necessário para apresentar os modelos criados no Blender.

Por fim só falta a função desenhar da classe modelo, que vai chamar a função desenhar de cada triângulo que compõe o modelo 3D.
        public  void desenhar(SharpGL.OpenGL gl)
        {

            gl.PushMatrix();
            gl.MatrixMode(OpenGL.GL_MODELVIEW);
            gl.LoadIdentity();
            gl.Enable(OpenGL.GL_LIGHTING);
            gl.Translate(origem.getX(), origem.getY(), origem.getZ());
            gl.Rotate(rotacao.getX(), rotacao.getY(), rotacao.getZ());

            foreach (Triangulo triangulo in lTriangulos)
                triangulo.desenhar(gl, textura);

            gl.Disable(OpenGL.GL_LIGHTING);
            gl.PopMatrix();

        }

Como é possível verificar pelo código apresentado ativámos a iluminação no OpenGL por isso temos de adicionar pelo menos uma luz, o seguinte código deve ser adicionado ao formulário principal na função openGLControl_Resized:

            float[] cor = {1,1,1 };
            gl.Light(OpenGL.GL_LIGHT0, OpenGL.GL_AMBIENT, cor);
            float[] posicao = { 0, 5, 0 };
            gl.Light(OpenGL.GL_LIGHT0, OpenGL.GL_POSITION, posicao);
            gl.Enable(OpenGL.GL_LIGHT0);

As luzes no OpenGL podem ter muitas mais características, aqui só definimos o mínimo necessário, ou seja, a posição e a cor.

Agora que já adicionámos um modelo e uma textura aos recursos do nosso projeto, não esquecer de definir que devem ser copiados para a pasta do projeto, já podemos criar um objeto da classe modelo:

Modelo modelo = new Modelo(@"Modelos\nave.obj");

De seguida carregamos a textura:

modelo.CarregaTextura(gl, @"Texturas\nave.png");

E por fim desenhamos:

modelo.desenhar(gl);

No próximo capítulo desta saga vamos inserir código para manipular o nosso objeto fazendo-o rodar, mover e tudo o mais.

Faça o download do projeto aqui.

Comentários

Mensagens populares deste blogue

Upgrade do Windows Home para Pro sem formatar

 Há algum tempo que tentava fazer o upgrade do meu Windows 10 da versão Home para a versão Pro, mas chegava sempre a um ponto em que me era solicitado para formatar o sistema e não estava para isso. Finalmente conseguinte seguindo estes passos: - seguinte estes passos  utilizei uma das chaves genéricas para o Windows 10 Pro e fui a Settings > Update & Security > Activation > Change the product key; - após inserir uma das chaves o Windows instala as funcionalidades Pro e pede para reiniciar; - agora tem o Windows Pro mas não está ativado, assim fui ao site urcdkeys  onde comprei uma chave para o Windows Pro por menos de €20; - com essa chave voltei a funcionalidade Change the product key e ativei o Windows; - e pronto, Windows Pro ativado sem formatar ou reinstalar. Importante : eu não tenho nada a ver com o site urcdkeys por isso a vossa experiência pode correr de forma diferente da minha.

Vamos fazer um carro com o Unity 3D

Neste artigo vamos fazer um carro, simples, com o Unity 3D. A ideia é utilizar o motor de física do Unity 3D para simular o comportamento do carro. Os passos a seguir são: [1] - Criar um projeto novo

Tem troco

Para hoje um pequeno programa que dá troco, bem dar não dá mas calcula o troco a dar em função das moedas disponíveis. Neste projeto vamos utilizar o novo Visual Studio 2012. Como era de se esperar vamos iniciar um projeto novo: Agora adicionamos os seguintes elementos:  - um botão para calcular as moedas a dar de troco  - um botão para repor o número de moedas iniciais disponíveis  - uma textbox para introduzir o valor a pagar  - uma textbox para introduzir o valor entregue  - umas labels para informar o utilizador do que deve introduzir e outra para mostrar o troco  - por fim uma grelha para mostrar os valores das moedas e as quantidades disponíveis de cada uma. A janela principal do programa fica assim: Agora o código, primeiro o evento load do formulário, neste vamos definir os valores das moedas e as respetivas quantidades Para guardar estes valores vamos necessitar de uma variável definida ao nível do formulário, logo abaixo da definição da class: Public Class Form1     Public mo