Анимация салюта с использованием Makie

Автор
avatar-igarajaigaraja
Notebook

Анимация салюта с использованием Makie

firework_with_wind.gif

Makie - популярная библиотека визуализации в экосистеме julia. В этом примере будут затронуты такие темы как:

  • Использование цветовых палитр (colormaps/colorschemes)
  • Создание mp4-анимаций (как альтернатива @gif в Plots.jl)
  • особенности построения поточечных графиков (scatter):
    • размер
    • маркер
    • цвет
    • альфа-канал

Модель салюта как система дифференциальных уравнений

Модель салюта в данном примере описывается системой обыкновенных дифференциальных уравнений (ОДУ), которая моделирует движение частиц под воздействием гравитации, сопротивления воздуха и горизонтального ветра. Система включает четыре переменные: координаты $x$ и $y$, а также их производные по времени (скорости $v_x$ и $v_y$). Уравнения имеют вид:

  • $\frac{dx}{dt} = v_x$
  • $\frac{dy}{dt} = v_y$
  • $\frac{dv_x}{dt} = -\frac{(k + k_w)}{m} v_x + \frac{k_w}{m} v_w$
  • $\frac{dv_y}{dt} = -g - \frac{k}{m} v_y$

Здесь $g$ — ускорение свободного падения, $k$ — коэффициент сопротивления воздуха, $m$ — масса частицы, $v_w$ — скорость ветра, $k_w$ — коэффициент сопротивления ветру. Эта система решается численно с помощью пакета DifferentialEquations.jl, что позволяет смоделировать реалистичное поведение частиц салюта после взрыва.


Установка зависимостей и импорт библиотек

Для работы с анимацией и решением дифференциальных уравнений нам понадобятся пакеты DifferentialEquations, CairoMakie и Random. Установим их, если они ещё не установлены, и импортируем.

In [ ]:
import Pkg; Pkg.add(["DifferentialEquations","CairoMakie","Random"])
using DifferentialEquations
using CairoMakie
using Random

# Устанавливаем seed для воспроизводимости
Random.seed!(1945_2025)

Структура программы

Наша программа состоит из нескольких ключевых компонентов: набора салютов, каждый из которых содержит частицы, для каждой частицы вычисляется траектория, а траектория представляет собой набор точек $(x, y)$.

fireworks_diagram.png

Определение модели и параметров

Определяем функцию particle_motion!, которая описывает движение частиц согласно приведённой выше системе ОДУ. Также задаём параметры симуляции: ускорение свободного падения, сопротивление воздуха, массу частицы, скорость ветра и другие.

In [ ]:
# Функция движения частицы с учетом ветра
function particle_motion!(du, u, p, t)
    g, k, m, v_w, k_w = p
    du[1] = u[3]  # dx/dt = v_x
    du[2] = u[4]  # dy/dt = v_y
    du[3] = -(k + k_w) / m * u[3] + k_w * v_w / m  # dv_x/dt = -(k + k_w)/m * v_x + k_w * v_w/m
    du[4] = -g - k / m * u[4]  # dv_y/dt = -g - k/m * v_y
end

# Параметры
g = 9.81      # ускорение свободного падения
k = 0.1       # коэффициент сопротивления воздуха
m = 0.1       # масса частицы
v_w = 0.1     # скорость горизонтального ветра (м/с, положительная — вправо)
k_w = 0.1     # коэффициент сопротивления ветру
n_fireworks = 3  # количество салютов
n_particles_per_firework = 150  # частиц на салют
v0_values = [1:2.5:25;]  # разные начальные скорости
tspan = (0.0, 1.5)  # общий временной интервал
saveat = 0.0125  # фиксированный шаг времени

Генерация салютов и выбор цветовых палитр

Каждый салют характеризуется случайной начальной позицией ((x_0, y_0)), временем взрыва и набором из трёх цветовых палитр. Мы используем Makie.ColorSchemes для создания визуально привлекательного эффекта, где частицы каждого салюта окрашиваются в три разные цветовые схемы.

In [ ]:
# Список палитр
available_palettes = [
    Makie.ColorSchemes.Reds,
    Makie.ColorSchemes.pink,
    Makie.ColorSchemes.solar,
    Makie.ColorSchemes.Blues
]

# Определение салютов
fireworks = []
for i in 1:n_fireworks
    # Случайная начальная позиция
    x0 = rand(-12:12)
    y0 = rand(5:3:15)
    # Случайное время взрыва (между 0.0 и 75% tspan)
    explosion_time = rand(tspan[1]:0.1:0.75*tspan[2])
    # Выбор трёх случайных палитр для салюта
    selected_palettes = shuffle(available_palettes)[1:3]  # выбираем 3 разные палитры
    push!(fireworks, (x0=x0, y0=y0, explosion_time=explosion_time, palettes=selected_palettes))
