Como os gráficos do NES foram organizados?

imagem

Lançado em 1983, o console doméstico do Nintendo Entertainment System (NES) era uma máquina barata, porém poderosa, que alcançou um sucesso fenomenal. Usando a Unidade de processamento de imagens (PPU), o sistema pode criar gráficos bastante impressionantes para aqueles tempos, que até hoje parecem muito bons no contexto certo. O aspecto mais importante foi a eficiência da memória - ao criar gráficos, tivemos que gerenciar com o mínimo de bytes possível. No entanto, junto com isso, o NES forneceu aos desenvolvedores recursos poderosos e fáceis de usar que permitiram destacar-se dos consoles domésticos mais antigos. Tendo entendido os princípios da criação de gráficos NES, você pode sentir a perfeição técnica do sistema e perceber o quanto é mais fácil para os desenvolvedores de jogos modernos trabalharem.

Os gráficos de fundo do NES foram montados a partir de quatro componentes separados, cuja combinação formou a imagem que vemos na tela. Cada componente foi responsável por um aspecto separado; cores, layout, gráficos em pixels brutos, etc. Esse sistema pode parecer desnecessariamente complicado e complicado, mas no final ele usou a memória com muito mais eficiência e permitiu a criação de efeitos simples em uma pequena quantidade de código. Se você deseja entender os gráficos do NES, esses quatro componentes serão as principais informações.

Este artigo pressupõe que você esteja familiarizado com a matemática do computador e, em particular, com o fato de que 8 bits = 1 byte e 8 bits podem representar 256 valores. Também é necessário entender como a notação hexadecimal funciona. Mas mesmo sem esse conhecimento técnico, o artigo pode parecer interessante.

Breve revisão



Acima está uma imagem da primeira cena de Castlevania (1986): o portão que leva ao castelo, onde o jogo ocorrerá. Esta imagem tem 256 × 240 pixels e usa 10 cores diferentes. Para descrever essa imagem na memória, precisamos aproveitar a paleta de cores limitada e economizar espaço armazenando apenas uma quantidade mínima de informações. Uma das abordagens ingênuas é usar uma paleta indexada na qual cada pixel tem um volume de 4 bits, ou seja, 2 pixels são colocados em um byte. Isso exigirá 256 * 240/2 = 30720 bytes, mas como veremos em breve, o NES pode lidar com essa tarefa com muito mais eficiência.

Os principais conceitos do tema gráfico do NES são blocos e blocos [1]. Um bloco é uma área de 8 × 8 pixels e um bloco é uma área de 16 × 16 pixels, e cada um deles é vinculado a uma grade com o mesmo tamanho de célula. Depois de adicionar essas grades, podemos ver a estrutura dos gráficos. Aqui está a entrada do castelo com uma grade em dupla ampliação.


Nesta grade, os blocos são mostrados em verde claro e os azulejos em verde escuro. As réguas ao longo dos eixos têm valores hexadecimais que podem ser adicionados para encontrar uma posição; por exemplo, o coração na barra de status é de $ 15 + $ 60 = $ 75, que é decimal em 117. Cada tela contém 16 × 15 blocos (240) e 32 × 30 blocos (960). Agora vamos ver como esta imagem é descrita e começar com os gráficos de pixel não processados.

CHR


A estrutura CHR descreve gráficos de pixel "brutos" sem sua cor e posição e é definida em blocos. A página inteira da memória contém 256 blocos CHR e cada bloco possui uma profundidade de 2 bits. Aqui estão os gráficos do coração:


E aqui está como é descrito em CHR [2]:

pixel-heart-chr

Essa descrição leva 2 bits por pixel, ou seja, com um tamanho de 8 × 8, verifica-se 8 * 8 * 2 = 128 bits = 16 bytes. Em seguida, a página inteira ocupa 16 * 256 = 4096 bytes. Aqui estão todos os CHRs usados ​​na imagem de Castlevania.


Lembre-se de que o preenchimento de uma imagem requer 960 blocos, mas o CHR permite que você use apenas 256. Isso significa que a maioria dos blocos é repetida, em média, 3,75 vezes, mas mais frequentemente apenas um pequeno número deles é usado (por exemplo, fundo vazio, blocos monocromáticos ou padrões repetidos). A imagem do Castlevania usa muitos ladrilhos vazios, além de azul sólido. Para ver como os blocos são atribuídos, usamos tabelas de nomes.

