Para o efeito de rolagem vertical na primeira parte de "The Legend of Zelda", são usadas manipulações gráficas de "hardware" do NES, provavelmente não fornecidas pelos desenvolvedores do console.
Não tenho acesso à documentação oficial da Unidade de processamento de imagens (PPU - chip gráfico) do console NES; portanto, é mais provável que minhas declarações sobre "comportamento indefinido" sejam suposições. Peguei a especificação do hardware gráfico do
NesDev Wiki . A PPU é controlada escrevendo nos registros com mapeamento de memória. Se você usar esses registros da maneira que (aparentemente) foi concebida pelos designers, seria impossível obter esse efeito:
Ao rolar a tela na vertical, a tela inteira deve rolar de uma só vez. O GIF anterior mostra um exemplo de rolagem vertical parcial. Parte da tela permanece estacionária (elementos da interface) e a outra parte (área de jogo) rola verticalmente. A rolagem vertical parcial é impossível de implementar com o trabalho "padrão" com PPU.
Por outro lado, a rolagem
horizontal parcial é totalmente definida e possível.
A gravação em um registro PPU separado no momento em que o quadro é desenhado pode levar a artefatos gráficos. A Lenda de Zelda intencionalmente causa um artefato que se manifesta como rolagem vertical parcial. Nesta postagem, falarei um pouco sobre o hardware gráfico do NES e explicarei como o truque de rolagem vertical funciona.
Tipos de gráficos
O console do NES possui dois tipos de gráficos:
- Sprites são peças que podem ser colocadas em locais arbitrários na tela e movidas independentemente uma da outra.
- Plano de fundo - uma grade de blocos que pode ser rolada suavemente como uma única imagem.
Para demonstrar a diferença entre os dois, mostrarei uma cena composta de sprites e plano de fundo:
E aqui está a mesma cena em que apenas sprites são visíveis:
E aqui está uma cena em que apenas o fundo é visível:
Rolagem
O processador de imagem (NES Picture Processor) suporta rolagem de imagens de fundo. Na memória gráfica, o gráfico de plano de fundo é armazenado como uma grade bidimensional de ladrilhos que cobrem uma área com o dobro da largura e altura da tela.
Uma “janela” é exibida na tela nesta grade, do tamanho de uma tela, e a posição dessa janela pode ser controlada com precisão. Movendo gradualmente a janela visível ao longo da grade, é criado um efeito de rolagem suave.
O sinal de vídeo NES de saída tem um tamanho de 256x240 pixels. A grade de blocos dentro da memória é representada como uma área de 512x480 pixels e é dividida em quatro retângulos do tamanho de uma tela chamados "tabelas de nomes". Os jogos podem configurar a unidade de processamento de imagens (PPU), indicando a posição da janela visível, selecionando a coordenada de pixel na grade das tabelas de nomes.
Quando você seleciona a coordenada (0, 0), toda a tabela de nomes no canto superior esquerdo será exibida na tela:
Passando para (125, 181), veremos um pouco de cada tabela de nomes:
A janela visível é minimizada na parte traseira da grade de blocos na memória. Passando para (342, 290), colocamos o canto superior esquerdo da tela visível dentro da tabela de nomes inferior direita e, graças à dobragem, partes de cada tabela de nomes estarão visíveis:
Memória insuficiente!
Cada tabela de nomes tem um tamanho de 1 KB, mas o NES aloca apenas 2 KB de sua memória de vídeo para essas tabelas, portanto, apenas duas tabelas de nomes podem caber na memória por vez.
Como ele pode ter quatro tabelas de nomes?
Espelhamento de tabelas de nomes
A memória de vídeo é conectada à PPU de tal maneira que, quando a PPU renderiza um bloco de uma das quatro tabelas de nomes aparentes, na verdade uma das duas tabelas reais é selecionada e a leitura vem daí. Em essência, isso significa que as quatro tabelas de nomes visíveis são realmente compostas por dois pares idênticos de tabelas.
Esta imagem mostra uma captura instantânea do conteúdo de todas as quatro tabelas. O canto superior esquerdo e o canto superior direito são os mesmos que os dois inferiores.
Por que simplesmente não manter duas tabelas de nomes?
Felizmente, a ligação exata entre as tabelas aparente e a real pode ser configurada em tempo de execução. Se o jogo quiser executar a rolagem horizontal, ele ajusta o equipamento gráfico para que as tabelas superior esquerda e superior direita sejam diferentes e possam ser roladas sem duplicação perceptível. Nessa configuração, as tabelas superior esquerda e inferior esquerda se referem à mesma tabela de nome real; da mesma forma para as duas tabelas certas. Essa configuração é chamada de espelhamento vertical.
Há também outra configuração possível - “Espelhamento Horizontal”, que os jogos usam para rolagem vertical.
Normalmente, os jogos não rolam na diagonal, porque cria artefatos ao redor das bordas da tela devido ao espelhamento das tabelas de nomes.
Cartuchos
O cartucho de cada jogo possui um hardware que permite configurar o espelhamento de tabela.
Alguns jogos não precisam mudar o espelhamento, portanto o espelhamento horizontal ou vertical é codificado em seus cartuchos. Outros jogos alternam dinamicamente entre esses dois modos, portanto, o espelhamento em seus cartuchos é configurado programaticamente. The Legend of Zelda pertence à segunda categoria. Finalmente, os cartuchos de alguns jogos realmente complexos têm memória de vídeo adicional, ou seja, eles não precisam de espelhamento: eles podem rolar simultaneamente na vertical e na horizontal sem artefatos visíveis de duplicação.
Exemplo real
Um exemplo de rolagem vertical exibida na tela.Isso mostra um registro de tabelas de nomes com espelhamento horizontal. A janela atualmente visível é destacada.Lembre-se de que a rolagem mais vertical não é incomum - a coisa incomum é a rolagem vertical com
tela dividida .
Tela dividida
Cada quadro do sinal de vídeo gerado pelo NES é renderizado de cima para baixo, uma linha de pixels por vez. Em cada linha, os pixels são desenhados um de cada vez, da esquerda para a direita. No meio da renderização do quadro, o jogo pode reconfigurar a PPU, o que afeta a exibição dos pixels que ainda não foram renderizados. Uma das alterações mais comuns no meio do quadro é atualizar a posição de rolagem horizontal.
Ao rolar horizontalmente entre as salas, The Legend of Zelda sempre começa na posição de rolagem (0, 0) e renderiza os elementos da interface na parte superior da tela. Depois de desenhar a última linha de pixels da interface na tela, a rolagem horizontal muda em um valor que aumenta a cada quadro, para que a câmera se mova sem problemas.
A animação da exibição das tabelas de nomes mostra como o jogo muda do espelhamento horizontal para o vertical antes de rolar e, em seguida, novamente para o horizontal após a conclusão da transição. Além disso, enquanto a rolagem continua, as tabelas de nomes superior esquerdo (e inferior esquerdo) são atualizadas e uma cópia da sala em que o jogador entra é gravada nelas. Após a rolagem, o jogo deixa de dividir a tela e novamente é renderizado inteiramente da tabela superior esquerda.
Medição de renderização
Para dividir a tela na posição desejada, o jogo precisa descobrir de alguma forma qual parte do quadro atual foi desenhada. As seqüências de pixels são renderizadas em uma frequência conhecida, portanto, o número das seqüências de pixels renderizadas pode ser determinado contando o número de ciclos do processador que passaram desde o início do quadro.
Existe outra técnica mais precisa, chamada Sprite Zero Hit.
O NES pode renderizar até 64 sprites por vez. O primeiro sprite na memória de vídeo é chamado Sprite Zero (zero sprite). Em cada quadro, assim que um pixel opaco de um sprite zero é sobreposto a um pixel de fundo opaco, o evento Sprite Zero Hit ocorre. Ele define um pouco em um dos registradores PPU com mapeamento de memória, que pode ser verificado pelo processador.
Para usar o Sprite Zero Hit para dividir a tela, os jogos colocam o sprite zero em uma posição vertical perto da borda dividida e, durante a renderização, eles constantemente verificam se o evento Sprite Zero Hit ocorreu. Nesse caso, o jogo muda da rolagem horizontal para implementar a separação.
A transição horizontal entre salas com e sem fundo é mostrada abaixo.
O círculo marrom que aparece no início da transição e desaparece no final é um sprite zero. Vamos dar uma olhada na interface com e sem fundo:
Um sprite zero é um sprite de bomba branqueada que combina perfeitamente com o sprite de bomba comum da interface do jogo. O sprite zero está configurado para aparecer em segundo plano, mas como os pixels pretos da interface são considerados transparentes, a bomba de sprite zero seria visível se não estivesse estrategicamente escondida atrás da bomba a partir da interface.
Observe que o Sprite Zero Hit ocorre algumas linhas de pixels antes da linha inferior da interface. Ocorre no pixel superior do fusível da bomba, que fica a 16 pixels da parte inferior da interface. Quando o Sprite Zero Hit ocorre, o jogo começa a contar os ciclos do processador e, após completar o número necessário de ciclos, define a rolagem horizontal.
Supressão do feixe
Na maioria das vezes, o PPU do console atrai pixels para a tela. Há um curto tempo de inatividade entre os quadros durante os quais a renderização não é executada. Esse fenômeno é chamado de apagamento (vertical em branco ou vblank). Alguns tipos de alterações na configuração da PPU podem ser feitas apenas durante o vblank.
Registro de rolagem
Os jogos alteram a posição de rolagem gravando no registro PPU chamado
PPUSCROLL
, que mapeia para o endereço de memória
0x2005
. A primeira operação de gravação no
PPUSCROLL
define o componente X da posição de rolagem e a segunda operação define o componente Y. Da mesma forma, a gravação alternativa é realizada ainda mais.
A seguir, são mostradas todas as operações de gravação diferentes de zero no
PPUSCROLL
durante esta reprodução (em câmera lenta) de 16 quadros da tela com a plotagem do jogo. O componente de posição de rolagem Y é incrementado a cada dois quadros. Todas as operações de gravação no
PPUSCROLL
neste exemplo são realizadas durante o vblank, o que faz com que todo o plano de fundo role junto com ele.
Tela de rolagem dividida
As operações de
PPUSCROLL
no
PPUSCROLL
durante o vblank entram em vigor no início do quadro desenhado imediatamente após o vblank. Se a posição de rolagem mudar durante a renderização do quadro (ou seja, não durante o vblank), essa alteração entrará em vigor quando o desenho atingir a próxima linha de pixels. A rolagem horizontal parcial é implementada escrevendo para
PPUSCROLL
enquanto o PPU desenha a última linha de pixels antes da rolagem.
Ao atualizar a posição de rolagem no meio do quadro, apenas a posição X da posição de rolagem é aplicada. Ou seja, o componente de posição de rolagem Y é descartado. Assim, se o jogo quiser dividir a tela e mudar a posição de rolar parte do quadro, ele poderá rolar apenas horizontalmente.
E, no entanto:
Acredite ou não, o valor do registro
PPUSCROLL
não mudou durante esta transição.
Você pode ver um artefato gráfico com um pixel de altura sob a interface. Este é um erro do meu emulador causado pela falta de sincronização dos ciclos de clock do processador com a renderização pixel por pixel.
Intervenção em outros registros
O segundo registro, chamado
PPUADDR
, mapeado para o endereço de memória
0x2006
, é usado para definir o endereço de memória de vídeo atual. Quando um jogo, por exemplo, deseja alterar um dos blocos na tabela de nomes, primeiro grava o endereço de memória de vídeo do
PPUADDR
em
PPUADDR
e, em seguida, grava o novo valor do
PPUDATA
em
PPUDATA
- este é o terceiro registro mapeado para o endereço
0x2007
.
Gravar no
PPUADDR
não durante o vblank (ou seja, ao renderizar um quadro) pode causar artefatos gráficos. Isso ocorre porque a cadeia PPU, que é afetada pela gravação no
PPUADDR
, também é diretamente controlada pelo dispositivo PPU no processo de obtenção de blocos da memória de vídeo para desenhá-los. Como o processo de renderização na tela é realizado de cima para baixo e da esquerda para a direita na linha, a PPU atribui essencialmente a
PPUADDR
valor do endereço do
PPUADDR
atual que
PPUADDR
desenhado. Quando a renderização é movida de um bloco para outro, o
PPUADDR
é incrementado pelo valor atual.
Assim, a gravação em
PPUADDR
no meio do quadro pode alterar os blocos recebidos pela PPU da memória durante o quadro atual.
Vamos
PPUADDR
operações de gravação para o
PPUADDR
durante o salto vertical. Como a tabela de nomes também é atualizada durante a transição, a saída de
todas as operações de gravação no
PPUADDR
será muito extensa. Com uma transição horizontal, a rolagem é definida durante a renderização de uma linha de pixels 63, portanto, consideraremos operações de gravação no
PPUADDR
somente durante essa linha.
O padrão é claramente visível. A cada dois quadros, o endereço registrado na linha de pixels 63 é reduzido em 32 (0x20). Mas como isso leva a uma atualização na posição real de rolagem?
Real Scrolling Register
Dentro da PPU, há um registro de 15 bits não mapeado para a CPU. É usado como o endereço atual para acessar a memória de vídeo e como uma configuração de rolagem em segundo plano.
Ao trabalhar com esse valor como um endereço, o bit 14 é ignorado e os bits 0-13 são tratados como um endereço na memória de vídeo.
Ao trabalhar com esse valor como uma configuração de rolagem, suas diferentes partes têm significados diferentes:
Selecionar uma tabela de nomes é um valor de 0 a 3 que determina a tabela de nomes atual a partir da qual o desenho é feito.
A rolagem grossa em X e a
rolagem grossa em Y determinam a coordenada do bloco dentro da tabela de nomes selecionada. Este é o bloco atual a ser desenhado.
A rolagem exata ao longo de Y contém um valor de 0 a 7, que determina o deslocamento vertical atual da linha de pixels dentro do bloco atual. As peças são quadrados com um lado de 8 pixels.
A rolagem exata no X está ausente neste registro. Há um registro separado contendo apenas o deslocamento horizontal do pixel atual, mas não é importante para explicar como a rolagem vertical é realizada em The Legend of Zelda.
O que acontece com esse registro quando um jogo grava no
PPUADDR
? Aqui estão as três primeiras operações de gravação da demonstração mostrada acima.
Ao dividir as entradas no endereço em componentes de rolagem, você pode entender claramente o que está acontecendo aqui. A cada dois quadros, o valor da
rolagem aproximada em Y diminui, o que leva à rolagem vertical em um bloco ou 8 pixels.
Ao longo de cada quadro, o deslocamento de rolagem inicial é 0,0, após o qual a gravação na linha de pixels 63 é realizada no endereço. Isso significa que as primeiras 63 linhas de pixels são desenhadas na parte superior da tabela de nomes selecionada que contém o plano de fundo da interface. No entanto, a 64ª linha de pixels é renderizada ainda mais com a rolagem vertical aplicada a partir desse endereço. Como a rolagem vertical diminui a cada dois quadros, dá a sensação de rolagem vertical de uma parte da tela.
Role para baixo para rolar para cima
The Legend of Zelda não pode esconder esse truque dos jogadores completamente. Ele cria um artefato visível nas transições verticais da tela, que são perceptíveis se você olhar atentamente. Ao se mover entre as salas, o primeiro quadro da animação de rolagem rolará para baixo. Aqui está a animação em câmera muito lenta.
Na tabela de nomes, você pode ver o que realmente está acontecendo. Embora possa parecer aos jogadores que a área visível irá rolar para cima sem problemas, a transição da rolagem começa movendo a área visível da tabela superior esquerda de nomes para a tabela inferior esquerda, que contém uma cópia do plano de fundo da sala. Isso é necessário porque a interface na parte superior da tela também faz parte da tabela de nomes e, se a área visível rolou para cima a partir de sua posição original, ela passaria pela interface.
A rolagem vertical é implementada gravando no registro
PPUADDR
no meio do quadro. O primeiro valor a ser gravado é
0x2800
. Dois quadros depois,
0x23A0
gravado e o valor começa a diminuir em 32 a cada segundo quadro.
Gravar o valor
0x2800
no registro
0x2800
PPUADDR
Tabela de PPUADDR
como 2, que renderiza a tabela de nomes inferior esquerda. Como os dois valores de rolagem são 0, ele começará no bloco superior esquerdo desta tabela de nomes. No entanto, a
rolagem exata em Y é 2, portanto, há um deslocamento vertical de dois pixels na parte superior da tabela de nomes inferior esquerda. É por isso que, no primeiro quadro da transição, vemos uma barra preta com 2 pixels de altura na parte inferior da tela. O valor de rolagem inicial da animação de transição é deslocado 2 pixels para baixo para tornar a transição perfeita.
Dois quadros depois, o
PPUADDR
gravado em
0x23A0
. Isso nos leva de volta à tabela de nomes no canto superior esquerdo e renderizamos a partir da 29ª linha de peças, ou seja, a parte inferior.
A rolagem exata em Y ainda contém 2.
Por que é necessário
definir a rolagem exata em Y para 2? Por que o jogo não escreve apenas
0x0800
e
0x03A0
para não sofrer um deslocamento de dois pixels?
Quatro tabelas de nomes ocupam a área de 4 KB no espaço de endereço PPU, de
0x2000
a
0x2FFF
. Cada bloco na tabela ocupa um byte da memória de vídeo (na verdade, são apenas índices em outra tabela), e a ordem dos blocos e tabelas de nomes na memória de vídeo é tal que
Selecionando uma tabela de nomes ,
rolagem grossa por Y e
rolagem grossa por X compõem o deslocamento do bloco dentro áreas de memória com tabelas de nomes. Ou seja, pegando os 12 bits mais baixos do registro PPU interno e adicionando-os a
0x2000
, é possível encontrar o endereço do
0x2000
na memória de vídeo. E isso não é coincidência! É exatamente assim que o registro deve ser tratado: como um registro de endereço e como um registro de rolagem.
Mas há uma falha.
Ao processar como um registro de endereço, os bits 12 e 13 são considerados parte do endereço. Durante a renderização, a PPU sobrescreve constantemente o registro com o endereço do bloco renderizado atual. Como os blocos estão localizados nas tabelas de nomes e as tabelas estão na área de memória de
0x2000
a
0x2FFF
, o PPU atribui valores desse intervalo ao registro.
Quando o jogo grava no
PPUADDR
no meio do quadro, se não anotar o endereço do bloco na tabela de nomes, o PPU tentará ler
de outro lugar na memória de vídeo. Quaisquer bytes que ele conte serão percebidos como blocos, o que provavelmente levará a resultados indesejáveis. Portanto, todos os valores registrados no meio do quadro no
PPUADDR
devem estar no intervalo de
0x2000
a
0x2FFF
. Tomando cada número nesse intervalo e levando em consideração seus componentes de rolagem, o valor
exato de rolagem em Y deve sempre ser igual a 2.
Essa limitação significa que não podemos alterar a
rolagem exata na direção
Y no meio do quadro, ou seja, ao usar esse truque para implementar a rolagem vertical da separação da tela, estamos limitados à rolagem de 8 pixels por vez e sempre temos um deslocamento vertical de dois pixels da borda do bloco. The Legend of Zelda move 4 pixels por quadro ao rolar horizontalmente, mas 8 pixels por quadro ao rolar verticalmente, e agora sabemos o porquê.
O artefato também é perceptível ao rolar entre as salas para baixo, mas, neste caso, ocorre no final da animação.
Leitura adicional
Anotações
Até eu descobrir o registro interno da PPU, meu emulador mostrava o efeito de apagar durante as transições verticais da tela The Legend of Zelda.
O sprite do link desceu a tela, como deveria ser, mas o plano de fundo não rolou. A eliminação foi causada pelo fato de o jogo atualizar gradualmente a tabela de nomes para conter os gráficos da nova sala, mas não atualizar a rolagem para manter as atualizações fora da tela.