Кодобред
18 марта 2011

Система частиц на PHP в 3D

С детства есть у меня заветная программерская мечта — написать физический движок. Как и положено мечте, я к ней никогда близко не подходил. Но однажды выдалась пара ночей, когда я должен был дежурить в помещении, и у меня с собой был ноутбук.
В общем, взялся я моделировать движение и столкновение частиц на PHP. Почему на PHP? Потому что это единственный язык, на котором я могу свободно излагать свои программерские мысли. В общем, сначала координаты выводились в консоли, потом стал делать графические снимки. Немедленно появилась мысль генерировать анимацию…
image


Погуглив, я нашел древненький класс GIFEncoder v.2, который позволял собирать кадры в фильм! Меня это очень воодушевило, и я продолжил эксперименты.
Самой первой, сложной и главной проблемой стало сложение импульсов. Дело в том, что при простом перемещении частицы на X, например, 1, и такой же Y, разбросанные во все 360 сторон частицы не хотели образовывать круг — они упорно выстраивались в ромб. Я плохо учил алгебру, но мне было интересно импровизировать, экспериментируя с коэффициентами.
Второй проблемой стала проекция разбросанных в 3D точек на плоскость. Так как геометрию я учил не лучше алгебры, меня выручили импровизированные коэффициенты: с правильными подобрать более-менее правдоподобную проекцию было бы не просто. Так или иначе, энтузиазма хватило до создания скрипта, генерирующего вышеприведенный ролик. А механизм лучше всего объяснить прямо в коде. Вы должны свободно читать и понимать PHP:
<?php
// В зависимости от задачи, может выполняться и пару часов =)
set_time_limit(0);
// засекаем время
$start = microtime(1);
// GIFEncoder вот отсюда:
// http://saintist.ru/2009/05/12/316/
include('gif_animate.php');

// Пространство для дивжения частиц
class space {
    // хранилище частиц
    public $points = array();
    // вместо матрицы - свойства
    // с объектным интерфейсом матрица не нужна
    private $x;
    private $y;
    private $z;
    // Шаги. См. метод step.
    public $steps = 0;

    public function __construct($x, $y, $z) {
        $this -> x = $x;
        $this -> y = $y;
        $this -> z = $z;
    }

    // -да будет частица...
    public function addPoint($point, $x, $y, $z, $fx = 0, $fy = 0, $fz = 0) {
        array_push($this -> points, array($point, $x, $y, $z));
        $point -> addP($fx, $fy, $fz);
    }

