Pull to refresh

Разбираем алгоритмы компьютерной графики. Часть 6 — Анимация «Плазма»

Reading time5 min
Views5.9K

Разновидностей алгоритмов генерации "плазм" столько же, сколько, наверное, звезд на небе. Но связывает их вместе принцип плавного формирования перехода цветов.

Для бесшовного формирования цвета очень часто используются тригонометрические функции. Во-первых, потому что они периодические, т.е. через определенный промежуток значения функции повторяются, а во-вторых, они возвращают непрерывные значения, т.е. бесконечно малому приращению аргумента соответствует бесконечно малое приращение функции. Благодаря этому можно используя простые комбинации функций получать плавное возрастание и убывание цветов.

Я попробую рассмотреть один из вариантов, который использует функции синуса и косинуса.

Сначала произведем небольшую оптимизацию. Вычисления тригонометрических функций довольно накладное для процессора занятие. А поскольку таких вычислений на каждом кадре анимации придется делать по нескольку штук, то желательно нужные нам значения рассчитать заранее.

Еще раз напомню, что функции синуса и косинуса – периодические. Их наименьший интервал, через который они начинают повторять значения, это 360 градусов или 2 * Пи.

Поэтому, чтобы запомнить только уникальные значения (все остальные можно вычислить на их основе) достаточно рассчитать кусочек от 0 до 2 * Пи.

Нужный нам кусочек из общего графика функции sin и cos
Нужный нам кусочек из общего графика функции sin и cos

Сделаем по 255 значений для каждой из функций. Для этого создадим два списка по 255 значений в каждом.

sintab = []     # Таблица заранее просчитанных значений синусов
costab = []     # и косинусов.

                # Заполняем начальными значениями таблицы sin и cos
for index in range(0, 255):
    sintab.append(math.sin(index * Pi2 / 255.0))
    costab.append(math.cos(index * Pi2 / 255.0))

Для того чтобы значения синуса и косинуса были в диапазоне от 0 до 1, разделим наш счетчик index на 255. А чтобы внутри массива получился только один период значений функций - умножим аргумент еще на 2 * Пи.

Теперь внутри массивов мы имеем кусочки синусоиды и косинусоиды по одному полному колебанию.

Подобный подход к работе с тригонометрическими функциями, очень часто используется при программировании демо-работ, для оптимизации скорости выполнения программ.

Дополнительно сделаем обратную процедуру, напишем функцию, которая по значению на вход, будет возвращать индекс из нашего массива, который бы соответствовал входному значению, но после получения остатка от деления на длину нашего массива. Звучит очень сложно, но попробую объяснить на пальцах, ну или на картинках:

Пусть у нас есть массив из трех элементов:

Есть число 5, которое мы хотим применить к нашему массиву и понять как бы оно расположилось на нашем массиве, если взять от него остаток от деления по длине нашего массива. Число 5 можно представить, как еще один массив:

Уберем из нашего второго массива длину первого:

Остается два элемента:

И пытаемся их совместить с нашим первоначальном массивом:

Таким образом наше число 5, попадает на ячейку с номером 2.

Вот кратко суть функции по высчитыванию индекса в массиве. В нашем случае будем вычислять индекс для диапазона от 0 до 2 * Пи.

def getInd(num):
    return int(((255.0 * (num % Pi2) / Pi2)))

Функция вернет целое значение в диапазоне от 0 до 255, получая на вход какое-то число и подставляя его в диапазон от 0 до 2 * Пи, который разбит на 255 частей.

А вот дальше уже пойдет почти "магия", поскольку мы начнем получать цветовые характеристики точек по формулам с использованием тригонометрических функций. Почему "магия"? Потому, что объяснить некоторые вещи не до конца получается, во всяком случае у меня. Просто при каких-то сочетаниях функций и арифметических действий - результат будет довольно красивым. И здесь зачастую приходится действовать эмпирическим путем.

Но обо всем по порядку.

Плазма не должна быть статичной! Чтобы этого добиться, нужно на каждой итерации обновления экрана и генерации кадра изображения, добавлять какую-то динамическую составляющую в расчет картинки.

Создадим переменную t (по умолчанию равная 0),  в которой будет накапливаться сдвиг для генерации изображения. Пусть эта переменная, на каждом кадре изменяется на значение delta:

delta = \frac {\pi * 2}{255}

И как только переменная t превысит значение 2 * Пи, то будем сбрасывать ее в 0. Т.е. она по кругу будет проходить через 255 значений, пока не доберется до 2 * Пи.