NAMETABLE


A tabela de nomes atribui um arquivo CHR a cada posição na tela e há 960. Cada posição é especificada em um byte, ou seja, a tabela de nomes inteira ocupa até 960 bytes. As peças são atribuídas na ordem da esquerda para a direita, de cima para baixo e correspondem à posição calculada encontrada, adicionando os valores das réguas mostradas acima. Ou seja, a posição no canto superior esquerdo é $ 0, à direita é $ 1 e, abaixo, $ 20.

Os valores na tabela nomeada dependem da ordem em que o CHR é preenchido. Aqui está uma das opções [3]:


Nesse caso, o coração (na posição $ 75) tem um valor de $ 13.

Em seguida, para adicionar cor, precisamos selecionar uma paleta.

Paleta


O NES possui uma paleta de sistema de 64 cores [4] e, a partir dele, selecionamos as paletas que serão usadas na renderização. Cada paleta contém 3 cores exclusivas mais a cor geral do plano de fundo. A imagem possui no máximo 4 paletas, que ocupam no total 16 bytes. Aqui estão as paletas para a imagem do Castlevania:

castlevania-pal

Paletas não podem ser usadas arbitrariamente. Apenas uma paleta é aplicada por bloco. É por causa dessa necessidade de separar cada área 16 × 16 de acordo com a paleta de cores do jogo para que o NES tenha uma aparência de "bloqueio". Gráficos executados com maestria, por exemplo, na tela inicial do Castlevania, podem ser evitados misturando cores nas bordas dos blocos, o que oculta a presença de uma grade.

A seleção de uma paleta para cada bloco é realizada usando o último componente - atributos.

Atributos


Os atributos ocupam 2 bits por bloco. Eles determinam qual das 4 paletas usar. Esta imagem mostra quais paletas definidas pelos atributos usam blocos diferentes [5]:


Como você pode ver, as paletas são divididas em seções, mas isso é complicado devido ao uso das mesmas cores em diferentes áreas. Vermelho no meio do portão se funde com as paredes ao redor, e um fundo preto desfoca a linha entre o castelo
e portões.

Com 2 bits por bloco ou 4 blocos por byte, os atributos da imagem ocupam apenas 240/4 = 60 bytes, mas devido à maneira como são codificados, outros 4 bytes são desperdiçados, ou seja, são obtidos no total 64 bytes. Isso significa que a imagem inteira, incluindo CHR, tabela nomeada, paletas e atributos, ocupa 4096 + 960 + 16 + 64 = 5136 bytes - muito melhor que os 30720 mencionados acima.

MAKECHR


Criar esses quatro componentes para gráficos NES é mais difícil do que usar as APIs regulares de bitmap, mas as ferramentas são úteis. Os desenvolvedores do NES provavelmente tinham algum tipo de cadeia de ferramentas, mas o que quer que fosse, a história não a salvou. Hoje, os desenvolvedores costumam escrever programas para converter gráficos no formato NES desejado.

Todas as imagens neste post foram criadas usando o makechr , uma ferramenta reescrita usada pelo Star Versus . Esta é uma ferramenta de linha de comando projetada para compilações automatizadas e destinada a velocidade, mensagens de erro de qualidade, portabilidade e compreensibilidade. Ele também cria visualizações interessantes, como as usadas no post.

Referências


Principalmente conhecimento sobre programação para NES, e especialmente sobre criação de gráficos, obtive das seguintes fontes:


Anotações


[1] Terminologia - em alguns documentos, os blocos são chamados de "meta-tiles", o que pessoalmente me parece menos útil.

[2] Codificação CHR - 2 bits por pixel não são armazenados próximos um do outro. A imagem completa é salva primeiro apenas com os bits baixos e, em seguida, novamente salva apenas com os bits altos.

Ou seja, o coração será armazenado assim:

pixel-coração-baixopixel-coração-alto

Cada linha é um byte. Ou seja, 01100110 é $ 66, 01111111 é $ 7f. No total, os bytes do coração são assim:

$ 66 $ 7f $ ff $ ff $ ff $ 7e $ 3c $ 18 $ 66 $ 5f $ bf $ bf $ ff $ 7e $ 3c $ 18

[3] Tabela de nomes - neste gráfico do jogo, a tabela de nomes é usada de maneira diferente. Normalmente, as letras do alfabeto são mantidas na memória na vizinhança, incluindo Castlevania.

[4] System Palette - O NES não usa uma paleta RGB, e as cores reais renderizadas dependem da TV em particular. Emuladores geralmente usam paletas RGB completamente diferentes. As cores neste artigo correspondem à paleta expressa em makechr.

[5] Codificação de Atributos - Os atributos são armazenados em uma ordem estranha. Eles não vão da esquerda para a direita, de cima para baixo - a área do bloco 2 × 2 é codificada com um byte, na forma da letra Z. É por isso que 4 bytes são desperdiçados; a linha inferior é um total de 8 bytes.

pal-block-group

Por exemplo, um bloco de $ 308 é armazenado com $ 30a, $ 348 e $ 34a. Seus valores da paleta são 1, 2, 3 e 3 e são armazenados na ordem da posição mais baixa para a posição mais alta, ou 11 :: 11 :: 10 :: 01 = 11111001. Portanto, o valor de byte desses atributos é $ f9.

Parte 2


Na primeira parte, falamos sobre os componentes dos gráficos de plano de fundo do NES - CHR, tabela de nomes, paletas e atributos. Mas isso é apenas metade da história.

Para começar, na verdade existem duas tabelas de nomes [6]. Cada um deles tem seus próprios atributos para definir a cor, mas eles têm o mesmo CHR. O equipamento do cartucho determina sua posição: um ao lado do outro ou um acima do outro. A seguir, exemplos de dois tipos diferentes de locais - Lode Runner (1984) e Bubble Bobble (1988).


bolha-bobble-rolagem

Rolagem


Para aproveitar a presença de duas tabelas de nomes, a PPU suporta a capacidade de rolar por pixel ao mesmo tempo nos eixos X e Y. É controlada por um registro com exibição de memória em US $ 2005: escrever apenas dois bytes neste endereço move a tela inteira para o número desejado de pixels [7] . No momento do lançamento do NES, essa era a principal vantagem sobre outros consoles domésticos, nos quais a rolagem geralmente tinha que reescrever toda a memória de vídeo. Um esquema tão fácil de usar levou ao surgimento de um grande número de plataformas e atiradores e tornou-se a principal razão para um sucesso tão grande do sistema.

Para um jogo simples, cujo campo tem apenas duas telas, por exemplo, Load Runner, bastava preencher as duas tabelas de nomes e alterar a rolagem de acordo. Mas na maioria dos jogos de rolagem, os níveis tinham uma largura arbitrária. Para implementá-los, o jogo deve atualizar a parte fora da tela das tabelas de nomes antes que elas apareçam na tela. O valor da rolagem é repetido, mas, como a tabela de nomes é atualizada constantemente, isso cria a ilusão de tamanho infinito.


Sprites


Além de rolar pelas tabelas de nomes, o NES também tinha um aspecto completamente diferente dos gráficos: sprites. Ao contrário de tabelas de nomes que precisam ser alinhadas em grades, os sprites podem ser posicionados arbitrariamente, para que possam ser usados ​​para exibir os personagens dos jogadores, obstáculos, projéteis e quaisquer objetos com movimentos complexos. Por exemplo, na cena acima de Mega Man (1987) para exibir o personagem de um jogador. pontos e faixas de energia são sprites usados, o que lhes permite sair da grade das tabelas de nomes ao rolar a tela.

Sprites têm sua própria página CHR [8] e um conjunto de 4 paletas. Além disso, eles ocupam uma página de 256 bytes de memória. que lista a posição e a aparência de cada sprite (como se vê, a memória de vídeo do NES é duas vezes e meia maior do que a mencionada na primeira parte do artigo). O formato desses registros é bastante incomum - eles contêm primeiro uma posição em Y, depois um número de bloco, depois um atributo, depois uma posição em X [9]. Como cada registro ocupa 4 bytes, há uma restrição estrita: na tela não pode haver mais do que 256/4 = 64 sprites por vez.