end

Моделирование траекторий частиц

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

In [ ]:
# Моделирование траекторий для каждого салюта
all_trajectories = []
all_particle_palettes = []
for (idx, fw) in enumerate(fireworks)
    trajectories = []
    particle_palettes = []
    # Разделяем частицы на три группы для трёх палитр
    particles_per_palette = div(n_particles_per_firework, 3)
    for i in 1:n_particles_per_firework
        # Выбор начальной скорости из v0_values
        n_v = length(v0_values)
        v0 = v0_values[ceil(Int, (i - 1) / (n_particles_per_firework / n_v))%n_v+1] + rand(-2:0.25:2)
        θ = 2π * (i - 1) / (n_particles_per_firework / n_v) + rand(-0.1:0.001:0.1)  # случайное отклонение угла
        u0 = [fw.x0, fw.y0, v0 * cos(θ), v0 * sin(θ)]  # начальная позиция и скорость
        prob = ODEProblem(particle_motion!, u0, tspan, (g, k, m, v_w, k_w))
        sol = solve(prob, Tsit5(), saveat=saveat)
        push!(trajectories, [(u[1], u[2]) for u in sol.u])
        # Назначаем палитру в зависимости от группы частицы
        palette_idx = (i - 1) ÷ particles_per_palette + 1
        if palette_idx > 3  # для последнего набора частиц
            palette_idx = 3
        end
        push!(particle_palettes, fw.palettes[palette_idx])
    end
    push!(all_trajectories, trajectories)
    push!(all_particle_palettes, particle_palettes)
end

Создание сцены и добавление звёздного фона

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

In [ ]:
# Визуализация
fig = Figure(backgroundcolor=:black)
ax = Axis(fig[1, 1], aspect=1, limits=(-20, 20, -20, 20),
    backgroundcolor=:black, xgridvisible=false, ygridvisible=false,
    xticksvisible=false, yticksvisible=false,
    xticklabelsvisible=false, yticklabelsvisible=false)

# Добавление звёзд на задний фон
n_stars = 50  # Количество звёзд
x_stars = rand(-20:0.1:20, n_stars)  # Случайные x-координаты
y_stars = rand(-20:0.1:20, n_stars)  # Случайные y-координаты

Создание анимации

Используем функцию record из CairoMakie для создания MP4-анимации. В каждом кадре анимации перерисовываем звёзды и частицы салютов, которые уже взорвались, с учётом эффекта хвоста и индивидуального выцветания.

In [ ]:
# Анимация
total_frames = ceil((tspan[2] - tspan[1]) / saveat) + 1  # общее количество кадров
record(fig, "firework_with_wind.mp4", 1:total_frames-1) do frame_idx
    empty!(ax)
    # Перерисовываем звёзды
    CairoMakie.scatter!(ax, x_stars, y_stars, marker=:star5, color=:yellow, markersize=5, alpha=0.5)
    # Текущее время в анимации
    current_time = (frame_idx - 1) * saveat

    # Проходим по каждому салюту
    for firework_idx in 1:n_fireworks
        fw = fireworks[firework_idx]
        trajectories = all_trajectories[firework_idx]
        particle_palettes = all_particle_palettes[firework_idx]
        # Проверяем, взорвался ли салют
        time_since_explosion = current_time - fw.explosion_time
        if time_since_explosion >= 0
            # Вычисляем индекс времени относительно времени взрыва
            i = Int(round(time_since_explosion / saveat)) + 1
            # Ограничиваем i длиной траектории
            i = min(i, length(trajectories[1]))
            # Индивидуальное выцветание для каждого салюта
            if time_since_explosion >= 0
                fading_start = 0.6  # 60% от времени видимости
                fading_duration = 0.4  # оставшиеся 40% для выцветания
                k_fading = time_since_explosion < fading_start ? 1.0 : max(0.0, 1.0 - (time_since_explosion - fading_start) / fading_duration)
                # Ограничиваем длину хвоста: отображаем последние 24 позиции
                for (traj, palette) in zip(trajectories, particle_palettes)
                    for j in max(1, i - 24):i
                        alpha = k_fading * (0.1 + 0.9 * exp(-0.4 * (i - j)))
                        markersize = 2 + 3 * exp(-0.5 * (i - j))
                        t = i > 1 ? (j - max(1, i - 14)) / (i - max(1, i - 14)) : 1.0
                        color = palette[t]
                        CairoMakie.scatter!(ax, [traj[j]], markersize=markersize, color=color, alpha=alpha)
                    end
                end
            end
        end
    end
end

Выполнение анимации может занять определённое время, по истечение которого будет создан файл "firework_with_wind.mp4"

Заключение

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