Moteur de rendu/d'animation basé sur du texte pour le terminal
Ce projet a été fortement inspiré par le projet populaire drawille, qui permet de dessiner sur le terminal en utilisant les caractères braille unicode.
L'avantage de dessiner avec des caractères braille par rapport aux caractères ASCII normaux est simple : comme chaque "caractère braille" est composé de 2 x 4 = 8
points possibles, nous avons 256
des variantes possibles que nous pouvons dessiner par caractère. Ces motifs braille permettent un dessin beaucoup plus "fin / plus fluide".
Mon implémentation est également livrée avec un moteur de rendu qui permet d'animer tout ce qui est dessiné à l'écran en utilisant la bibliothèque ncurses. Mon implémentation vise à être très performante en :
- Utilisation d'une quantité minimale de mémoire.
- Avoir une très bonne autonomie.
tout en restant simple d'utilisation.
Voici quelques exemples qui montrent ce qui peut être fait avec cette bibliothèque. Ces exemples peuvent également être trouvés dans examples.c
:
Je suis déjà assez satisfait de la mise en œuvre de ma structure de grille, qui stocke et accède aux données de manière très compacte. Je suis curieux de savoir si les performances de la structure de rendu peuvent être encore améliorées ? J'essaie déjà de rendre uniquement ce qui a changé par rapport à l'image précédente, mais peut-être que je peux l'optimiser encore plus ?
De plus, je ne sais pas si mon implémentation fait bon usage des directives de codage de style C. De plus, je veux m'assurer que la bibliothèque est conviviale. Alors, faites-moi savoir quelle fonctionnalité vous (en tant qu'utilisateur) attendez de l'API de cette bibliothèque, et s'il y a quelque chose qui vous manque lorsque vous l'utilisez dans l'état actuel.
grille.c
#define _POSIX_C_SOURCE 199309L
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <time.h>
#include "grid.h"
#include "unicode.h"
#include "constants.h"
grid *grid_new(int grid_width, int grid_height)
{
if ((grid_width % 2 != 0) || (grid_height % 4 != 0))
return NULL;
grid *p_grid = calloc(1, sizeof(*p_grid));
p_grid->width = grid_width;
p_grid->height = grid_height;
p_grid->buffer_size = grid_width / group_width * grid_height / group_height;
p_grid->buffer = calloc(p_grid->buffer_size, sizeof(int));
return p_grid;
}
void grid_free(grid *p_grid)
{
free(p_grid->buffer);
free(p_grid);
}
void grid_clear(grid *g)
{
for (int i = 0; i < g->buffer_size; ++i)
{
g->buffer[i] = 0x00;
}
}
void grid_fill(grid *g)
{
for (int i = 0; i < g->buffer_size; ++i)
{
g->buffer[i] = 0xFF;
}
}
void grid_print_buffer(grid *g, char* tag) {
printf(tag);
for (int i = 0; i < g->buffer_size; i++)
{
printf("0x%02x%s", g->buffer[i], i == g->buffer_size - 1 ? "\n" : ",");
}
}
void grid_modify_pixel(grid *g, int x, int y, int value)
{
// ToDo validate coords
int bytes_per_line = g->width / group_width;
int byte_idx = (x / group_width) + (y / group_height) * bytes_per_line;
int bit_idx = (x % group_width) * group_height + (y % group_height);
g->buffer[byte_idx] = (g->buffer[byte_idx] & ~(1 << bit_idx)) | (value << bit_idx);
}
void grid_set_pixel(grid *g, int x, int y)
{
grid_modify_pixel(g, x, y, 1);
}
void grid_unset_pixel(grid *g, int x, int y)
{
grid_modify_pixel(g, x, y, 0);
}
void grid_draw_line(grid *g, int x1, int y1, int x2, int y2)
{
// Bresenham's line algorithm
int x_diff = x1 > x2 ? x1 - x2 : x2 - x1;
int y_diff = y1 > y2 ? y1 - y2 : y2 - y1;
int x_direction = x1 <= x2 ? 1 : -1;
int y_direction = y1 <= y2 ? 1 : -1;
int err = (x_diff > y_diff ? x_diff : -y_diff) / 2;
while (1)
{
grid_set_pixel(g, x1, y1);
if (x1 == x2 && y1 == y2)
{
break;
}
int err2 = err;
if (err2 > -x_diff)
{
err -= y_diff;
x1 += x_direction;
}
if (err2 < y_diff)
{
err += x_diff;
y1 += y_direction;
}
}
}
void grid_draw_triangle(grid *g, int x1, int y1, int x2, int y2, int x3, int y3)
{
// ToDo: Add filling algorithm
grid_draw_line(g, x1, y1, x2, y2);
grid_draw_line(g, x2, y2, x3, y3);
grid_draw_line(g, x3, y3, x1, y1);
}
grille.h
#ifndef GRID_H
#define GRID_H
typedef struct
{
int width;
int height;
int buffer_size;
int *buffer;
} grid;
grid *grid_new(int grid_width, int grid_height);
void grid_free(grid *p_grid);
void grid_clear(grid *g);
void grid_fill(grid *g);
void grid_print_buffer(grid *g, char* tag);
void grid_modify_pixel(grid *g, int x, int y, int value);
void grid_set_pixel(grid *g, int x, int y);
void grid_unset_pixel(grid *g, int x, int y);
void grid_draw_line(grid *g, int x1, int y1, int x2, int y2);
void grid_draw_triangle(grid *g, int x1, int y1, int x2, int y2, int x3, int y3);
#endif
moteur de rendu.c
#include "grid.h"
#include "unicode.h"
#include "renderer.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "constants.h"
#include <ncurses.h>
#include <unistd.h>
#include <locale.h>
render_context* p_render_context;
const int braille_offset = 0x2800;
const int TRANSFORMATION_MATRIX[8] ={ 0x01, 0x02, 0x04, 0x40, 0x08, 0x10, 0x20, 0x80 };
wchar_t lookup_table[256] ={};
void renderer_new(grid *p_grid) {
// Set locale for ncurses to process unicode correctly
setlocale(LC_ALL, "");
// Generate braille lookup table
grid_generate_lookup_table();
// Create copy of initial grid for caching, but zero out buffer
grid *p_cached_grid = calloc(1, sizeof(*p_grid));
p_cached_grid->width = p_grid->width;
p_cached_grid->height = p_grid->height;
p_cached_grid->buffer_size = p_grid->buffer_size;
p_cached_grid->buffer = calloc(p_grid->buffer_size, sizeof(int));
// Store cached grid in render_context
p_render_context = calloc(1, sizeof(*p_render_context));
p_render_context->p_cached_grid = p_cached_grid;
p_render_context->frames_rendered = 0;
// Initialize ncurses
initscr();
noecho();
curs_set(0);
}
void renderer_update(grid* p_grid)
{
// Notes:
// Should only render the characters that changed from current grid buffer to the cached one
// Iterate over grid and look for differences to cached_grid
for (int i = 0; i < p_grid->buffer_size; i++)
{
// Difference was found, note that this character must be re-rendered
if (p_grid->buffer[i] != p_render_context->p_cached_grid->buffer[i]) {
// Compute row and column index of the character we need to re-render
int pos_x = i % (p_render_context->p_cached_grid->width / group_width);
int pos_y = i / (p_render_context->p_cached_grid->width / group_width);
// Obtain correct braille character
char uc[5];
int braille = lookup_table[p_grid->buffer[i]];
int_to_unicode_char(braille, uc);
// Linebreak if we reached the right end of the grid
if (i % (p_grid->width / group_width) == 0 && i != 0)
{
printw("\n");
}
// Render the braille character at the position that changed
mvprintw(pos_y, pos_x, uc);
//printw("Change index %i [%i->%i] Rerendering coordinate (%i, %i).\n", i, p_render_context->p_cached_grid->buffer[i], p_grid->buffer[i], pos_x, pos_y);
}
}
// ToDo: Update p_cached_grid
p_render_context->frames_rendered++;
//grid_print_buffer(p_render_context->p_cached_grid, "cached: ");
//grid_print_buffer(p_grid, "current: ");
// Update cached buffer with current one
memcpy(p_render_context->p_cached_grid->buffer, p_grid->buffer, sizeof(int) * p_grid->buffer_size);
// Sleep some milliseconds so that changes are visible to the human eye
napms(render_delay_ms);
// Refresh terminal to render changes
refresh();
}
void renderer_free()
{
// Wait before all allocations are free'd
napms(2000);
// Free all allocations and end ncurses window
free(p_render_context->p_cached_grid->buffer);
free(p_render_context->p_cached_grid);
free(p_render_context);
endwin();
}
void grid_generate_lookup_table()
{
for (int i = 0; i < 256; ++i)
{
int unicode = braille_offset;
for (int j = 0; j < 8; ++j)
{
if (((i & (1 << j)) != 0))
{
unicode += TRANSFORMATION_MATRIX[j];
}
}
lookup_table[i] = unicode;
}
}
moteur de rendu.h
#ifndef RENDERER_H
#define RENDERER_H
#include "grid.h"
typedef struct {
grid* p_cached_grid;
int frames_rendered;
} render_context;
void renderer_new(grid* p_grid);
void renderer_update(grid* p_grid);
void renderer_free();
void grid_generate_lookup_table();
#endif
unicode.c
void int_to_unicode_char(unsigned int code, char *chars)
{
if (code <= 0x7F)
{
chars[0] = (code & 0x7F);
chars[1] = '\0';
}
else if (code <= 0x7FF)
{
// one continuation byte
chars[1] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[0] = 0xC0 | (code & 0x1F);
chars[2] = '\0';
}
else if (code <= 0xFFFF)
{
// two continuation bytes
chars[2] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[1] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[0] = 0xE0 | (code & 0xF);
chars[3] = '\0';
}
else if (code <= 0x10FFFF)
{
// three continuation bytes
chars[3] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[2] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[1] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[0] = 0xF0 | (code & 0x7);
chars[4] = '\0';
}
else
{
// unicode replacement character
chars[2] = 0xEF;
chars[1] = 0xBF;
chars[0] = 0xBD;
chars[3] = '\0';
}
}
unicode.h
#ifndef UNICODE_H
#define UNICODE_H
void int_to_unicode_char(unsigned int code, char *chars);
#endif
constantes.c
const int group_height = 4;
const int group_width = 2;
const int render_delay_ms = 10;
constantes.h
#ifndef CONSTANTS_H
#define CONSTANTS_H
extern const int group_height;
extern const int group_width;
extern const int render_delay_ms;
#endif
exemples.c
#include <math.h>
#include "grid.h"
#include "renderer.h"
#include <stdio.h>
void example_filling_bar()
{
int width = 100;
int height = 24;
grid *g = grid_new(width, height);
renderer_new(g);
// Fill grid from left to right (simple animation)
renderer_update(g);
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
grid_set_pixel(g, i, j);
}
renderer_update(g);
}
// Free allocations
renderer_free();
grid_free(g);
}
void example_build_block()
{
int width = 100;
int height = 40;
grid *g = grid_new(width, height);
renderer_new(g);
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
grid_set_pixel(g, x, y);
renderer_update(g);
}
}
// Free allocations
renderer_free();
grid_free(g);
}
void example_sine_tracking()
{
int width = 124;
int height = 40;
grid *g = grid_new(width, height);
renderer_new(g);
double shift = 0;
while (1)
{
grid_clear(g);
// Draw line
grid_draw_line(g, 0, height / 2, width - 1, (height + sin(shift) * height) / 2);
// Draw curve
for (int j = 0; j < width; j++)
{
grid_set_pixel(g, j, (height / 2 * sin(0.05 * j + shift) + height / 2));
}
// Move curve
shift += 0.05;
renderer_update(g);
}
// Free allocations
renderer_free();
grid_free(g);
}
void example_spiral_effect()
{
int width = 60;
int height = 32;
grid *g = grid_new(width, height);
renderer_new(g);
// Start with an empty grid
grid_clear(g);
int m = width, n = height;
int sr = 0, sc = 0, er = m - 1, ec = n - 1;
while (sr <= er && sc <= ec)
{
for (int i = sc; i <= ec; ++i)
{
grid_set_pixel(g, sr, i);
renderer_update(g);
}
for (int i = sr + 1; i <= er; ++i)
{
grid_set_pixel(g, i, ec);
renderer_update(g);
}
for (int i = ec - 1; sr != er && i >= sc; --i)
{
grid_set_pixel(g, er, i);
renderer_update(g);
}
for (int i = er - 1; sc != ec && i > sr; --i)
{
grid_set_pixel(g, i, sc);
renderer_update(g);
}
sr++, sc++;
er--, ec--;
}
// Free allocations
renderer_free();
grid_free(g);
}
exemples.h
#ifndef EXAMPLES_H
#define EXAMPLES_H
#include "grid.h"
void example_filling_bar();
void example_build_block();
void example_sine_tracking();
void example_spiral_effect();
#endif
principal c
#include <stdio.h>
#include <unistd.h>
#include <math.h>
#include "examples.h"
int main()
{
//example_sine_tracking();
//example_build_block();
example_spiral_effect();
return 0;
}
Et enfin, le Makefile pour tout compiler :
prog:
gcc -g -o dots examples.c constants.c grid.c unicode.c renderer.c main.c -Wall -Werror -lncursesw -lm
clean:
rm dots
J'apprécie chaque commentaire ! Le projet est également disponible sur GitHub :https://github.com/766F6964/DotDotDot
Remarque : Lors du test, assurez-vous d'avoir installé une police de terminal capable d'afficher correctement les caractères braille, sinon elle aura l'air foirée.
Réponses
Code assez sympa.
Petite revue de quelques problèmes annexes.
sizeof *ptr
contre.sizeof type
Code bien utilisé sizeof *ptr
dans 2 cas sur 3.
grid *p_cached_grid = calloc(1, sizeof(*p_grid));
p_cached_grid->buffer = calloc(p_grid->buffer_size, sizeof(int)); // why sizeof(int)
p_render_context = calloc(1, sizeof(*p_render_context));
Recommander de continuer cela
// p_cached_grid->buffer = calloc(p_grid->buffer_size, sizeof(int));
p_cached_grid->buffer = calloc(p_grid->buffer_size, sizeof *(p_cached_grid->buffer));
// or
p_cached_grid->buffer = calloc(p_grid->buffer_size, sizeof p_cached_grid->buffer[0]);
// or other variations.
Mauvaise manipulation des mères porteuses
Bien que cela ne soit pas important pour ce code, il est préférable de détecter les substituts et peut-être de les traiter comme une erreur (forme de caractère de remplacement Unicode).
Algorithme de la ligne de Bresenham
Une mise en œuvre meilleure que d'habitude.
Pour ce code, aucun problème constaté.
En général , le code échoue lorsque x1 - x2
ou y1 - y2
déborde. Il existe des moyens de gérer cela en utilisant unsigned
pour gérer la différence sans recourir à des mathématiques plus larges.
Je posterais un exemple de code, mais mon code de référence n'est pas à jour.
Utiliser les constantes nommées de manière cohérente
Vous avez défini grid_width
et grid_height
, très bien, mais malheureusement vous ne l'utilisez pas de manière cohérente. Dans grid_new()
par exemple, la première ligne peut être remplacée par :
if ((grid_width % group_width != 0) || (grid_height % group_height != 0))
De plus, il est courant d'avoir des constantes globales telles que celles-ci écrites en MAJUSCULES, il est donc plus facile de les distinguer des variables.
Se servir dememset()
Vous avez écrit des boucles dans grid_clear()
et grid_fill()
, mais vous pouvez facilement effectuer cette tâche avec memset(), qui est plus susceptible d'être optimisé. Bien sûr, grid_clear()
peut être réécrit pour faire memset(g->buffer, 0, g->buffer_size * sizeof(*g->buffer))
. Si g->buffer
était un uint8_t *
, vous pouvez également utiliser à l' memset()
intérieur grid_fill()
.
Utiliser uint8_t
pour la grille
Vous n'utilisez que 8 bits pour chaque caractère de la grille, vous pouvez donc le stocker dans un uint8_t
au lieu d'un int
. Cela réduit l'utilisation de la mémoire de la grille par un facteur 4, et permet également memset()
d'être utilisé dans grid_fill()
.
Envisagez de coder en dur la table de recherche
Vous pourriez penser, qu'est-ce que c'est que du blasphème ? ! Tout le monde sait que vous devriez éviter de coder en dur ! Mais dans ce cas, les caractères Unicode Braille sont immuables, et vous gaspillez beaucoup de code pour générer les caractères, et quelques cycles CPU à chaque fois que vous démarrez votre programme, alors que vous pouvez simplement écrire :
wchar_t lookup_table[256] = L"⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇"
L"⠈⠉⠊⠋⠌⠍⠎⠏... "
...
L" ...⣿";
Envisagez d'utiliser ncursesw
Au lieu d'avoir à convertir wchar_t
vous-même une chaîne UTF-8, vous pouvez utiliser la version large de ncurses qui vous permet d'imprimer wchar_t
directement des chaînes. Depuis la version 6 de ncurses, ceci est inclus par défaut et vous permet d'imprimer des chaînes larges que vous pouvez utiliser à la mvaddwstr()place de mvprintw()
.
Pensez à ne pas mettre en cache la grille vous-même
Une grande caractéristique de ncurses est qu'il met en cache ce qui est à l'écran et n'enverra que les caractères et codes de contrôle nécessaires au terminal pour mettre à jour ce qui a vraiment été changé. Vous faites la même chose vous-même, dupliquant ainsi ce que fait ncurses.
Je vois deux façons de se débarrasser de cette inefficacité. Tout d'abord, vous pouvez supprimer complètement vos propres tampons et simplement écrire directement à l'écran avec les fonctions curses. Bien sûr, si vous avez besoin de mettre à jour un seul point dans un caractère Braille, vous devez savoir quel modèle Braille est déjà à l'écran. Vous pouvez relire le contenu de l'écran avec des commandes telles que mvin_wch(). L'inconvénient est que la lecture de caractères individuels peut entraîner de nombreux appels de fonction et que vous devez décoder le caractère braille dans un masque de bits.
Une autre option consiste à conserver un seul tampon et à donner tout le tampon à ncurses à chaque actualisation. Si vous pensez que c'est inefficace, considérez que vous copiez vous-même l'intégralité du tampon dans le tampon mis en cache à chaque actualisation. Si vous suivez cette voie, vous souhaiterez probablement avoir le tampon d'origine pour une manipulation facile des points individuels, et un second tampon de type wchar_t *
que vous mettez à jour en parallèle et que vous pouvez envoyer à ncurses pour imprimer en une seule fois. Remarque, il y a aussi un wmemset()qui pourrait être utile ici.
Je suggérerais d'opter pour la deuxième option. Vous devriez commencer à évaluer votre code afin de pouvoir mesurer objectivement ses performances.