Os bytes Y e X especificam o pixel superior esquerdo do sprite desenhado. Portanto, no lado direito da tela, o sprite pode ser cortado, mas no lado esquerdo ele deixa espaço vazio. O byte do bloco é semelhante ao valor na tabela de nomes, somente para esses blocos os sprites usam seu próprio CHR. Um byte de atributo é um pacote de bits que executa três tarefas: dois bits são alocados à paleta, dois bits são usados ​​para espelhar o sprite horizontal ou verticalmente e um bit determina se o sprite deve ser renderizado sob as tabelas de nomes [10].

Limitações


Os sistemas modernos permitem trabalhar com sprites de qualquer tamanho arbitrário, mas no NES, o sprite devido às limitações do CHR tinha que ter um tamanho de 8 × 8 [11]. Objetos maiores são compostos de vários sprites, e o programa deve garantir que todas as partes individuais sejam renderizadas uma ao lado da outra. Por exemplo, o tamanho de um personagem Megaman pode chegar a 10 sprites, o que também permite que você use mais cores, principalmente para os olhos brancos e o tom de pele.


A principal limitação associada ao uso de sprites é que não deve haver mais de 8 sprites por linha raster. Se mais de 8 sprites aparecerem em qualquer linha horizontal da tela, os que aparecerem mais tarde simplesmente não serão renderizados. Esta é a razão da cintilação em jogos com muitos sprites; o programa troca os endereços dos sprites na memória para que cada um deles seja renderizado pelo menos ocasionalmente.

megaman-flicker

Por fim, a rolagem não afeta os sprites: a posição do sprite na tela é determinada por seus valores Y e X, independentemente da posição da rolagem. Às vezes, isso é uma vantagem, por exemplo, quando o nível se move em relação ao jogador ou a interface permanece em uma posição fixa. No entanto, em outros casos, isso é um sinal de menos - você precisa mover o objeto em movimento e, em seguida, mudar sua posição pela quantidade de alterações na rolagem.

Anotações


[6] Em teoria, existem quatro tabelas de nomes, mas elas são espelhadas de forma que apenas duas delas contêm gráficos únicos. Quando colocados lado a lado, isso é chamado de espelhamento vertical e, quando as tabelas de nomes estão localizadas uma acima da outra, espelhamento horizontal.

[7] Há também um registro que seleciona com qual tabela de nomes começar a renderizar, ou seja, a rolagem é na verdade um valor de 10 bits ou 9 bits, se você considerar o espelhamento.

[8] Nem sempre é esse o caso. A PPU pode ser configurada para usar a mesma página CHR para tabelas de nomes e para sprites.

[9] Talvez essa ordem tenha sido usada porque corresponde aos dados que a PPU precisa processar para obter uma renderização eficiente.

[10] Este bit é usado para vários efeitos, por exemplo, para mover Mario sob os blocos brancos em Super Mario Bros 3, ou para tornar neblina sobre sprites em Castlevania 3.

[11] O PPU também tem uma opção para ativar sprites 8 × 16, que é usado em jogos como Contra, onde existem personagens altos. No entanto, todas as outras restrições se aplicam.

Parte 3


Nas partes anteriores, falamos sobre dados do CHR, planos de fundo baseados em tabelas de nomes, sprites e rolagem. E isso é praticamente tudo o que um simples cartucho NES pode fazer sem hardware adicional. Mas, para ir além, precisamos explicar em detalhes como a renderização funciona.

Renderização



Renderização de varredura com uma pausa para vblank

Como outros computadores antigos, o NES foi projetado para funcionar com televisões CRT. Eles desenham linhas de digitalização na tela, uma de cada vez, da esquerda para a direita, de cima para baixo, usando uma pistola de elétrons que se move fisicamente para o ponto na tela em que essas linhas são desenhadas. Depois de atingir o canto inferior, ocorre um período chamado "espaço em branco vertical" (ou vblank): a pistola de elétrons retorna ao canto superior esquerdo para se preparar para desenhar o próximo quadro. Dentro do NES, o PPU (Picture Processing Unit) realiza a renderização de varredura automaticamente, em cada quadro, e o código que trabalha na CPU executa todas as tarefas que o jogo deve executar. O Vblank permite que o programa substitua os dados na memória PPU, pois, caso contrário, esses dados serão usados ​​para renderização. Na maioria das vezes, são feitas alterações na tabela de nomes e paletas de PPU durante essa pequena janela.

