Ponto V!

Home Arquitetura Programação Introdução a Renderização por Software
André Caceres Carrilho
Introdução a Renderização por SoftwareImprimir
Escrito por André Caceres

1. Introdução

O processo de Software Rendering foi muito utilizado antigamente (década de 1990) quando ainda não havia disponível chips dedicados ao processamento gráfico.

Jogos como o primeiro Quake (22, junho, 1996) e Unreal (22, maio, 1998), por exemplo, revolucionaram a indústria por apresentarem ambientes totalmente tridimensionais (incluindo os personagens, que diferentemente de jogos anteriores, como Doom e Wolfenstein, eram imagens bidimensionais projetadas na tela).

Mas como o motor do Quake renderizava a cena, sendo que a primeira placa gráfica da antiga 3dfx foi lançada no dia 1 outubro de 1996? A resposta para esta pergunta é o resultado do empenho de duas figuras na indústrias dos games, John Carmack (id Software) e Michael Abrash, que trabalharam no motor gráfico do jogo.

Ao contrário de muitos jogos atuais, que encarregam a CPU apenas para o processamento lógico do jogo, deixando a parte gráfica para a GPU, jogos como Quake faziam todo o processamento na CPU. Pode parecer simples hoje em dia, por existirem processadores com múltiplos núcleos e clock mais elevado com relação aos processadores Pentium MMX da época.

Para conseguir renderizar a pelo menos 30 quadros por segundo efetuando todo o processamento na CPU, os programadores utilizaram os recursos SIMD (Single Instruction, Multiple Data) dos processadores, fazendo a otimização das rotinas de render em Assembly.

1.1. Motivação

Muitos questionam a finalidade de um Software Rendering atualmente, sendo que temos APIs como DirectX e OpenGL que facilitam em muito o desenvolvimento, além de apresentarem performance superior, já que fazem o uso do pipeline gráfico implementado via hardware nas GPUs.

Realmente, há muitos programadores que não são a favor de “reinventar a roda”, mas deve-se lembrar da importância em realizar uma implementação como esta em termos educacionais. Entender o que se passa no "background" dessas tecnologias, permitindo até uma futura melhor utilização delas.

Neste artigo, será abordado a estrutura para o desenvolvimento de um motor 3D que utiliza Software Rendering implementada em C++ utilizando Assembly "inline" para a otimização.

2. Classes Básicas

2.1. Matemática

As classes para tratamentos matemáticos foram implementadas utilizando programação genérica, com templates. Como será tratado mais a frente, foi feito o uso de otimização em Assembly, e para isso criou-se as especializações dos templates para os tipos de números reais float (32 bits) e double (64 bits).

#ifndef SR_VECTOR3_HPP

#define SR_VECTOR3_HPP

template< typename Scalar > class Vector3 
{ 

// ... código generalizado

};

#if defined( SSE_CODE )

template< > class Vector3< float > 
{

// ... código específico para float

};

template< > class Vector3< double > 
{

// ... código específico para double

};

#endif // !SSE_CODE

typedef Vector3< float > Vector3f;
typedef Vector3< double > Vector3d;

#endif // !SR_VECTOR3_HPP

O mesmo esquema de código acima foi reproduzido para as classes Vector2, Vector4, Matrix3x3, Matrix4x4 e Quaternion. No momento não há nenhum artigo sobre a utilização específica de Quaternions aqui no PontoV, mas há uma boa explicação no link abaixo:

Os Quaternions foram utilizados para representar as rotações da câmera.

2.2. Utilidades

Não será apresentado aqui com detalhes os conceitos matemáticos envolvidos por trás das transformações de coordenadas com a câmera, apenas será dito que envolve a transformação entre as coordenadas mundo e um referêncial arbitrário, no qual a posição da câmera é a origem, e o eixo-Z contém a profundidade dos objetos.

O código principal é descrito no link a seguir:

No motor, o código das classes da câmera foi implementado na seguinte organização:

#ifndef SR_CAMERA_HPP

#define SR_CAMERA_HPP

class BaseCamera {

// ...

};

class FpsCamera : public BaseCamera {

// ...

};

class TpsCamera : public BaseCamera {

// ...

};

class RtsCamera : public BaseCamera {

// ...

};

Deve-se lembrar da importância das classes primitivas implementadas, pois são delas que o processo de rasterização partirá.

