Este é um tutorial que tem a finalidade de compartilhar um pouco as entrelinhas da programação de um jogo. Independente de qual engine você use para fazer seu jogo, boa parte dos conceitos que seguem aqui estão embutidos nela e muitas vezes são desenhados da forma mais genérica possível, para que os desenvolvedores de jogos que a usem não necessitem reescrever o código sempre que precisar modificar algo. É como se a engine estivesse um degrau abaixo do jogo em si.
Enquanto que em um software como RPG Maker e o Unity3D você está mais preocupado com gráficos, áudios e a mecânica do jogo em si, aqui o desenvolvedor estará muito mais interessado em conceitos como renderização e backbuffer, carregamento de memória e overflows, error handling, inputs, dentre outros.
Por fim, para que você tenha um relativo domínio nesse tutorial, é necessário um conhecimento intermediário em C++ (ou C), além de ter instalados o Visual Studio (qualquer versão) e os kits de desenvolvimentos do DirectX e do Windows 7.
Por ele ser demasiado grande, estarei dividindo-o em duas partes. A segunda parte (17/08) sai na segunda-feira apenas porque não terminei de editar e tenho que ir embora do trabalho.
Sumário
1. Estrutura de um motor de jogo
2. DirectX, OpenGL e outros
3. Sobre as Linguagens de Programação
Esta primeira parte pretendo apenas discutir os fundamentais no desenvolvimento de jogos, APIs gráficas e afins. A segunda parte é puramente discussão técnica de códigos.
Estrutura de um motor de jogo
Um motor de jogo é uma coleção de arquivos de código que contém os elementos fundamentais comuns à maioria dos jogos, a maior parte dos jogos comerciais (e não-comerciais) são escritos usando um motor de jogo.
Como dizem que uma imagem vale mais que mil palavras, podemos ilustrar a estrutura dessa forma:
De uma forma bem simplificada, esta é uma das formas de se pensar a estrutura um motor de jogo. Pensemos que os inputs (entradas, ou seja, mouse e teclado no nosso caso) estão definidos pelo "Jogo Genérico", que por sua vez está relacionado às regras básicas de um jogo qualquer - detalhes como físicas (colisões, por exemplo), inteligência artificial e gerenciamento de memória. O Jogo Específico (que é o nosso produto final; Pokemon, por exemplo) liga-se somente ao motor de jogo porque, na hora de escrevê-lo, estamos nos abstraindo de todos aqueles detalhes acima - usamos tudo aquilo como ferramentas para nosso objetivo. Então, se a cada passo que damos queremos um som, o código no Jogo Específico vai chamar uma função já específica predefinida no módulo de áudio e por aí vai.
Existem muitas outras formas de fazer a concepção do diagrama de jogo - por exemplo, é comum retirarmos a parte do "motor" quando desejamos fazer algo bastante específico e não temos interesse em reutilizar o código para algo futuro; assim, todos os outros módulos se ligam direto ao jogo principal. Na verdade, esse tipo de ação não é tão interessante porque simplesmente, quando formos fazer outro jogo, muito vai ser perdido.
DirectX, OpenGL e outros
Em resumo: o DirectX é um conjunto de APIs que te permitem acessar diversos recursos de baixo nível do Windows, onde o desempenho acaba sendo melhor do que as técnicas tradicionais para programação no Windows.
APIs:
O DirectX tem suporte apenas para Windows (o que inclui o Xbox One e Windows Phone). O OpenGL é o maior concorrente e é a alternativa mais usada fora desse ecosistema, possuindo suporte à dispositivos como PS4, Android, OS X e até no próprio Windows.
Agora, contar um pouco de história que ilustra bastante coisas:
Sobre as linguagens de programação
Existem inúmeras linguagens de programação que você pode utilizar para fazer seu jogo: C/C++, Java, Python, Haskell, Ada, Ruby, etc. Em todas elas, você pode ter mais ou menos dificuldade de fazer seu jogo, mas todas vão recair em um ponto comum: performance.
C++ é a linguagem mais utilizada porque te dá uma versatilidade muito grande - você pode programar em qualquer dispositivo em geral e, tendo acesso diretamente ao metal (gerenciando memória na mão, por exemplo), você terá uma das melhores performances possíveis. Java, por exemplo, apesar de ter evoluído muito, você sempre terá algum probleminha devido à máquina virtual. Python e Ruby, o problema reside nos interpretadores, e por aí vai.
Outro fator para a maior performance é que as APIs do DirectX e OpenGL em geral são implementadas nessa mesma linguagem, o que facilita a comunicação entre ambos. No Java, por exemplo, se você quiser utilizar algum recurso do OpenGL, você é forçado a ir para a Java Native Interface (JNI) que permite você misturar um pouco de C++ no seu código. Minecraft, por exemplo, é boa parte em Java, mas muitas das funções de renderização são escritas em outra linguagem.
Algumas APIs gráficas, como SDL, LWJGL, Allegro, etc, são nada mais que simplificações de funções do OGL/DX; e mesmo tendo amarrações (bindings) a outras linguagens, no final tudo se resume ao C++.
Abaixo a transcrição de um excerto sobre otimização de jogos que escrevi em certo momento:
Continua na parte 2... (se eu não ficar com preguiça de escrever após ter a ideia igual nesse post)
Enquanto que em um software como RPG Maker e o Unity3D você está mais preocupado com gráficos, áudios e a mecânica do jogo em si, aqui o desenvolvedor estará muito mais interessado em conceitos como renderização e backbuffer, carregamento de memória e overflows, error handling, inputs, dentre outros.
Por fim, para que você tenha um relativo domínio nesse tutorial, é necessário um conhecimento intermediário em C++ (ou C), além de ter instalados o Visual Studio (qualquer versão) e os kits de desenvolvimentos do DirectX e do Windows 7.
Por ele ser demasiado grande, estarei dividindo-o em duas partes. A segunda parte (17/08) sai na segunda-feira apenas porque não terminei de editar e tenho que ir embora do trabalho.
Sumário
1. Estrutura de um motor de jogo
2. DirectX, OpenGL e outros
3. Sobre as Linguagens de Programação
Esta primeira parte pretendo apenas discutir os fundamentais no desenvolvimento de jogos, APIs gráficas e afins. A segunda parte é puramente discussão técnica de códigos.
Estrutura de um motor de jogo
Um motor de jogo é uma coleção de arquivos de código que contém os elementos fundamentais comuns à maioria dos jogos, a maior parte dos jogos comerciais (e não-comerciais) são escritos usando um motor de jogo.
Como dizem que uma imagem vale mais que mil palavras, podemos ilustrar a estrutura dessa forma:
De uma forma bem simplificada, esta é uma das formas de se pensar a estrutura um motor de jogo. Pensemos que os inputs (entradas, ou seja, mouse e teclado no nosso caso) estão definidos pelo "Jogo Genérico", que por sua vez está relacionado às regras básicas de um jogo qualquer - detalhes como físicas (colisões, por exemplo), inteligência artificial e gerenciamento de memória. O Jogo Específico (que é o nosso produto final; Pokemon, por exemplo) liga-se somente ao motor de jogo porque, na hora de escrevê-lo, estamos nos abstraindo de todos aqueles detalhes acima - usamos tudo aquilo como ferramentas para nosso objetivo. Então, se a cada passo que damos queremos um som, o código no Jogo Específico vai chamar uma função já específica predefinida no módulo de áudio e por aí vai.
Existem muitas outras formas de fazer a concepção do diagrama de jogo - por exemplo, é comum retirarmos a parte do "motor" quando desejamos fazer algo bastante específico e não temos interesse em reutilizar o código para algo futuro; assim, todos os outros módulos se ligam direto ao jogo principal. Na verdade, esse tipo de ação não é tão interessante porque simplesmente, quando formos fazer outro jogo, muito vai ser perdido.
DirectX, OpenGL e outros
Em resumo: o DirectX é um conjunto de APIs que te permitem acessar diversos recursos de baixo nível do Windows, onde o desempenho acaba sendo melhor do que as técnicas tradicionais para programação no Windows.
APIs:
- Direct3D: A principal e a mais famosa, responsável pelos gráficos. Diferente do que o nome implica, o Direct3D também suporta a criação de gráficos 2D.
- XACT: API de áudio com suporte a arquivos WAV. Útil para efeitos e músicas.
- DirectInput: Legado, mas usado principalmente para receber entradas de dispositivos como joysticks, controles de corrida, etc.
- XInput: Uma API mais nova que substitui o DirectInput para o Windows e Xbox 360/One. Funciona a partir do Windows XP e não dá suporte à hardware antigos como o DirectInput.
- DirectPlay: API para comunicação de rede. Suporta a escrita de jogos que conectam com outros jogadores via internet e LANs.
- DirectSetup: Fácil suporte para dar suporte à instalação do DirectX runtime para rodar seu jogo.
O DirectX tem suporte apenas para Windows (o que inclui o Xbox One e Windows Phone). O OpenGL é o maior concorrente e é a alternativa mais usada fora desse ecosistema, possuindo suporte à dispositivos como PS4, Android, OS X e até no próprio Windows.
Agora, contar um pouco de história que ilustra bastante coisas:
Início do Conflito
Um dia, no início da década de 90, a Microsoft olhou ao seu redor. Eles viram o SNES e o Sega Genesis sendo incríveis, possuindo bastantes jogos de ação e tal. E eles viram o DOS. Desenvolvedores codavam para o DOS assim como faziam para jogos de console: diretamente no metal. Só que diferente de videogames comuns, no entanto, onde um desenvolvedor que faz um jogo de SNES sabe exatamente as especificações de seu hardware, os desenvolvedores para o DOS tinham que escrever código para múltiplas configurações. E isso é mais difícil do que parece.
E a Microsoft teve um problema ainda maior: Windows. Entenda, o Windows queria possuir o hardware, diferente do DOS que deixava os desenvolvedores fazerem o que quisessem. Possuir o hardware era necessário para que houvesse cooperação entre aplicações. Cooperação é exatamente o que desenvolvedores de jogos odeiam porque isso toma recursos de hardware que eles poderiam estar usando para serem incríveis.
De forma a promover, o desenvolvimento de jogos no Windows, a Microsoft precisava de uma API uniforme que era de baixo nível, rodava no Windows sem ser retardada por ele e, mais importante, multi-hardware. Uma única API para todos os hardwares gráficos, sonoros e de entrada.
Assim, o DirectX nasceu.
Aceleradores 3D nasceram alguns meses depois. E Microsoft teve um problema. DirectDraw, o componente gráfico do DirectX, apenas lidava com gráficos 2D: alocando memória gráfica e fazendo movimentos de bits entre diferentes seções alocadas da memória.
Então a Microsoft comprou um pouco de middleware e a maquiou como Direct3D versão 3. Isso foi universalmente injuriante. E por uma boa razão; olhar para o código do D3D v3 era como encarar a Arca da Aliança.
O John Carmack, na Id Software (atualmente na Oculus) deu uma olhada nesse lixo e disse "Foda-se!" e decidiu escrever outra API: OpenGL.
Outra parte do monstro de muitas cabeças era também que a Microsoft esteve trabalhando com a SGI em uma implementação OpenGL para o Windows. A ideia aqui era cortejar desenvolvedores de aplicações GL comuns: workstations. Ferramentas CAD, modelagens, esse tipo de coisas. Jogos eram a coisa mais distante na cabeça da Microsoft. Isso era primariamente uma coisa do Windows NT, mas Microsoft decidiu adicionar ao Windows95 também.
De forma a aliciar os desenvolvedores de workstations para Windows, Microsoft tentou suborná-los com acesso à estas placas gráficas 3D novinhas. Microsoft implementou o protocolo ICD (Installable Client Driver): uma fabricante de placa gráfica poderia substituir a implementação OpenGL via software da Microsoft por uma baseada em hardware. O código poderia usar uma implementação de hardware do OpenGL se estivesse disponível.
Nos primeiros dias, as placas de vídeo a nível de consumidor não possuiam suporte para o OpenGL. Isso não impediu que Carmack portasse Quake para OpenGL (GLQuake) em sua workstation SGI. Do leia-me do GLQuake:
Teoricamente, glquake irá rodar em qualquer implementação OpenGL que suporte extensões de objetos texturais, mas a não ser que seja um hardware bem potente que acelere todo o necessário, o jogo não ficará aceitável. Se ele tem que ir por qualquer caminho de emulação de software, a performance ficará bem abaixo de um frame por segundo.
Agora (março de 97), o único hardware com implementação OpenGL que pode rodar glQuake razoavelmente é uma Integraph Realizm, que é uma placa MUITO cara. 3dlabs esteve melhorando sua performance significantemente, mas com os drivers disponívels, ainda não é bom o suficiente para jogar. Parte dos drivers atuais do 3dlabs para glint e placas permedia podem também derrubar o NT quando saindo de fullscreen, então não recomendo rodar glQuake em hardware da 3dlabs.
3dfx fez uma opengl32.dll que implementa tudo que o glQuake precisa, mas não é uma implementação total do OpenGL. É improvável que outras aplicações opengl funcionem bem com isso, então considere basicamente um "driver do glquake".
Esse era o nascimento dos drivers miniGL. Eles evoluíram para implementações OpenGL completas eventualmente, quando o hardware ficou poderoso o suficiente para implementar a maior parte das funcionalidades do OpenGL em hardware. nVidia foi a primeira a oferecer a implementação total da OpenGL. Muitos outros fabricantes lutavam, o que é um dos motivos pelos quais os desenvolvedores preferiam Direct3D: eles eram compatíveis em um largo acervo de hardware. Eventualmente, apenas nVidia e ATI (agora AMD) restaram e ambas tinham uma boa implementação OpenGL.
Ascenção do OpenGL
Logo, a batalha estava feita: Direct3D vs OpenGL. É uma história bem interessante, considerando o quão ruim D3D v3 era.
O Comitê Revisor da Arquitetura OpenGL (do inglês, ARB) é uma organização responsável por manter o OpenGL. Eles emitem um número de extensões, mantem o repositório destas e criam novas versões da API. O ARB é um comitê feito por muitas empresas da indústrica gráfica, assim como várias desenvolvedoras de sistemas operacionais. Microsoft e Apple, por várias vezes, já foram membros do ARB.
3Dfx veio com voodoo2. Este foi o primeiro hardware que poderia fazer multitexturamento, o que é algo que o OpenGL não podia fazer antes. Enquanto 3Dfx era fortemente contra o OpenGL, a NVIDIA, desenvolvedora do próximo chip gráfico de multitexturamento (TNT1) o amava. Então o ARB gerou uma nova extensão: GL_ARB_multitextura que permitia acesso ao multitexturamento.
Enquanto isso, Direct3D v5 era lançado. Agora, D3D tinha de fato virado uma API ao invés de algo que um gato vomitou. O problema? Sem multitexturamento.
Opa.
Agora, isso não machuchou nem tanto quanto deveria porque pessoas não usavam tanto multitexturamento. Não diretamente. Multitexturamento atingia a performance um pouco e, em muitos casos, não vali a pena comparado a multi-passagem. E, claro, desenvolvedores de jogos adoram ter certeza que seus jogos funcionam bem em hardwares antigos, que não tinham multitexturamento, então muitos jogos foram feitos sem ele.
D3D assim recebeu uma repressão.
O tempo passa e a NVIDIA lança a GeForce 256 (não a GeForce GT-250; a primeira GeForce de fato), encerrando a competição em placas gráficas pelos próximos dois anos. O maior ponto de venda era a capacidade de realizar Vertex Transform e Lightning (T&L) direto no hardware. Não apenas isso, NVIDIA adorava tanto o OpenGL de forma que o motor T&L deles era de fato OpenGL. Quase literalmente; como entendo, parte de seus registradores na verdade tomavam os enumeradores OpenGL diretamente como valores.
Direct3D v6 é lançado. Pelo menos tinha multitexturas, mas... sem T&L no hardware. OpenGL sempre teve um fluxo (pipeline) T&L, mesmo que antes da 256 isso tenha sido implementado em software. Então foi bem fácil para a NVIDIA converter a implementação em software para uma solução em hardware. Somente na versão 7 que o D3D finalmente teria suporte à T&L no hardware.
Alvorada dos Shaders, Crepúsculo do OpenGL
Então, a GeForece 3 foi lançada. E muitas coisas aconteceram ao mesmo tempo.
Microsoft tinha decidido que eles não se atrasariam novamente. Então ao invés de esperar pelo que a NVIDIA estava fazendo e copiar depois, eles tomaram a posição de ir até eles e conversar. E então se apaixonaram e tiveram um console juntos.
Um divórcio bagunçado foi realizado depois, mas isso é outra história.
O que isso significou para o PC foi que a GeForce 3 veio simultaneamente com D3D v8. E não é difícil ver como a GeForce 3 influenciou os shaders do D3D 8. Os pixels shaders do Shader Model 1.0 eram extremamente específicos ao hardware da NVIDIA. Não havia qualquer tentativa de pelo menos tentar abstrair o hardware da NVIDIA; SM 1.0 era apenas o que a GeForce 3 fazia.
Quando a ATI entrou na corrida de performance nas placas gráficas com a Radeon 8500, havia um problema. O fluxo de processamento da 8500 era mais poderoso que o da NVIDIA. Então Microsoft emitiu o Shader Model 1.1, que era basicamente "o que a 8500 fizer".
Isso pode soar como uma falha da parte do D3D. Mas falhas e sucessos são questão de degraus. E falhas épicas estavam acontecendo na terra do OpenGL.
NVIDIA amava OpenGL, então quando o GeForce 3 foi lançado, eles lançaram extensoes OpenGL. Extensões proprietárias OpenGL: apenas da NVIDIA. Naturalmente, quando a 8500 apareceu, não pode usar nenhuma delas.
Né, então pelo menos na terra do D3D 8, voCê podia pelo menos rodar seus shaders da SM 1.0 em hardware da ATI. Claro, você teria que escrever novos shaders para tomar vantagem da 8500, mas pelo menos seu código funcionava.
De forma a ter shaders de qualquer time no OpenGL da Radeon 8500, ATI tinha que escrever um número de extensões OpenGL. Extensões proprietárias OpenGL: apenas da ATI. Então você necessitava um código da NVIDIA e um código da ATI apenas para ter shaders.
Agora você pode perguntar: "Onde estava o Comitê do OpenGL, cuja função era manter o OpenGL atual?". Onde muitos comitês costumam acabar: sendo idiotas.
Entenda, mencionei o ARB_multitexture acima porque ele pesa em tudo isso. O ARB parecia (de uma perspectiva de alguém de fora) que queria evitar a ideia de shaders. Eles perceberam que se jogassem configurabilidade suficiente em um fluxo de função-fixa, eles poderiam igualar a habilidade de um fluxo de shader.
Então o ARB lançava extensão após extensão. Cada extensão com as palavras "texture_env" era mais uma maneira de corrigir este design que envelhecia. Veja o registro: entre ARB e extensões EXT, haviam oito dessas feitas. Muitas foram promovidas à versões principais do OpenGL.
Microsoft era parte do ARB nesta época; eles saíram perto de quando o D3D 9 foi lançado. Então é inteiramente possível que eles estivessem trabalhando para sabotar o OpenGL de alguma forma. Eu pessoalmente duvido dessa teoria por dois motivos. Um, eles teriam que ter ajuda dos outros membros para fazer isso, já que cada membro tem apenas um voto. E, mais importante, dois, o ARB não necessitava da ajuda da Microsoft para estragar tudo. Veremos isso mais a frente.
Eventualmente o comitê, provavelmente sob ameaças da ATI e da NVIDIA (ambos membros ativos) eventualmente perdeu o juízo o suficiente para criar shaders ao estilo assembly.
Quer algo mais estúpido?
T&L no hardware. Algo que o OpenGL tinha primeiro. Bem, é interessante. Para ter a maior performance possível de T&L no hardware, você precisa armazenar seus dados de vertex na GPU. Afinal, é a GPU que quer usar essa informação.
No D3D v7, Microsoft introduziu o conceito de Vertex Buffers. Eles alocavam trilhos de memória de GPU para armazenar dados de vertex.
Quer saber quando OpenGL conseguiu algo equivalente a isso? Ah, NVIDIA, sendo uma amante de todas as coisas do OpenGL (enquanto fossem extensões proprietárias da NVIDIA) lançou a extensão de Vertex Array Range quando a GeForce 256 foi lançada. Mas quando o ARB decidiu dar funcionalidade similar?
Dois anos depois. Isso foi após eles terem aprovado vertex shaders e fragment shaders (pixel, no idioma D3D). Esse foi o tempo que o ARB levou para desenvolver uma solução multi-plataforma para armazenar dados de vertex na memória da GPU. Novamente, algo que T&L de hardware necessita para atingir performance máxima.
Um Idioma para Arruiná-los
Então, o ambiente de desenvolvimento do OpenGL esteve fraturado por um tempo. Sem shaders multi-hardware, sem armazenamento de vertex em GPUs multi-hardware, enquanto os usuários D3D aproveitavam de ambos. Poderia ficar pior?
Você... pode dizer que sim. Conheça o 3D Labs.
Quem são eles, você pergunta? Eles são uma empresa falida que considero ser os verdadeiros assassinos do OpenGL. Claro, o inepticidade do ARB fez OpenGL vulnerável quando ele deveria estar dominando o D3D. Mas 3D Labs é provavelmente o maior motivo para mim para o estado atual do OpenGL no mercado. O que eles fizeram de fato para causar isso?
Criaram a OpenGL Shading Language.
3D Labs era uma empresa que estava morrendo. Suas caras GPUs estavam sendo marginalizadas pela pressão crescente da NVIDIA no mercado de workstations. E diferente da NVIDIA, 3d Labs não tinha presença no mercado principail; se NVIDIA vencesse, eles morriam.
E foi isso que aconteceu.
Então, em uma aposta para continuar relevante em um mundo que não queria seus produtos, 3D Labs apareceu em uma certa Game Developer Conference (GDC) com apresentações do que eles chamavam de "OpenGL 2.0". Isso seria uma reescrita completa, do zero, da API do OpenGL. E isso fazia sentido: havia bastante sujeira na API naquela época (nota: essa sujeira ainda existe). Apenas repare como carregamentos de texturas e amarramento (binding) funcionam; é semi-arcano.
Parte da proposta deles era uma shading language. Naturalmente. No entando, diferente das extensões multi-plataformas atuais do ARB, a shading language deles era "alto-nível" (C é alto nível para uma shading language. Sim, verdade).
Agora a Microsoft estava trabalhando em sua própria shading language de alto nível. onde eles, em toda a imaginação coletiva da Microsoft, chamavam de... High Level Shading Language (HLSL). Mas a abordagem deles era fundamentalmente diferente.
O maior problema com a shader language da 3D Labs era que ela era embarcada. Entenda, HLSL era uma linguagem que a Microsoft definiu. Eles lançavam um compilador para ela e ela gerava o código assembly do Shader Model 2.0 (ou posteriores Shader Models) que você alimentaria o D3D. Nos dias do D3D v9, HLSL nunca era tocado diretamente pelo D3D. Era uma ótima abstração, mas puramente opcional. E um desenvolvedor sempre tinha a oportunidade de ir atrás do compilar e polir a saída para máxima performance.
A linguagem da 3D Labs não tinha nada disso. Você dava ao driver uma linguagem parecida com C e ela produzia um shader. Fim da história. Não um shader assembly, não algo que você alimentava outra coisa. O objeto OpenGL representava um shader.
O que isso significava era que os usuários OpenGL estavam vulneráveis às vagarosidades de desenvolvedores que ainda estavam aprendendo a compilar linguagens do nível de assembly. Bugs de compilação corriam soltos pela recém-batizada OpenGL Shading Language (GLSL). O que é pior, se você conseguisse que um shader compilasse em múltiplas plataformas corretamente (não trivial), você ainda estava sujeito aos optimizadores do dia. Que não eram tão ótimos quanto podiam ser.
Enquanto esta era a maior falha da GLSL, não era a única. De longe.
No D3D, e em outras velhas linguagens assembly no OpenGL, você poderia misturar e combinar vertex e fragment (pixel) shaders. Enquanto eles comunicassem com a mesma interface, você poderia usar qualquer vertex shader com qualquer fragment shader compatível. E ainda haviam níveis de incompatibilidade que eles poderiam aceitar; um vertex shader poderia escrever uma saída que um fragment shader não lesse. E assim em diante.
GLSL não tinha nada disso. Vertex shaders e fragment shaders eram unidos no que o 3D Labs chamava de "objeto de programa". Então se você quisesse compartilhar os programados do vertex e fragment, você tinha que construir múltiplos objetos de programas. E isso causava o segundo problema.
3D Labs pensava que estavam sendo inteligentes. Eles basearam o modelo de compilação do GLSL em C/C++. Você pega um arquivo .c ou .cpp e compila em um arquivo de objeto. Então você pega um ou mais arquivos de objetos e os linka a um programa. Então é assim que o GLSL compila: você compila seu shader (vertex ou fragment) em um objeto de shader. Então coloca esses shaders em um objeto de programa e os linka para criar seu programa.
Enquanto isso permitia ideias legais em potencial que continham código extra que os shaders principais podiam chamar, o que isso significava na prática era que os shaders compilavam duas vezes. Uma no estágio de compilação e outra no estágio de linking. O compilador da NVIDIA em particular era conhecido por basicametne rodar a compilação duplamente. Ele não gerava qualquer tipo de objeto intermediário, apenas compilava uma vez e jogava fora a resposta, então compilava de novo no tempo de linkagem.
Então mesmo que você quisesse linkar seu vertex shader à dois diferentes fragment shaders, você tinha que fazer um monte de compilação a mair que no D3D. Especialmente porque a compilação de uma linguagem similar à C era totalmente feita offline, não ao início da execução do programa.
Haviam outros problemas com a GLSL. Talvez pareceria errado jogar toda a culpa na 3D Labs, já que o ARB eventualmente aprovou a ideia e incorporou a linguagem (mas nada da iniciativa "OpenGL 2.0" deles). Mas foi ideia deles.
E aqui está a parte realmente triste: 3D Labs estava correta (na maior parte). GLSL não é uma linguagem de shaders baseada em vetores da mesma forma que HLSL era na época. Isso porque o hardware da 3D Labs era escalar (similar aos hardwares modernos da NVIDIA), mas eles estavam ultimamente certos na direção que muitos fabricantes de hardware seguiram.
Eles estavam corretos em ir com um modelo de compilação online para uma linguagem de "alto nível". D3D até mudou para isso eventualmente.
O problema era que 3D Labs estavam corretos na hora errada. E tentando conjurar o futuro muito cedo, em tentando ser à prova do futuro, eles deixaram de lado o presente. Soa similar de como OpenGL sempre teve a possibilidade para a funcionalidade de T&L. Exceto que o fluxo do T&L do OpenGL ainda era útil antes do T&L de hardware, enquanto GLSL era um fardo antes que o mundo o entendesse.
GLSL é uma boa linguagem agora. Mas para a época? Era horrível. E OpenGL sofreu por isso.
Caindo em Torno de uma Apoteose
Enquanto mantenho que 3D Labs deu o golpe fatal, foi o próprio ARB que botaria a última unha no caixão.
Esta é uma história que você já deve ter ouvido. Próximo ao OpenGL 2.1, OpenGL estava com um problema. Havia muito código legado. A API não era mais fácil de usar. Haviam 5 maneiras de fazer as coisas e nenhuma ideia de qual era mais rápido. Você podia "aprender" OpenGL com tutoriais simples, mas não aprendia realmente a API que te daria performance e poderes reais.
Então o ARB decidiu tentar outra reinvenção do OpenGL. Isso era similar ao "OpenGL 2.0" do 3D Labs, mas melhor porque o ARB estava por trás. Eles chamavam de "Longs Peak".
O que há de ruim por gastar um tempo para melhorar a API? Era ruim porque Microsoft se deixou vulnerável. Olha, este era o momento que o Windows Vista estava lançando.
Com Vista, Microsoft decidiu instituir algumas das mudanças muito necessárias nos displays gráficos. Eles forçaram drivers a se submeterem ao sistema operacional para virtualização de memória gráfica e várias outras coisas.
Enquanto é possível debater os méritos disso ou se isso era possível, o fato é que: Microsoft instituiu que o D3D 10 seria apenas do Vista (e acima). Mesmo que você tivesse hardware capaz de rodar o D3D 10, você não poderia rodar aplicações D3D 10 sem também rodar o Vista.
Você também pode lembrar que o Vista... é, bem, digamos apenas que não deu muito certo. Então você tinha um sistema operacional que tinha um subdesempenho, uma nova API que rodava apenas naquele SO e uma nova geração de hardware que necessitavam daquela API e SO para fazer qualquer coisa que ser mais rápido que a geração anterior.
No entanto, desenvolvedores podiam acessar recursos do nível do D3D 10 através do OpenGL. Bem, eles poderiam se o ARB não estivesse ocupado trabalhando no Longs Peak.
Basicamente, o ARB passou um bom ano e meio a dois anos trabalhando para fazer a API melhor. Na hora que o OpenGL 3.0 apareceu, a adoção do Vista já havia encerrado e o Windows 7 já aparecia para colocar o Vista atrás deles, e a maioria dos desenvolvedores de jogos não se importavam com esses recursos do nível D3D 10 de qualquer forma. Afinal, o hardware do D3D 10 rodava aplicações D3D 9 tranquilamente. E com o aparecimento de ports PC-para-console (ou desenvolvedores de PC mudando de barco para desenvolvimento de consoles. Escolha), desenvolvedores não necessitavam desses recursos do D3D 10.
Agora, se desenvolvedores tivessem acesso à estes recursos mais cedo pelo OpenGL em máquinas Windows XP, então o desenvolvimento do OpenGL teria recebido um muito-necessário empurrado. Mas o ARB perdeu sua chance. E quer saber a pior parte?
Apesar de ter gasto dois preciosos anos tentando reescrever a API do zero... eles ainda falharam e apenas reverteram para o status quo (exceto por um mecanismo de depreciação).
Então não apenas o ARB perdeu uma janela crucial de oportunidade, eles sequer terminaram a tarefa que fizeram perder essa chance. Uma falha épica em todos os sentidos.
E este é o conto entre do OpenGL vs Direct3D. Um conto de oportunidades perdidas, estupidez bruta, cegueira proposital e simples toleza.
Sobre as linguagens de programação
Existem inúmeras linguagens de programação que você pode utilizar para fazer seu jogo: C/C++, Java, Python, Haskell, Ada, Ruby, etc. Em todas elas, você pode ter mais ou menos dificuldade de fazer seu jogo, mas todas vão recair em um ponto comum: performance.
C++ é a linguagem mais utilizada porque te dá uma versatilidade muito grande - você pode programar em qualquer dispositivo em geral e, tendo acesso diretamente ao metal (gerenciando memória na mão, por exemplo), você terá uma das melhores performances possíveis. Java, por exemplo, apesar de ter evoluído muito, você sempre terá algum probleminha devido à máquina virtual. Python e Ruby, o problema reside nos interpretadores, e por aí vai.
Outro fator para a maior performance é que as APIs do DirectX e OpenGL em geral são implementadas nessa mesma linguagem, o que facilita a comunicação entre ambos. No Java, por exemplo, se você quiser utilizar algum recurso do OpenGL, você é forçado a ir para a Java Native Interface (JNI) que permite você misturar um pouco de C++ no seu código. Minecraft, por exemplo, é boa parte em Java, mas muitas das funções de renderização são escritas em outra linguagem.
Algumas APIs gráficas, como SDL, LWJGL, Allegro, etc, são nada mais que simplificações de funções do OGL/DX; e mesmo tendo amarrações (bindings) a outras linguagens, no final tudo se resume ao C++.
Abaixo a transcrição de um excerto sobre otimização de jogos que escrevi em certo momento:
De uma forma geral, o que eu vejo é que muitos desenvolvedores se preocupam desnecessariamente com otimização de sistemas quando ela é simplesmente desnecessária em boa parte dos casos, ou traz benefícios tão irrisórios que não compensa o trabalho.
Permitam-me dar uma breve explicação com o que sei sobre otimização.
Otimizar significa tornar algo ótimo (no sentido de máxima performance), como substituir um algoritmo de busca linear por um de busca binária, usar algoritmos de ordenação eficientes ou mesmo procurar uma forma mais inteligente de achar máximos e mínimos de um determinado conjunto (método de Monte Carlo? método do Gradiente?). Os estudos de otimização computacional surgiram a partir da necessidade de fazer aplicações que possam resolver esses tipos de problemas (que envolvem matemática, afinal) de forma mais rápida, eficiente e eficaz.
Certo, e onde quero chegar?
Muito disso era extremamente importante quando os computadores ainda tinham alguns meros kbytes de memória e a frequência dos processadores não ultrapassava meio-gigahertz, onde você era obrigado a economizar bytes no código (alguém se lembra do bug do milênio?) e tentar fazer com que ele fizesse a mesma coisa da forma mais rápida possível - muitos preferiam recorrer a outros mecanismos, como linguagem de máquina e tudo mais. A grande questão aqui é que os computadores evoluiram ao ponto que a quantidade de operações de ponto flutuante por segundo (vulgo FLOPS) e a memória RAM disponível se tornou tão grande que certas coisas são triviais e não se percebe a diferença mesmo que você escreva teu código totalmente em binário.
E é aí que entra a questão dos jogos. Ao contrário do senso comum, vou começar falando dos jogos 3D e depois ir para o 2D, e assim vocês entendem onde quero chegar: pensem que a unidade básica desses jogos é o triângulo - com três vértices e cada um com uma coordenada (x, y, z). Seu cenário será formado pela composição de vários desses triângulos, cada um com sua própria coordenada, e que estarão listadas em uma matriz. Obviamente, quanto maior a quantidade de triângulos, maior será a sua quantidade de detalhes e, por consequência, mais extensa será essa sua matriz - e daí que surge a ideia de que "gráficos 4K são mais pesados que 1080p" e "gráficos 1080p são mais detalhados/pesados 720p", é apenas algo natural que está acontecendo. Agora perceba que todas as operações que você faz com os jogos consiste em mexer com essas matrizes: rotacionar a câmera do jogo pode significa fazer uma transformação linear na matriz; aproximar ela pode significar calcular a inversa, etc. E a cada movimento que você faz, inúmeras operações são executadas e dependendo do seu equipamento, ele pode não dar conta disso tudo - e daí, a sua taxa de FPS começa a cair. Antes que me perguntem, o programador (mesmo que mexa com código puro) raramente se preocupa em fazer esse tipo de operação; existem várias bibliotecas já prontas para isso (o próprio DirectX e OpenGL tem funções de calcular o ortogonal da matriz, por exemplo).
O que eu quero falar é que, para esse nível de necessidade, se preocupar com recursos computacionais se torna realmente importante (it matters!); ou é isso, ou ninguém vai conseguir jogar teu jogo. Muitas vezes, o que os desenvolvedores fazem não são otimização no sentido estrito da palavra (ou seja, melhorar a forma de calcular), muitas vezes por falta de conhecimento, mas apenas se utilizar de recursos computacionais mais eficientes. Alguns que posso citar:
- Carregar tudo no cache ou na RAM.
- Jogar todo o trabalho para a GPU ou Coprocessadores.
- Realmente descer a nível de máquina.
O primeiro se trata daquela velha tela de "Loading" (um dos motivos, na verdade). Vamos pensar em termos de console que é mais fácil de analisar: o Xbox One tem 16 gb de RAM e 1 TB de HD. Um dos jogos que tenho, o Tomb Raider, é basicamente um bluray de 50 gb. Dentro de um sistema computacional, temos vários tipos de memória, onde o acesso ao Cache é muito mais rápido que o acesso à RAM que é mais rápida que o acesso ao HD. O mundo ideal seria que tudo pudesse ser acessado direto do cache, mas isso é impossível porque eles tem tamanho em escala de MBs; porém, a RAM é mais versátil. Daí, então, a ideia da tela de Loading é justamente pegarmos tudo que formos utilizar e está no HD (mais lento) e carregar na RAM (mais rápida) de forma que quando estivermos jogando, o tempo de acesso às texturas e afins vai diminuir e nossa vida será um pouco mais feliz. O que for ainda mais utilizado, pode ir pro cache. No fim, quando não precisarmos mais desses arquivos, descarregamos tudo e repetimos o processo.
GPUs são as placas de vídeo - e elas tem esse nome justamente porque permitem paralelizar as contas - em termos simples, você faz várias operações iguais (e muitas vezes, simples) ao mesmo tempo, ao invés de fazer tudo no "serial" (um por um), como é no seu processador, então seu tempo de processamento de uma determinada região do código naturalmente cai. Isso se dá porque a estrutura da placa é composta de vários núcleos de processamento (muitas vezes, passando de 2000 deles), e como muitas vezes as operações desse tipo são relacionadas a vídeos (somar uma matriz com outra, por exemplo), surgiu esse nome. Em geral, uma GPU processando as coisas em "serial" tende a ser mais devagar que uma CPU comum, mas a vantagem da paralelização permite o ganho e a vantagem de uso. É óbvio que você depende se o seu usuário vai ter ou não esse hardware. Em geral, se você não tem, você acaba fazendo suas continhas de forma mais devagar (porque processadores comuns, em geral, só tem até 8 núcleos virtuais, o que não é tão eficiente para esse tipo de coisa). Existem também o conceito de coprocessadores, que são análogos às GPUs, só que são processadores escravos que ficam esperando comandos da CPU mãe.
O terceiro método é mais complexo porque a linguagem de máquina que você vai utilizar em geral depende do hardware de quem está executando o código - dificilmente o mesmo código Assembly usado em um processador Intel vai ser o mesmo de um processador AMD (existem alternativas para contornar isso, como intrinsics do compilador, mas não vou abordar aqui). Por isso mesmo, somente quem realmente está mexendo com um hardware fraco (como celulares antigos) utiliza isso, visto que é um trabalho de corno. Mas, anyway, essa é a maior vantagem de se programar para consoles: como você sabe a configuração exata e que ela não vai mudar, você pode escrever teu codigozinho sabendo que não vai ter nenhuma dor de cabeça por incompatibilidade. Outras coisas incluem usar linguagens mais próximas de máquina - por exemplo, já cansei de citar, o Java tem uma parte do código denominada JNI (Java Native Interface) que permite você escrever código totalmente em C/C++ e fora do jardim da máquina virtual de forma a atingir performance (e daí tem mais chances de ter incompatibilidade). Jogos como o Minecraft (e, acredito, o Runescape) tem diversas partes escritas em C++ justamente porque a JVM não dá o desempenho que você precisa (e, muitas vezes, é melhor você dar mallocs, callocs e frees na medida que você precisa do que depender do garbage collector alheio).
E, agora, podemos entrar nos jogos 2D: eles não tem nada disso. O sistema de coordenadas é bidimensional, a quantidade de detalhes em geral também tende a ser menor e, por isso mesmo, as operações envolvidas são exponencialmente mais simples, e os computadores hoje em dia já são capazes de lidar com elas sem muitos problemas, mesmo que sejam relativamente mal programadas, e temos uma quantidade relativamente alta de memória RAM - texturas/sprites 2D são também muito menores que as usadas 3D na hora de se carregar, por exemplo. Quando buscamos aumentar a eficiência do nosso programa, temos que observar a forma que ele é estruturado: boas práticas de programação com reuso de código, por exemplo, acaba se tornando essencial. Também tem o fato que jogos em Java sempre vão estar sob uma camada extra - no caso, a JVM (ou Dalvik, no Android) - em comparação com a contrapartida em outras linguagens, de forma que o uso deles é naturalmente maior. A forma que você busca e organiza seus dados no database também é relevante, principalmente se a quantidade de dados for crescente. A ideia de carregar somente o que acharmos necessário na RAM e reusar também é essencial (se seu mapa é todo gramado, carregue apenas UMA vez aquele tile e faça tudo com ele), e a própria classe de renderização (por exemplo, a função de carregar imagens utilizada de forma ineficiente; a JGLW, e a Slick para 2D, se utiliza do OpenGl que está desenvolvido em C, então naturalmente o armazenamento e o acesso tende a ser mais rápido) pode influenciar. Em geral, dado que em jogos 2D existe um limite teórico do número de coisas que devem existir na tela ao mesmo instante, a RAM deve começar a se estabilizar em algum momento. Mas para se fazer uma análise mais complexa disso, é necessário terminar tudo que você quer, analisar o desempenho e fazer modificações para ver se diminui e lentamente ganhar experiência (repare que mesmo o Skype, que é um relativo programa de texto, usa 150mb de RAM).
Óbvio como existem exceções, como jogos que tem quadritrilhões de partículas, onde cada uma se movimenta para um lugar específico a ser calculado, ou jogos que tentam renderizar 3D a partir de ferramentas 2D (existe um projeto famoso que faz isso, se não me engano). Nesses casos, sim, cada detalhe importa, mas se você precisa desse tipo de coisa, muito provavelmente seu jogo está estruturado de forma errônea desde o começo e a complexidade de desenvolvimento só faz crescer.
Continua na parte 2... (se eu não ficar com preguiça de escrever após ter a ideia igual nesse post)