터미널 용 텍스트 기반 렌더링 / 애니메이션 엔진
이 프로젝트는 점자 유니 코드 문자를 사용하여 터미널에 그림을 그릴 수있는 인기있는 드로 유 프로젝트에서 큰 영감을 받았습니다.
일반 ASCII 문자에 비해 점자 문자로 그리는 것의 장점은 간단합니다. 모든 "점자 문자"는 2 x 4 = 8
가능한 스팟 으로 구성되어 있기 때문에 문자 256
별로 그릴 수 있는 가능한 변형 이 있습니다. 이러한 점자 패턴을 사용하면 훨씬 더 미세하고 부드러운 그림을 그릴 수 있습니다.
내 구현에는 ncurses 라이브러리를 사용하여 화면에 그려지는 모든 것을 애니메이션화 할 수있는 렌더링 엔진도 함께 제공됩니다. 내 구현은 다음과 같이 매우 성능이 뛰어난 것을 목표로합니다.
- 최소한의 메모리 사용.
- 아주 좋은 런타임을 가지고 있습니다.
여전히 사용하기 쉽습니다.
다음은이 라이브러리로 수행 할 수있는 작업을 보여주는 몇 가지 예입니다. 이러한 예는 다음에서도 찾을 수 있습니다 examples.c
.
저는 이미 매우 간결한 방식으로 데이터를 저장하고 액세스하는 그리드 구조의 구현에 상당히 만족합니다. 렌더링 구조의 성능을 더 향상시킬 수 있는지 궁금합니다. 이미 이전 프레임에서 변경된 사항 만 렌더링하려고하는데 더 최적화 할 수 있을까요?
또한 내 구현이 C 스타일 코딩 지침을 잘 사용하는지 확실하지 않습니다. 또한 라이브러리가 사용자 친화적인지 확인하고 싶습니다. 따라서 사용자로서이 라이브러리의 API에서 기대할 수있는 기능과 현재 상태에서 사용할 때 놓친 부분이 있으면 알려주세요.
grid.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);
}
grid.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
renderer.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;
}
}
renderer.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
constants.c
const int group_height = 4;
const int group_width = 2;
const int render_delay_ms = 10;
constants.h
#ifndef CONSTANTS_H
#define CONSTANTS_H
extern const int group_height;
extern const int group_width;
extern const int render_delay_ms;
#endif
examples.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);
}
examples.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
main.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;
}
마지막으로 모든 것을 컴파일하는 Makefile :
prog:
gcc -g -o dots examples.c constants.c grid.c unicode.c renderer.c main.c -Wall -Werror -lncursesw -lm
clean:
rm dots
모든 피드백에 감사드립니다! 이 프로젝트는 GitHub에서도 사용할 수 있습니다.https://github.com/766F6964/DotDotDot
참고 :이를 테스트 할 때 점자 문자를 올바르게 표시 할 수있는 터미널 글꼴이 설치되어 있는지 확인하십시오. 그렇지 않으면 엉망이되어 보입니다.
답변
꽤 멋진 코드.
몇 가지 부수적 인 문제에 대한 약간의 검토.
sizeof *ptr
대 sizeof type
코드 sizeof *ptr
는 3 가지 중 2 가지 경우에 잘 사용 됩니다.
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));
계속하는 것이 좋습니다.
// 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.
대리자에 대한 부적절한 처리
이 코드 에는 중요하지 않지만 서로 게이트를 감지하고 오류로 처리하는 것이 더 좋습니다 (유니 코드 대체 문자 형식).
Bresenham의 선 알고리즘
일반적인 구현보다 낫습니다.
위해 이 코드, 더 문제는 볼 수 없습니다.
에서 일반 코드 때 실패 x1 - x2
또는 y1 - y2
오버 플로우. unsigned
더 넓은 수학에 의존하지 않고 차이를 처리하기 위해 사용하여 이것을 처리하는 방법이 있습니다 .
샘플 코드를 게시하고 싶지만 내 참조 코드가 최신 상태가 아닙니다.
일관되게 명명 된 상수 사용
당신은 정의 grid_width
하고 grid_height
매우 훌륭하지만 불행히도 일관되게 사용하고 있지 않습니다. 예 grid_new()
를 들어, 첫 번째 줄은 다음과 같이 바꿀 수 있습니다.
if ((grid_width % group_width != 0) || (grid_height % group_height != 0))
또한 이러한 전역 상수를 모두 대문자로 작성하는 것이 일반적이므로 변수와 구별하기가 더 쉽습니다.
활용 memset()
grid_clear()
및에 루프를 작성 grid_fill()
했지만을 사용하여이 작업을 쉽게 수행 할 수 memset()있으며 최적화 가능성이 더 높습니다. 확실히 grid_clear()
할 수 있도록 다시 작성할 수 있습니다 memset(g->buffer, 0, g->buffer_size * sizeof(*g->buffer))
. 경우 g->buffer
을했다 uint8_t *
, 당신은 또한 사용할 수있는 memset()
내부 grid_fill()
.
사용 uint8_t
그리드에 대한
당신은 그리드의 각 문자에 대해 8 비트를 사용하고, 그래서 당신은 그것을 저장할 수있는 uint8_t
대신의 int
. 이렇게하면 그리드의 메모리 사용량이 4 배 감소 memset()
하고 grid_fill()
.
조회 테이블 하드 코딩 고려
이게 무슨 신성 모독이라고 생각할 수 있습니까?! 누구나 하드 코딩을 피해야한다는 것을 알고 있습니다! 그러나이 경우 유니 코드 점자 문자는 고정되어 있으며 문자를 생성하는 데 많은 코드를 낭비하고 프로그램을 시작할 때마다 일부 CPU주기를 다음과 같이 작성할 수 있습니다.
wchar_t lookup_table[256] = L"⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇"
L"⠈⠉⠊⠋⠌⠍⠎⠏... "
...
L" ...⣿";
ncursesw 사용 고려
wchar_t
UTF-8 문자열로 직접 변환하는 대신 wchar_t
문자열을 직접 인쇄 할 수있는 광범위한 버전의 ncurses를 사용할 수 있습니다. ncurses 버전 6부터 이것은 기본적으로 포함되어 있으며 mvaddwstr()대신 사용할 수있는 넓은 문자열을 인쇄 할 수 있습니다 mvprintw()
.
그리드를 직접 캐싱하지 않는 것이 좋습니다.
ncurses의 큰 특징은 화면에있는 내용을 캐시하고 실제로 변경된 내용을 업데이트하기 위해 필요한 문자와 제어 코드 만 터미널에 전송한다는 것입니다. 당신은 스스로 똑같이하고 있으므로 ncurses가하는 일을 복제합니다.
이 비 효율성을 제거하는 두 가지 방법이 있습니다. 첫째, 자신의 버퍼를 모두 없애고 curses 함수를 사용하여 화면에 직접 쓸 수 있습니다. 물론 점자 문자의 단일 점을 업데이트해야하는 경우 화면에 이미 어떤 점자 패턴이 있는지 알아야합니다. 와 같은 명령을 사용하여 화면의 내용을 다시 읽을 수 있습니다 mvin_wch(). 단점은 개별 문자를 다시 읽으면 많은 함수 호출이 발생할 수 있으며 점자 문자를 비트 마스크로 다시 디코딩해야한다는 것입니다.
또 다른 옵션은 단일 버퍼를 유지하고 새로 고칠 때마다 전체 버퍼를 ncurses에 제공하는 것입니다. 이것이 비효율적이라고 생각되면 새로 고칠 때마다 전체 버퍼를 캐시 된 버퍼에 복사했다고 생각하십시오. 그래도 이런 식으로 가면 개별 도트를 쉽게 조작 할 수있는 원래 버퍼와 wchar_t *
병렬로 업데이트 하는 두 번째 버퍼 유형 을 원할 것이며 한 번에 인쇄하기 위해 ncurses로 보낼 수 있습니다. 참고 또한이 wmemset()여기에 도움이 될 수있다.
두 번째 옵션을 선택하는 것이 좋습니다. 코드의 성능을 객관적으로 측정 할 수 있도록 코드 벤치마킹을 시작해야합니다.