No motor, as primitivas básicas para a organizar os dados tridimensionais foram definidas da seguinte forma:

#ifndef SR_VERTEX_HPP

#define SR_VERTEX_HPP

class Triangle3D {

//...

};

class IndexUV {

//...

};

class Polygon {

//...

};

class Vertex {

//...

};

class ScreenVertex {

//...

};

#endif // !SR_VERTEX_HPP

2.3. Modelos Tridimensionais

Foram escolhidos pela facilidade de implementação e pela quantidade de recursos disponíveis na Web os formatos MDL, MD2 e MD3 da id Software. Como não é o intuito tratar deles no artigo, seguem algumas boas referências para o aprendizado.

Descrição dos formatos:

Inclusive no próprio PontoV temos um artigo que trata em detalhes como carregar e animar um modelo MD2: Animação por Vértices

Abaixo temos o esquema do arquivo que contém as definições das classes envolvidas com o carregamento dos modelos MD2.

#ifndef SR_MD2_MODEL_HPP

#define SR_MD2_MODEL_HPP

// ...

class Md2Header {

//...

};

class Md2Vertex {

//...

};

class Md2Frame {

//...

};

class Md2Model : public Object3D {

//...

};

#endif // !SR_MD2_MODEL_HPP

A classe Object3D é uma classe base para os modelos tridimensionais. Como será visto mais a frente (na classe de rasterização), esta será a classe que servirá como parâmetro nas chamadas dos métodos de rendering.

2.4. Cenários

Descrição dos formatos:

#ifndef SR_BSP_LOADER_HPP

#define SR_BSP_LOADER_HPP

class MapBsp {

// ...

};

class MapBsp38 : public MapBsp {

// ... Quake II 

};

class MapBsp30 : public MapBsp {

// ... Half-Life

};

#endif // !SR_BSP_LOADER_HPP

Na Figura abaixo podemos visuazar um arquivo do Quake II (BSP38) sem a utilização do PVS – Potential Visibility Set (não será tratado neste artigo) que foi implementado apenas para os cenários do Half-Life (BSP30).

Figura 1 – Versão wireframe e texturizada do arquivo “demo1.bsp” contido na versão de demonstração do Quake II.
Figura 1 – Versão wireframe e texturizada do arquivo “demo1.bsp” contido na versão de demonstração do Quake II.

3. Rasterização

O processo de rasterização da cena segue um pipeline (sequência de etapas) contendo primeiramente o Vertex Shader, e em seguida o Pixel Shader.

Vertex Shader:

  1. Transformação das coordenadas de mundo para coordenadas locais;
  2. Cálculo dos vetores normais a cada polígono;
  3. Backspace culling;
  4. Ordenação dos polígonos;
  5. Projeção de perspectiva (projetar as coordenadas na tela);

Pixel Shader:

  1. Cálculo da iluminação;
  2. Desenho dos triângulos;

3.1. Backspace Culling

Esta etapa consiste em determinar quais faces serão mostradas na tela e quais não. Não queremos desenhar polígonos que estão voltados para trás, já que não iremos vê-lo, isso apenas gastaria processamento e causaria uma redução na taxa de quadros por segundo.

A determinação de faces ocultas é bastante simples quando se tem disponível os vetores normais de cada polígono. Basta efetuar o produto escalar entre o vetor normal e o vetor de visualização da câmera (ambos em suas formas unitárias). Caso o resultado seja menor que zero, a face está oculta, e portanto não deve ser desenhada.

Negligênciar esta etapa pode fazer com que a taxa de quadros por segundo caia pela metade.

3.3. Ordenação dos polígonos

Nesta etapa, todos os polígonos que não estão ocultos devem ser ordenados segundo sua distância à origem (atual posição da câmera). Serão desenhados primeiramente os mais próximos, até os mais distântes, sendo que não será desenhado nenhum pixel onde já foi previamente setado a cor (para garantir zero overdraw).

Na implementação atual, a ordenação se dá da seguinte forma:

  • Primeiramente, é computado o valor médio dos três vértices do triângulo (Xm, Ym, Zm).
  • Em seguida, é feito a ordenação de todos os triângulos visíveis pela função std::sort();

3.2. Rasterizando um Triângulo

Tendo as coordenadas dos três vértices do triângulo projetadas na tela, e as correspondentes coordenadas de textura (U,V), pode-se partir para a rasterização do triângulo.

