[CODE] Сглаживание (антиалиасинг) на Spectrum: реалистичные методы vs. восприятие

TapeLoader

Бог форума
Пользователь
Сообщения
27
Счётчик реакций
34
Очки
740
Коллеги, приветствую всех, кто бьётся над тем, чтобы впихнуть в знакомые нам 256x192 и два цвета в знакомые 8x8 больше, чем кажется возможным.

Хочу поднять, возможно, философский, но очень практический вопрос, который возникает при отрисовке плавных линий, кривых и диагоналей. Речь о сглаживании (антиалиасинге).

На современных системах это решается на уровне железа или сложных шейдеров. Нашем же случае — это чистой воды искусство обмана зрения в условиях жесточайших ограничений (2 цвета в атрибуте, жёсткая сетка знакомест).

Давайте разберем тему по косточкам:

1. Классические приёмы и «спектрум-джиттер». Самый известный метод — использование двух соседних атрибутов для создания третьего, промежуточного цвета за счёт мерцания (dithering). Но это работает только в статике или очень медленной анимации. Какие ещё классические алгоритмы (например, упрощённый алгоритм Ву для линии) реально применимы на Z80? Есть ли у кого готовая, вылизанная подпрограмма рисования сглаженной линии?
2. Цвет vs. Яркость. Из-за особенностей атрибутов, иногда эффективнее сглаживать не переход между цветами (ink/paper), а между яркостями (BRIGHT). Была ли у вас практика, когда сглаженная серая шкала из чёрного, темно-серого (черный+bright) и светло-серого (белый+bright) выглядела убедительнее, чем попытки смешивать, например, синий и красный?
3. Контекст — всё. Эффективность сглаживания сильно зависит от фона.
· На однородном фоне можно позволить себе растянуть переход на несколько знакомест.
· На пестром или текстурном фоне эти попытки часто разваливаются, создавая лишь «грязь».
· Как вы решаете, когда стоит заморачиваться со сглаживанием, а когда честные «лесенки» выглядят стилистически уместнее?
4. Аппаратные трюки и полупиксели. Здесь вопрос к гуру. Существуют ли (или возможны ли в принципе) методы псевдо-сглаживания с использованием аппаратных особенностей? Например:
· Микропоскроллирование кадра или области для размытия (крайне сомнительно по производительности).
· Использование интерференции ULA+ для создания промежуточных оттенков на границах (как в некоторых демках).
· Быстрое переключение атрибутов (вроде FLI) в одной строке для более сложных градиентов.
5. Практика и примеры. Это самый важный блок. Давайте не только говорить, но и показывать.
· Есть ли у вас наработки — скриншоты, где ваш метод сглаживания выглядит победоносно?
· А может, наоборот, есть пример, где попытка всё сгладить привела к худшему результату, чем грубая пиксельная графика? Почему так вышло?
· Поделитесь, если не жалко, кодом ключевых процедур. Интересно сравнить компромисс между качеством картинки, скоростью отрисовки и размером кода.

Итог и цель темы:

Попробуем отойти от вопроса «можно ли» (можно всё, было бы время и память) к вопросу «как это сделать целесообразно». Собрать в одной теме не просто теорию, а практические рецепты, готовые процедуры и наглядные сравнения, которые помогут принимать взвешенные решения при отрисовке следующей игры или демки.

Жду ваших примеров, кода и мнений!
 
Начнем...
Внесу свою лепту

Dither Fill для плавных градиентов

Этот код не рисует линии, а заливает область узором дизеринга, создавая иллюзию плавного перехода между двумя цветами в одном атрибутном знакоместе (8x8 пикселей). Это базовый, но крайне полезный приём.

Алгоритм: Используется простая матрица дизеринга 2x2 для смешивания двух цветов (например, INK и PAPER) в четырёх возможных паттернах внутри знакоместа.
Assembler Z80:
; -------------------------------------------------------------
; Подпрограмма DITHER_FILL
; Рисует блок знакомест с плавным градиентом методом дизеринга.
;
; Входные параметры в регистрах:
;   B  - высота области в знакоместах (обычно 1-24)
;   C  - ширина области в знакоместах (обычно 1-32)
;   HL - адрес в экранной области (память изображения)
;   D  - байт паттерна для цвета 1 (например, %11111111 для INK)
;   E  - байт паттерна для цвета 2 (например, %00000000 для PAPER)
;
; Использует: AF, BC, DE, HL
; -------------------------------------------------------------

