segunda-feira, 10 de fevereiro de 2014

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.