Приступаем к генерации изображения, сначала размещу часть кода, затем поясню:

    for i in range(0, MY):
        y = i / MY
        for j in range(0, MX):
            x = j / MX
                            # Вычисляем цветовые коэффициенты и затем сами цветовые составляющие
            a1 = 8.0 * sintab[getInd(x + t)]
            a2 = 7.0 * costab[getInd(x + t)]
            a3 = 6.0 * sintab[getInd(x + t)]
            r = int(100 * abs(sintab[getInd(a1 * x + t)] + costab[getInd(a1 * y - t)]))
            g = int(100 * abs(costab[getInd(a2 * x - t)] + sintab[getInd(a2 * y + t)]))
            b = int(100 * abs(sintab[getInd(a3 * x + t)] + costab[getInd(a3 * y - t)]))
            pygame.draw.rect(screen, (r, g, b), (i*scale, j*scale, scale, scale))

Итоговая анимация плазмы будет вот такая:

Цвета на gif немного искажены, в True color у меня не удалось ее сделать
Цвета на gif немного искажены, в True color у меня не удалось ее сделать

У нас есть два вложенных цикла. Первый проходит по строкам изображения, второй по колонкам. Пересечение этих циклов - это обрабатываемая точка на экране с координатой i,j. Формируем для каждой из этих координат, ее аналог, но выравненный по диапазону от 0 до 1. Делается это просто делением на ширину первоначального диапазона, для y, это i / MY, для x, это j / MX.

Теперь добавим "хаоса" в генерацию изображения. Сделаем три коэффициента для трех составляющих цвета: a1 для красного (red), a2 для зеленого (green), a3 для синего (blue).

Вычисляем их по следующему принципу: получаем значение индекса для координаты x + t и получаем значение синуса или косинуса для этого индекса. Полученное значение умножаем на произвольный коэффициент. На самом деле это число поможет нам изменять количество волн на экране.

Например:

a1 * 18
a1 * 18
a2 * 17
a2 * 17
a3 * 16
a3 * 16
Все три коэффициента умноженные на значения перечисленные выше, одновременно
Все три коэффициента умноженные на значения перечисленные выше, одновременно

Как видите, здесь можно играть коэффициентами как угодно, результат аналитически просчитать заранее сложно.

Но это мы пока вычисляли коэффициенты, теперь используем их при вычислении цветов r, g, b.

Мы опять применим сочетания синусов и косинусов взятые из таблиц, но с учетом добавления туда коэффициентов a. Что именно использовать cos или sin не сильно принципиально, просто их смещение в разных сочетаниях дает интересные результаты.

Умножение на 100 здесь служит для того, чтобы дробное значение цвета привести к диапазону от 0 до 255, в котором его обрабатывает библиотека PyGame.

Советую "поиграться"  коэффициентами, расположением функций sin и cos. Получаются очень примечательные картины.

Итоговый код:

import pygame
import math

MX = MY = 256           # Размер массива с плазмой

Pi2 = math.pi * 2.0     # Просто константа 2 * Пи для простоты и скорости

scale = 4               # Масштаб точек для вывода на экран

SX = MX * scale  # Размер экрана исходя из размера плазмы и ее масштаба
SY = MY * scale

pygame.init()
screen = pygame.display.set_mode((SX, SY))
running = True

sintab = []     # Таблица заранее просчитанных значений синусов
costab = []     # и косинусов.

                # Заполняем начальными значениями таблицы sin и cos
for index in range(0, 255):
    sintab.append(math.sin(index * Pi2 / 255.0))
    costab.append(math.cos(index * Pi2 / 255.0))

t = 0                   # Сдвиг для получения анимации
delta = Pi2 / 255.0     # Шаг нашего сдвига, на каждом кадре анимации

# -------------------------------------------------------------------------------------------------------
# Возвращаем индекс в диапазоне от 0 до 255, которое занимало бы число num, если бы его располагали от
# 0 до (2 * Пи)
# -------------------------------------------------------------------------------------------------------
def getInd(num):
    return int(((255.0 * (num % Pi2) / Pi2)))

# -------------------------------------------------------------------------------------------------------
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    for i in range(0, MY):
        y = i / MY
        for j in range(0, MX):
            x = j / MX
                            # Вычисляем цветовые коэффициенты и затем сами цветовые составляющие
            a1 = 8.0 * sintab[getInd(x + t)]
            a2 = 7.0 * costab[getInd(x + t)]
            a3 = 6.0 * sintab[getInd(x + t)]
            r = int(100 * abs(sintab[getInd(a1 * x + t)] + costab[getInd(a1 * y - t)]))
            g = int(100 * abs(costab[getInd(a2 * x - t)] + sintab[getInd(a2 * y + t)]))
            b = int(100 * abs(sintab[getInd(a3 * x + t)] + costab[getInd(a3 * y - t)]))
            pygame.draw.rect(screen, (r, g, b), (i*scale, j*scale, scale, scale))

    t += delta              # "Двигаем" анимацию
    if t >= Pi2:
        t = 0

    pygame.display.flip()

pygame.quit()

Ссылка на предыдущую статью: Разбираем алгоритмы компьютерной графики. Часть 5 – Анимация «Shade Bobs»

В следующий раз рассмотрим алгоритм искажений на плоскости, например эффект линзы.

Tags:
Hubs:
Total votes 16: ↑16 and ↓0+15
Comments7

Articles