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

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

C# IEnumerable e IEnumerator

Neste artigo vamos aprender como utilizar a interface IEnumerator por forma a permitir utilizar um ciclo foreach num conjunto ou coleção de dados. A maior parte das coleções (listas e outras) já implementam a interface, mas neste caso vamos personalizar a maneira como percorremos a lista. Quando utilizamos código assim: foreach(Class c in Collection) { ... } O compilador converte este código em algo assim: IEnumerator cc = Collection.GetEnumerator() while(cc.MoveNext()) { c=(Class)cc.Current; ... } Ao implementar a interface IEnumerable significa que a classe implementa uma versão da função GetEnumerator() que deve devolver uma classe que implemente a interface IEnumerator. Vamos explorar um exemplo. Começamos pela classe client Esta classe permitirá guardar os dados dos clientes, existindo um campo para indicar se o cliente ainda está ativo ou não. De seguida temos uma classe que define uma lista de clientes e que implementa a interface IEnumerable que de

React - Introdução

 Neste post vamos fazer uma breve introdução ao React. React é uma framework javascript e por isso é importante ter conhecimentos desta linguagem de programação para melhor compreender o seu funcionamento. O que é necessário? Para construir páginas com React é necessário ter instalado a framework Node e o seu instalador de packages o npm. Com o Node instalado basta abrir uma janela da linha de comandos, eu aconselho utilizar o novo Windows Terminal ou o Cmder . Na sua linha de comando escolhida execute o comando: npx create-react-app Tutorial01 Este comando vai criar uma pasta com o nome Tutorial01 e instalar dos os ficheiros necessários para construir a sua primeira aplicação React dentro dessa pasta. De seguida entramos na pasta criada com o comando: cd Tutorial01 E iniciamos a aplicação com o comando: npm start Deve conseguir ver uma página com o seguinte aspeto: A partir daqui, até fechar a linha de comando, todas as alterações feitas aos ficheiros da sua aplicação são automaticam