No entanto, algumas alterações no estado da PPU podem ser feitas durante a renderização da tela. Eles são chamados de "efeitos raster". A ação mais comum executada durante a renderização da tela é definir a posição de rolagem. Graças a isso, parte da imagem permanece estática (por exemplo, a interface do jogo) e todo o resto continua rolando. Para alcançar esse efeito, é necessário selecionar com precisão o tempo para alterar o valor de rolagem, para que ocorra na linha de varredura desejada. Existem muitas técnicas para implementar esse tipo de sincronização entre o código do jogo e o PPU.

Tela dividida



O nível rola e a interface na parte superior da tela permanece estacionária

Em primeiro lugar, a PPU possui hardware interno que processa sprite na posição de memória zero de uma maneira especial. Ao renderizar esse sprite, se um de seus pixels se sobrepuser à parte visível do plano de fundo, um bit é definido como "sprite0 flag". O código do jogo pode primeiro colocar esse sprite no local em que a divisão da tela deve ocorrer e, em seguida, aguardar um loop, verificando o valor do sinalizador sprite0. Portanto, quando o loop for encerrado, o jogo saberá com certeza qual linha raster está sendo renderizada atualmente. Essa técnica é usada para implementar o compartilhamento simples de tela em muitos jogos da NES, incluindo Ninja Gaiden (1989), mostrado acima [12]

ninja-hud

Sprite0 está localizado em Y $ 26, X $ a0. Quando sua linha inferior de pixels é renderizada, o sinalizador sprite0 é definido

Em alguns jogos, o sinalizador sprite0 é combinado com outra técnica - loop previsível (“um ciclo com tempo previsível”): o programa espera até que algumas linhas extras sejam renderizadas para dividir a tela em mais partes.Por exemplo, essa técnica é usada em muitos protetores de tela Ninja Gaiden para criar efeitos dramáticos, por exemplo, um campo movido pelo vento ou uma imagem de um castelo à distância. O jogo executa tarefas como tocar música e aguardar a entrada do jogador, no início da renderização do quadro, depois usa o sprite0 para procurar a primeira divisão e, para todas as outras, usa loops cronometrados.

ninjas-em-campo

vista para o castelo

No entanto, a maioria dos jogos não pode gastar tempo esperando em ciclos, especialmente em cenas ativas em que o tempo da CPU vale seu peso em ouro. Nesses casos, é usado um equipamento especial instalado nos cartuchos (chamado de mapeador, porque usa seu próprio mapeamento na memória (mapeamento de memória)), que pode receber uma notificação sobre o momento de renderizar uma determinada linha raster [13], o que elimina completamente a necessidade de ciclos de espera. O código do jogo pode executar qualquer uma de suas tarefas e no momento desejado, para que o processador seja usado da melhor maneira possível. A maioria dos jogos mais modernos para o NES, que possuem muitas divisões de tela, usa mapeadores dessa maneira.

nível de trem

Aqui está um exemplo de Ninja Gaiden 2, que usa um mapeador para realizar várias divisões e simular a rolagem de paralaxe, o que cria uma sensação de grande velocidade, apesar do nível estático. Observe que todas as partes móveis individuais ocupam estritamente faixas horizontais; isto é, nenhuma das camadas de fundo pode se sobrepor a outra. Isso ocorre porque as separações são realmente implementadas alterando a rolagem de linhas raster individuais.

Troca de banco


Os mapeadores podem executar muitas outras funções, mas a mais comum delas é a troca de bancos. Esta é uma operação na qual todo o bloco de espaços de endereço é reatribuído para apontar para outra parte da memória [14]. Os bancos de comutação podem ser executados com o código do programa (que permite criar muitos níveis e músicas nos jogos), bem como com os dados do CHR, graças aos quais você pode substituir instantaneamente os blocos referenciados por tabelas de nomes ou sprites. Se você usar a alternância de bancos entre os quadros, poderá animar todo o fundo de cada vez. Mas quando usado como um efeito raster, isso permite desenhar gráficos completamente diferentes em diferentes partes da tela. Nos jogos da série Ninja Gaiden, essa abordagem é usada durante o processo do jogo para renderizar a interface separadamente do nível, bem como durante os protetores de tela,que permite armazenar cenas visuais e de texto em diferentes bancos CHR.