DITHER_FILL:
    PUSH HL                 ; Сохраняем начальный адрес строки

    ; Создадим в стеке 4 паттерна строк для матрицы 2x2
    ; Паттерны для двух цветов (D и E) в шахматном порядке:
    ; Строка 0: D E D E ...
    ; Строка 1: E D E D ...
    ; Строка 2: D E D E ... (повтор строки 0)
    ; Строка 3: E D E D ... (повтор строки 1)

    LD A, D                 ; A = паттерн цвета 1 (INK)
    LD (PATTERN_0), A       ; Сохраняем для чётной строки, чётного столбца
    LD (PATTERN_2), A       ; И для нечётной строки, нечётного столбца

    LD A, E                 ; A = паттерн цвета 2 (PAPER)
    LD (PATTERN_1), A       ; Для чётной строки, нечётного столбца
    LD (PATTERN_3), A       ; Для нечётной строки, чётного столбца

    ; Основной цикл по строкам знакомест
ROW_LOOP:
    PUSH BC                 ; Сохраняем счётчики (B=высота, C=ширина)
    PUSH HL                 ; Сохраняем адрес начала строки в экранной памяти

    ; Определим, чётная или нечётная строка (по стартовому адресу)?
    ; Простой способ: проверим младший бит номера строки (0-23).
    ; Для этого можно использовать регистр B или отдельный счётчик.
    ; Здесь для простоты используем флаг из внешнего счётчика.
    ; Предположим, что в IXH у нас номер текущей строки (0...B-1).

    LD B, C                 ; B = ширина области (счётчик для COL_LOOP)

COL_LOOP:
    ; Загрузим паттерн для текущей позиции 2x2.
    ; Для этого нужно знать: чётная/нечётная строка и чётный/нечётный столбец.
    BIT 0, (IX+0)           ; Проверяем младший бит номера строки (0=чётная, 1=нечётная)
    JR Z, EVEN_ROW

ODD_ROW:                    ; Нечётная строка матрицы
    BIT 0, B                ; Проверяем младший бит счётчика столбца (инвертировано)
    LD A, (PATTERN_3)       ; Готовим паттерн для нечётной строки, чётного столбца
    JR Z, DRAW_PATTERN
    LD A, (PATTERN_2)       ; Паттерн для нечётной строки, нечётного столбца
    JR DRAW_PATTERN

EVEN_ROW:                   ; Чётная строка матрицы
    BIT 0, B                ; Проверяем младший бит счётчика столбца
    LD A, (PATTERN_1)       ; Паттерн для чётной строки, нечётного столбца
    JR Z, DRAW_PATTERN
    LD A, (PATTERN_0)       ; Паттерн для чётной строки, чётного столбца

DRAW_PATTERN:
    LD (HL), A              ; Записываем паттерн в экранную память
    INC HL                  ; Переходим к следующему знакоместу в строке

    DJNZ COL_LOOP           ; Повторяем для всей строки

    ; Восстанавливаем адрес начала строки и переходим к следующей строке экрана
    POP HL                  ; Восстанавливаем начало строки
    CALL NEXT_SCREEN_LINE   ; HL = адрес следующей строки экрана (стандартная процедура)
    POP BC                  ; Восстанавливаем счётчики (B=высота, C=ширина)
    DJNZ ROW_LOOP           ; Повторяем для всех строк

    RET

; --- Данные ---
PATTERN_0: DEFB 0           ; Паттерн для чётной строки, чётного столбца
PATTERN_1: DEFB 0           ; Для чётной строки, нечётного столбца
PATTERN_2: DEFB 0           ; Для нечётной строки, нечётного столбца
PATTERN_3: DEFB 0           ; Для нечётной строки, чётного столбца

; -------------------------------------------------------------
; Вспомогательная процедура: переход на следующую строку экрана
; Вход: HL - текущий адрес экрана
; Выход: HL - адрес следующей строки
; -------------------------------------------------------------
NEXT_SCREEN_LINE:
    INC H                   ; Переход к следующей строке внутри символа
    LD A, H
    AND %00000111           ; Проверим, не вышли ли за пределы 8 строк символа?
    RET NZ                  ; Если нет, возвращаемся
    LD A, L
    ADD A, 32               ; Переходим к следующему знакоместу
    LD L, A
    RET C                   ; Если был переход через 256 байт
    LD A, H
    SUB 8                   ; Корректируем старший байт
    LD H, A
    RET
 
Последний раз редактировалось модератором:
Косяк в коде: процедура вылетает на экран
Assembler Z80:
    ; Восстанавливаем адрес начала строки и переходим к следующей строке экрана
    POP HL                  ; Восстанавливаем начало строки
    CALL NEXT_SCREEN_LINE   ; HL = адрес следующей строки экрана (стандартная процедура)
    POP BC                  ; Восстанавливаем счётчики (B=высота, C=ширина)
    DJNZ ROW_LOOP           ; Повторяем для всех строк

    pop hl ;ВОССТАНОВИТЬ HL
    RET
Хорошо бы пример использования со значениями регистров D и E.

@admin, можно ли добавить к полю "код" опцию "Выделить всё"? Было бы удобно.
 
Последний раз редактировалось модератором:
Да, сделаю возможность копирования кода
 
Назад
Вверх