    // Шаг. Ключевой элемент, в этом методе двигаются частицы.
    // В какой-то версии скрипта, они у меня сталкивались.
    // Чтобы они (на случай - если столкновения включены) не прыгали сквозь друг друга,
    // шаги могут быть дробными. Число, которым выражен шаг = скорости самой медленной
    // частицы, но только если она < 1.
    // Изначально, 1 шаг = перемещение частицы со скоростью 1 на 1 пиксель за шаг.
    // Поэтому дробное число можно считать частью шага.
    public function step() {
        // вот этим числом выражен шаг
        $t = 1;
        // Матрица занятых координат.
        // Использовалась для поиска столкновений, далее не используется.
        $busy = array();
        // В этом цикле чистим space от вылетевших за пределы поля частиц
        // и вычисляем величину шага
        foreach ($this -> points as $n => &$point) {
            $x = $point[1];
            $y = $point[2];
            $z = $point[3];
            // Если частица за пределами поля - удаляем её, и пропускаем итерацию
            if (!($x >= 0 && $x < $this->x && $y >= 0 && $y < $this->y && $z >= 0 && $z < $this->z)) {
                unset($this->points[$n]);
                continue;
            }
            // Сила импульсов частицы в трех измерениях
            $f = abs($point[0]->p['fx']) + abs($point[0]->p['fy']) + abs($point[0]->p['fz']);
            // Если она вообще движется, и при том меньше 1 и текущей $t
            if ($f > 0 && 1 / $f < $t) {
                // то меняем числовое выражение шага
                $t = 1 / $f;
            }
        }
        // Главный цикл. Перемещение частиц.
        foreach ($this -> points as $n => &$point) {

            $xyz = abs($point[0]->p['fx']) + abs($point[0]->p['fy']) + abs($point[0]->p['fz']);
            // Экспериментально вывел этот коэффициент для сложения импульсов
            $hypo = pow(abs($point[0]->p['fx']), 2) + pow(abs($point[0]->p['fy']), 2) + pow(abs($point[0]->p['fz']), 2);
            // Переменные вида $move_x - это растояние, на которое должна быть перемещена частица.
            // Ниже они вычисляются.
            // Эта еденица для X и Y равна пикселю, для Z - визуально заметна только
            // при наличии импульса в X или Y, естественно.
            if ($point[0]->p['fx'] != 0 && $xyz > 0) {
                if ($point[0]->p['fx'] < 0) {
                    $fxmin = 1;
                }
                $move_x = sqrt($hypo) * ($point[0]->p['fx'] / $xyz);
                if (isset($fxmin)) $move_x = $move_x * -1;
            } else {
                $move_x = 0;
            }

            if ($point[0]->p['fy'] != 0 && $xyz > 0) {
                if ($point[0]->p['fy'] < 0) {
                    $fymin = 1;
                }
                $move_y = sqrt($hypo) * ($point[0]->p['fy'] / $xyz);
                if (isset($fymin)) $move_y = $move_y * -1;
            } else {
                $move_y = 0;
            }

            if ($point[0]->p['fz'] != 0 && $xyz > 0) {
                if ($point[0]->p['fz'] < 0) {
                    $fzmin = 1;
                }
                $move_z = sqrt($hypo) * ($point[0]->p['fz'] / $xyz);
                if (isset($fzmin)) $move_z = $move_z * -1;
            } else {
                $move_z = 0;
            }
            // умножаем на длину шага и меняем координаты
            $point[1] += $move_x*$t;
            $point[2] += $move_y*$t;
            $point[3] += $move_z*$t;
        }
        $this -> steps+=$t;
    }
    // Метод делает снимок space в GIF
    public function shot() {
        $r = imagecreatetruecolor($this -> x+1, $this -> y+1);
        // думаю, все очевидно
        foreach ($this -> points as &$point) {
            $x = $point[1];
            $y = $point[2];
            $z = $point[3];
            if ($x >= 0 && $x < $this->x && $y >= 0 && $y < $this->y && $z >= 0 && $z < $this -> z) {
                // оказывается, не всё. Нам надо, что частицы, которые дальше по Z
                // двигались медленнее тех, что ближе к нам - по Z.
                $ox = $this->x / 2;
                $oy = $this->y / 2;
                $oz = $this->z / 2;
                $xf = $x - $ox;
                $yf = $y - $oy;
                $zf = $z;
                $xr = $x - $xf / ($this->z/($this->z - $z));
                $yr = $y - $yf / ($this->z/($this->z - $z));
                imageline($r, $xr, $yr, $xr, $yr, $point[0]->color);
            }
        }
        ob_start();
        imagegif($r);
        return ob_get_clean();
    }
}
class Point {
    public $p = array('fx' => 0, 'fy' => 0, 'fz' => 0);
    public $color;
    // Конструктор генерирует случайный цвет
    public function __construct() {
        $red = dechex(rand(1, 255));
        if (strlen($red) < 2) $red = '0'.$red;
        $green = dechex(rand(1, 255));
        if (strlen($green) < 2) $green = '0'.$green;
        $blue = dechex(rand(1, 255));
        if (strlen($blue) < 2) $blue = '0'.$blue;
        $this->color = hexdec("0x$red$green$blue");
    }
    public function addP($fx = 0, $fy = 0, $fz = 0) {
        $this->p['fx'] += $fx;
        $this->p['fy'] += $fy;
        $this->p['fz'] += $fz;
    }
}
$gifs = array(); // GD-шные GIF-ы, которые потом будут объединены
$space = new space(399, 399, 399, false, true);
// Рандомно генерируем импульсы для частиц.
// Чтобы они летели с разной скоростью, и в разных направлениях
$rands = range(-10, 10, 0.5);
for ($i=0; $i<1000; $i++) {
    // ... и стало так
    $space->addPoint(new Point, 200, 200, 200, $rands[array_rand($rands)], $rands[array_rand($rands)], $rands[array_rand($rands)]);
}
// запускаем на 100  ц е л ы х  шагов...
while($space->steps < 100) {
    $nshot = $space -> steps;
    $space -> step();
    if ((int)$nshot < (int)$space -> steps) {
        array_push($gifs, $space -> shot());
        echo 'Step #',$space -> steps,"\r\n";
    }
}
// собираем кадры в фильм...
$gif = new GIFEncoder($gifs, 0, 0, 0, 0, 0, 0, 'bin');

file_put_contents('c:\anim.gif', $gif -> GetAnimation());
echo "\r\nTime: ",substr(microtime(1) - $start, 0, 4);

// приятного просмотра :-)

Принцип вам понятен. Одним из первых роликов был этот:
image
Потом в 3D, этот:
image
Бред? Возможно. Но не бойтесь реализовывать самые бредовые идеи. Это — лучший способ научиться думать на языке программирования.
+86
3,6k 61
Комментарии 83
Похожие публикации
Популярное за сутки