goofall-bg

,

goofall-nt

,


CHR. ,

quem são eles banco inferior

Na parte inferior, outro banco CHR é usado. Ao trocar de banco, o valor de rolagem também é redefinido.A

troca de banco também pode ser usada para rolagem de paralaxe, de forma limitada (mas ainda impressionante). Se a cena possui uma parte do plano de fundo composta de um padrão de repetição curto, esse mesmo padrão pode estar contido em vários bancos com um deslocamento de uma quantidade diferente. Em seguida, esse padrão pode ser rolado para um determinado valor alternando para o banco com o deslocamento correspondente. Essa técnica pode ser usada para rolagem de paralaxe, mesmo com sobreposição de fundo devido à presença de blocos que não são afetados pela troca de memória [15]. A desvantagem desse método é que, no total, todos os bancos precisam ocupar muito espaço do CHR.

metal-storm-bg

Metal Storm (1991) usa comutação de banco para rolagem camada a lado

metal-tempestade-nt

Repetir a tabela de nomes permite criar esse efeito

CHR com a troca de bancos - essa é uma ferramenta muito poderosa, mas tem suas limitações. Embora seja útil para animar a tela inteira, essa técnica não é muito adequada para substituir apenas uma pequena parte da tela; isso também requer alterações na tabela de nomes. Além disso, a quantidade de CHR no cartucho é limitada e, para mudar para os dados, eles devem primeiro existir. Finalmente, com exceção dos efeitos raster baseados em rolagem, o jogo sempre possui uma grade rigorosa de tabelas de nomes, o que limita a faixa dinâmica de efeitos gráficos.

Outros exemplos


vice fogo

O jogo Vice: Project Doom (1991) cria esse efeito de chama, definindo repetidamente a posição de rolagem em cada linha raster. O caractere em primeiro plano é criado a partir de sprites que não são afetados pela rolagem.

mestre das espadas

O Sword Master (1990) usa a troca de banco para rolar montanhas à distância, bem como dividir a tela da interface e a grama em primeiro plano.

Agradecimentos


Eu não seria capaz de gerar todos esses gráficos para um artigo sem as poderosas funções de depuração fornecidas pelo emulador FCEUX. Além disso, o wiki do site NesDev se tornou uma fonte útil de informações sobre o sprite0:


Anotações


[12] De fato, a situação com Ninja Gaiden é um pouco mais complicada. O jogo usa sprites de sprites 8 × 16 - um modo especial fornecido pela PPU que renderiza sprites como pares sobrepostos verticalmente. Ou seja, o sprite0 é completamente transparente e o sprite1 tem uma linha de pixels na parte inferior. Ele também define a camada z desses sprites para que sejam renderizados por trás da escuridão da interface, o que torna tudo invisível.

[13] Isso é bastante complicado de implementar. O código do jogo grava a linha raster desejada no espaço de endereço do mapeador. O mapeador intercepta solicitações de acesso à memória PPU, contando quando uma nova linha raster é renderizada. Ao atingir a linha de varredura desejada, ele gera uma interrupção do programa (IRQ) durante a qual o código do jogo é executado, fazendo o que é necessário durante essa linha de varredura específica.

[14] A comutação é realizada mapeando o equipamento na memória, interceptando as operações de acesso à memória e redefinindo a localização física da qual os dados são obtidos. O resultado é instantâneo, mas possui uma grande fracionalidade, por causa da qual os intervalos de endereços variam em 4 KB ou 8 KB.

[15] A única maneira de alternar bancos CHR sem afetar cada bloco é duplicar os dados do bloco entre os bancos ou ter um mapeador com menos granulação. Com esse mapeador, você pode alternar uma parte menor do banco, por exemplo, apenas 1 KB por vez, e todo o resto permanecerá inalterado.

Source: https://habr.com/ru/post/pt470614/


All Articles