Como posso fazer uma demonstração gráfica sem um buffer de quadros?

Aug 18 2020

Estou programando um microcontrolador - um minúsculo computador de 8 bits que tem cerca de 100 MHz - velocidade de clock de CPU de 300 MHz e 64 K de RAM de aplicativo (na verdade, ROM), mas apenas uma pequena quantidade de RAM (6 K) - nem de longe o suficiente para um buffer de quadro gráfico.

Portanto, desejo exibir uma demonstração de gráficos animados nesta máquina. Parece ter poder de computação suficiente para calcular gráficos animados de algum tipo.

Para que os gráficos animados sejam exibidos, de alguma forma precisarei ter um programa de computador que, cada vez que for chamado, retorne uma única linha de pixels de um quadro de imagem, que é então exibido. O programa controlador de exibição continua a fazer isso, chamando linha após linha até exibir um quadro inteiro e, em seguida, começa no topo novamente para o próximo quadro.

Só para ficar claro, não estou perguntando sobre o programa que controla a tela. Estou perguntando sobre o programa de demonstração. Preciso de um programa de demonstração que produza seus gráficos uma linha por vez, que meu controlador de vídeo jogará na tela.

Estou tentando imaginar que tipo de programa gráfico poderia funcionar dessa maneira? Presumivelmente, a exibição de algum tipo de fórmula matemática.

Alguém pode sugerir técnicas de programação gráfica que funcionem dessa forma, em que se possa chamar uma função, que calcule e retorne uma linha do quadro, apenas quando solicitada? Existem programas que funcionam assim que eu poderia examinar?

Espero que a pergunta faça sentido. Quaisquer sugestões na direção certa ou dicas serão apreciadas.

Respostas

4 NicolBolas Aug 18 2020 at 22:55

Este é um caso em que olhar para o funcionamento de um hardware mais retro seria razoável. O hardware mais antigo tinha fortes limitações de memória e capacidade de processamento. Seu chip de + 100 MHz é muito mais rápido do que a maioria dos chips de consumo dos anos 80 e anteriores. Portanto, embora essas CPUs precisem de chips gráficos dedicados para renderizar, sua CPU muito mais rápida provavelmente pode lidar com a tarefa de forma adequada.

Eu sugeriria começar com algo como a arquitetura tilemap-and-sprite do NES. Essas arquiteturas são projetadas para serem eficientes em termos de memória (aproveitando muito o espaço de armazenamento limitado) e computacionalmente eficientes em uma saída linha por linha, já que geraram dados de pixel e os enviaram diretamente para o dispositivo de exibição na velocidade esperada de um CRT.

A ideia geral é esta. Um mapa de blocos consiste em duas partes: uma série de blocos e uma matriz 2D de índices no mapa de blocos que representa a imagem que está sendo criada. Os tiles são tipicamente 8x8, e no NES eram 2 bits por pixel e os tiles usavam paletas. Ou, mais especificamente, o índice no mapa de bloco inclui não apenas o índice de um bloco, mas também o índice da paleta a ser usado com aquele bloco. Portanto, um bloco não está inerentemente associado a uma paleta; a associação é feita no ponto de uso (tecnicamente, no NES, o mapa de blocos e o mapa de paletas eram separados, pois cada bloco 2x2 de blocos deveria usar a mesma paleta).

Para permitir a rolagem, o mapa de blocos é maior do que a tela visível e há um deslocamento que representa onde o canto superior esquerdo da tela visível está dentro do mapa de blocos. Deixe este deslocamento estar na posição Xoffe Yoff.

Você pode ver como isso torna o processamento linha por linha trivial. Para gerar uma linha horizontal para a posição horizontal Ypos(no espaço da tela), você precisa obter o pixel inicial dentro do mapa de blocos. Isso requer a transformação da posição XY (0, Ypos)do espaço da tela para o espaço do mapa de blocos. Então você adiciona a ele o vetor (Xoff, Yoff), mantendo o vetor `` Xoff, Yoff + Ypos) `.

Observe que, ao fazer qualquer mapeamento do espaço da tela para o espaço do tilemap, o espaço do tilemap deve envolver os eixos X e Y. Portanto, sempre que você computar um novo pixel no espaço do mapa de mosaico, você deve envolvê-lo em torno do tamanho do espaço do mapa de mosaico.

Agora, precisamos dividir este pixel do tilemap em dois componentes 2D: o índice do tile dentro do tilemap que nos dá este pixel, e o pixel dentro do tile que precisamos buscar para este pixel. O índice do bloco é apenas o número inteiro do pixel do espaço do mapa do bloco dividido pelo tamanho do bloco. A coordenada de pixel é o pixel de espaço do mapa de mosaico inteiro modificado pelo tamanho do mosaico. Dado um tamanho de bloco de 8 x 8, você está fazendo o seguinte:

ivec2 tilemap_pixel = ...; //Compute the tilemap starting pixel as above.
ivec2 tilemap_tile = ivec2(tilemap_pixel.x & ~0x7, tilemap_pixel.y & ~0x7); //Mask off the lower 3 bits.
ivec2 pixel_in_tile = ivec2(tilemap_pixel.x & 0x7, tilemap_pixel.y & 0x7); //Mask off all but the lower 3 bits.

Assim tilemap_tile, agora temos o índice do bloco no qual estamos trabalhando no momento. E pixel_in_tilenos dá o pixel em que estamos trabalhando. Portanto, podemos buscar aquele pixel, cujo valor pode ser mapeado em uma paleta para produzir a cor final para aquele pixel. Ou podemos usá-lo diretamente.

Obter o próximo pixel é bastante trivial. Nós apenas incrementamos pixel_in_tile.xem 1 módulo o tamanho do ladrilho. Se o incremento exceder o tamanho do ladrilho, então também incrementamos tilemap_tile.xem 1, modulo o tamanho do ladrilho. E você continua até preencher a linha de pixels.

Existem muitas oportunidades para otimização de desempenho de tal algoritmo.

Os blocos provavelmente representam os maiores dados. Um conjunto de blocos de 128 elementos de blocos de 8x8, mesmo com 2bpp, é 2K. Mas o fato é que eles podem ser armazenados na ROM , já que provavelmente você não está alterando os blocos por conta própria. O tamanho do mapa de blocos (presumindo que ele deve estar na RAM) depende da resolução de saída desejada e do tamanho do bloco. Um mapa de blocos para blocos de 8x8 que pode cobrir uma tela de 320x240 tem 1.200 bytes. Não exatamente minúsculo. Você precisará ocupar mais memória se quiser espaço para uma rolagem suave (e, portanto, um mapa de blocos maior).

Dito isso, também é importante notar que o "tamanho de saída" não precisa ser o tamanho real do dispositivo de exibição . Por exemplo, se você pretende desenhar para um dispositivo de exibição de 1080p, você ainda pode renderizar para uma resolução interna que é, por exemplo, 8 vezes menor, 240x135. É fácil modificar este algoritmo para gerar essencialmente o mesmo valor de pixel 8 vezes consecutivas, bem como reutilizar a mesma linha 8 vezes por linha de 1080p. Na verdade, não seria muito difícil fazer com que esse algoritmo permitisse a rolagem de subpixel (rolagem no espaço 1080p, não no espaço 135p) e até mesmo adicionando alguma filtragem entre os valores dos pixels. Um mapa de blocos de 240x135 "tamanho de saída" exigiria apenas 510 bytes para a região visível, portanto, você teria mais espaço para uma área de rolagem maior.