Esta etapa possui a maior importância de todo o código, pois dependendo da resolução da tela utilizada, ela poderá chegar a tomar mais de 90% dos ciclos do processador apenas para ser pintar a textura.

Há várias formas de se efetuar o mapeamento de textura nos polígonos. Recomenda-se utilizar o mapeamento de textura perspectivo, que apesar de ser o mais custoso (em termos de processamento), é o que dá a melhor aparência final.

Para mais detalhes técnicos, o link abaixo contém uma série de artigos do Chris Hecker, detalhando o assunto.

3.4. Implementação

No código abaixo podemos visualizar como é feita a chamada dos métodos para renderizar os modelos e cenários na classe de rasterização. Note que o mesmo conceito utilizado pelos Modelos de personagens também é utilizado pelos cenários.

#ifndef SR_RASTERIZER_HPP

#define SR_RASTERIZER_HPP

class Rasterizer {

// ...
    public:

        // Model Drawing

        void RenderWireFrame( const Object3D* obj );

        void RenderTextured( const Object3D* obj );

        // Map drawing

        void RenderWireFrame( const MapBsp* map );
        void RenderTextured( const MapBsp* map );

        // ...

    private:

        void DrawTriangleWireFrame( /* ... */ );

        void DrawTriangleTextured( /* ... */ );

// ...

};

#endif // !SR_RASTERIZER_HPP

3.5. Otimizações

A otimização tomou como base a criação de código em Assembly inline utilizando os registradores e “instruction set” SSE. Basicamente, há 8 registradores de 128 bits disponíveis: xmm0, xmm1, ..., xmm7.

Com as instruções SSE podemos realizar operações com custo computacional até quatro vezes menor com ponto flutuante de precisão simples (float 32 bits), ou então duas vezes menor com ponto flutuante de precisão dupla (double 64 bits).

Como exemplo, a soma de dois Vector4f seria realizada quatro vezes mais rápida com a utilização do código SSE, bastando mover os valores (x, y, z, w) de cada instância da classe para um registrador separado (xmm0 e xmm1, por exemplo) e salvar o resultado da instrução addps no registrador xmm2.

Para maiores informações sobre como utilizar Assembly inline no compilador GCC, há o artigo abaixo:

4. Conclusões

Por ser um assunto técnico, artigo buscou tratar superficialmente os principais tópicos envolvidos no processo de renderização por software em tempo real, deixando de lado detalhes que devem ser estudados individualmente e na sua devida sequência.

Embora possa parecer trabalhoso, é de interesse para os programadores conhecer a fundo os modelos matemáticos e computacionais envolvidos no processo de renderização tridimensional.

Figura 2 - Visualização do arquivo “soldier.md2” contido na versão de demonstração do Quake II.

Figura 2 - Visualização do arquivo “soldier.md2” contido na versão de demonstração do Quake II.

Referências


Comentários (5)
  • Vitor Almeida da Silva  - Interessante
    avatar

    O artigo serviu para aumentar a curiosidade e possui ótimas referências, mas uma série básica sobre os detalhes de como escrever um software renderer seria também muito educativo.

  • André Caceres
    avatar

    O intuito deste artigo foi abordar o tópico de maneira sucinta e introdutória. Não levei a fundo os tópicos por decisão própria, já que não sabia a receptividade de um assunto desse na comunidade.

    Caso haja interesse (por parte dos leitores), posso continuar e detalhar mais em outros artigos.

    O problema é que o artigo iria ser restrito a quem compreende bem Algebra Linear/Geometria Analítica e Computação Gráfica.

  • Vitor Almeida da Silva
    avatar

    Eu tenho interesse e bastante curiosidade :)

  • Anônimo
    avatar

    André, se o único problema é os leitores já conhecerem bem Algebra e afins, não é problema.

    Fica a cargo do leitor ler o material e já conhecer esses tópicos para melhor entendimento do assunto, inclusive temos artigos no Ponto V que cobrem o básico desses tópicos.

  • Thiago Dias Pastor  - Implementacao em C#
    avatar

    Muito bom ver alguem tocando neste tipo de assunto =P
    Caso alguem tenha curiosidade, a uns anos atras eu implementei um software render em C# e fiz um artigo descrevendo o "paso a passo"(http://ploobs.com.br/?p=663)

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

Busca

Linguagens

Twitter