Как сгенерировать изображение 3D кубиков (игральных костей) на стороне сервера?

Необходимо на стороне сервера генерировать изображения с игральными костями, "выпавшими" рандомно (угол поворота, угол проекции, значения на гранях -- все случайное).

Рассчитывать координаты точек в проекции я, к сожалению, не умею, реализовать самостоятельно с нуля не выходит, поэтому ищу либы, которые подобное делать умеют.

Очень хотелось бы реализовать на PHP, но рассматриваю все варианты.

Пока найдено лишь одно решение: http://www.fmwconcepts.com/imagemagick/3Dbox/ -- но хочется альтернатив.

Буду признателен за любые подсказки и наводки.


Ответы (1 шт):

Автор решения: Laukhin Andrey

Все же не смог устоять перед этой задачей. Делюсь свои решением.

Класс сцены

define('M_2PI', 2 * M_PI);

class Scene
{
    public $ambient = 0; // фоновый свет
    public $vlight = [0,0,1]; // направление света

    public $rproj = 0.15; // Коэффициент перспективы
    
    private $cx = 0; // Смещение области просмотра
    private $cy = 0;
    
    private $cubes = []; // Кубики

    public function Scene($width, $height)
    {
        $this->cx = $width / 2;
        $this->cy = $height / 2;
    }
    
    // Конвейер
    public function RenderPoints(& $points, $tx, $ty, $tz, $ax, $ay, $az, $scale)
    {
        for($i = 0; $i < sizeof($points); $i++)
        {
            // Вращение-извращение
            $points[$i] = rotx($points[$i], $ax);
            $points[$i] = roty($points[$i], $ay);
            $points[$i] = rotz($points[$i], $az);

            // Смещение
            $points[$i][0] += $tx;
            $points[$i][1] += $ty;
            $points[$i][2] += $tz;

            // Камеру по удобнее поставим
            $points[$i] = rotx($points[$i], M_PI/4);
            $points[$i][2] += 2;
                
            // Одноточечная перспективная проекция
            $z = $points[$i][2];
            $points[$i][0] /= $this->rproj * $z + 1;
            $points[$i][1] /= $this->rproj * $z + 1;

            // Масштабирование
            $points[$i][0] *= $scale;
            $points[$i][1] *= $scale;
            $points[$i][2] *= $scale;

            // Смещение области просмотра
            $points[$i][0] += $this->cx;
            $points[$i][1] += $this->cy;
        }
    }
    
    private function DrawCube($gd, $cube)
    {
        $points =& $cube->points;
        $faces =& $cube->faces;
        $normals =& $cube->normals;
        $color =& $cube->color;
        
        for($i = 0; $i < sizeof($faces); $i++)
        {
            // Тест глубины
            if($normals[$i][2] > 0) continue;
            
            $face = $faces[$i];
            
            // Освещение грани по нормали
            $diffuse = max(-dotproduct($normals[$i], $this->vlight), 0);
            $ck = $this->ambient + $diffuse;
            
            $c = rgb(lim255($ck * $color[0]), lim255($ck * $color[1]), lim255($ck * $color[2]));
            
            $p1 = $points[$face[0]];
            $p2 = $points[$face[1]];
            $p3 = $points[$face[2]];
            $p4 = $points[$face[3]];

            $poly = [$p1[0],$p1[1],$p2[0],$p2[1],$p3[0],$p3[1],$p4[0],$p4[1]];
            imagefilledpolygon($gd, $poly, 4, $c);
            
            // Граница
            if($cube->drawedges)
            {
                imagepolygon($gd, $poly, 4, $cube->color_edg);
                imagepolygon($gd, $poly, 4, $cube->color_edg); // так задумано
            }
            else imagepolygon($gd, $poly, 4, $c); // antialias
            
            // Точки
            foreach($cube->facedots[$i] as &$dots)
            {
                $poly = poly2gd($dots);
                imagefilledpolygon($gd, $poly, sizeof($dots), $cube->dotcolor);
                imagepolygon($gd, $poly, sizeof($dots), $cube->dotcolor);
            }
        }
    }
    
    public function Add($cube)
    {
        $this->cubes[] = $cube;
    }

    public function Render()
    {
        normalize($this->vlight);
        
        foreach($this->cubes as $cube)
        {
            $this->RenderPoints($cube->points,
                $cube->x, $cube->y, $cube->z,
                $cube->ax, $cube->ay, $cube->az,
                $cube->scale);
            
            foreach($cube->facedots as &$dotset)
                foreach($dotset as &$dots)
                    $this->RenderPoints($dots,
                        $cube->x, $cube->y, $cube->z,
                        $cube->ax, $cube->ay, $cube->az,
                        $cube->scale);
            
            $cube->CalculateNormals();
        }
    }

    public function Draw($gd)
    {
        foreach($this->cubes as $cube)
            $this->DrawCube($gd, $cube);
    }
}

Класс, описывающий структуру игрового кубика

class GameCube3D
{
    public $points = [
        [-1, 1, -1], [1, 1, -1], [1,-1,-1], [-1,-1,-1],
        [-1,1,1], [1,1,1], [1,-1,1], [-1,-1,1]
    ];

    public $faces = [[0,1,2,3],[1,5,6,2],[5,4,7,6],[4,0,3,7],[4,5,1,0],[3,2,6,7]];
    public $normals = [[0,0,-1],[1,0,0],[0,0,1],[-1,0,0],[0,1,0],[0,-1,0]];

    public $scale = 80; // масштаб

    // углы поворота и координаты
    public $ax = 0, $ay = 0, $az = 0;
    public $x = 0, $y = 0, $z = 0;

    public $color = [255, 255, 255];
    public $color_edge = 0x111111;

    public $drawedges = false;

    public $dotres = 48; // разрешение точки
    public $dotsize = 0.2; // размер точки
    public $facedots = []; // точки для всех граней
    public $dotcolor = 0x000000;

    public function GameCube3d($x, $y, $z, $ax, $ay, $az, $color)
    {
        $this->x = $x;
        $this->y = $y;
        $this->z = $z;
        $this->ax = $ax;
        $this->ay = $ay;
        $this->az = $az;
        
        $this->GenerateDots();
    }

    // Схема расположения точек на кубике
    public function GenerateDots()
    {
        $s = $this->dotsize;
        
        $this->facedots = [
            [
                $this->Dot(0,0,$s),
                $this->Dot(0.5,0.5,$s),
                $this->Dot(0.5,-0.5,$s),
                $this->Dot(-0.5,-0.5,$s),
                $this->Dot(-0.5,0.5,$s),
            ],
            [
                $this->Dot(-0.5,-0.5,$s,2,1),
                $this->Dot(0.5,-0.5,$s,2,1),
                $this->Dot(-0.5,0.5,$s,2,1),
                $this->Dot(0.5,0.5,$s,2,1),
            ],
            [
                $this->Dot(0.4,-0.4,$s,0,1),
                $this->Dot(-0.4,0.4,$s,0,1),
            ],
            [
                $this->Dot(-0.5,0.5,$s,2),
                $this->Dot(0,0,$s,2),
                $this->Dot(0.5,-0.5,$s,2),
            ],
            [
                $this->Dot(-0.5,0.5,$s, 1,1),
                $this->Dot(0,0.5,$s, 1,1),
                $this->Dot(0.5,0.5,$s, 1,1),
                $this->Dot(-0.5,-0.5,$s, 1,1),
                $this->Dot(0,-0.5,$s, 1,1),
                $this->Dot(0.5,-0.5,$s, 1,1),
            ],
            [
                $this->Dot(0,0,$s, 1),
            ],
        ];
    }

    // Расчет нормалей
    public function CalculateNormals()
    {
        for($i = 0; $i < sizeof($this->faces); $i++)
        {
            $f = $this->faces[$i];
            
            $this->normals[$i] = cross_product3p(
                $this->points[$f[0]],
                $this->points[$f[1]],
                $this->points[$f[2]]
            );
            
            normalize($this->normals[$i]);
        }
    }

    // Получаем точку на кубе в нужной плоскости
    public function Dot($x, $y, $r, $f=0, $d = -1)
    {
        $poly = [];
        
        $da = M_2PI / $this->dotres;
        for($a = 0; $a < M_2PI; $a += $da)
        {
            $u = $x + $r * sin($a);
            $v = $y + $r * cos($a);
            
            switch($f)
            {
                case 0: $poly[] = [$u, $v, $d]; break; //XY
                case 1: $poly[] = [$u, $d, $v]; break; //XZ
                case 2: $poly[] = [$d, $v, $u]; //ZY
            }
        }
        return $poly;
    }
}

Вспомогательные функции

function rotx($v, $a)
{
    $n[0] = $v[0];
    $n[1] = $v[1] * cos($a) - $v[2] * sin($a);
    $n[2] = $v[1] * sin($a) + $v[2] * cos($a);

    return $n;
}

function roty($v, $a)
{
    $n[0] = $v[0] * cos($a) - $v[2] * sin($a);
    $n[1] = $v[1];
    $n[2] = $v[0] * sin($a) + $v[2] * cos($a);
    
    return $n;
}

function rotz($v, $a)
{
    $n[0] = $v[0] * cos($a) - $v[1] * sin($a);
    $n[1] = $v[0] * sin($a) + $v[1] * cos($a);
    $n[2] = $v[2];
    
    return $n;
}

function rgb($r,$g,$b)
{
    return $r<<16 | $g<<8 | $b;
}

function cross_product3p($p1, $p2, $p3)
{
    $v1 = [$p2[0] - $p1[0], $p2[1] - $p1[1], $p2[2] - $p1[2]];
    $v2 = [$p3[0] - $p2[0], $p3[1] - $p2[1], $p3[2] - $p2[2]];
    
    return [$v1[1] * $v2[2] - $v2[1] * $v1[2],
            $v1[2] * $v2[0] - $v2[2] * $v1[0],
            $v1[0] * $v2[1] - $v2[0] * $v1[1]];
}

function dotproduct($v1, $v2)
{
    return $v1[0]*$v2[0] + $v1[1]*$v2[1] + $v1[2]*$v2[2];
}

function normalize(& $v)
{
    $mag = sqrt($v[0]*$v[0] + $v[1]*$v[1] + $v[2]*$v[2]);
    $v[0] /= $mag;
    $v[1] /= $mag;
    $v[2] /= $mag;
}

function poly2gd($poly)
{
    $newpoly = [];
    foreach($poly as $p) array_push($newpoly, $p[0], $p[1]);
    return $newpoly;
}

function lim255($v) { return $v > 255 ? 255 : $v; };

Использование

include('inc/gamecube3d.php');

header("Content-Type: image/png");

$width = 800;
$height = 600;

$color_bkg = 0x1b183b;

$scene = new Scene($width, $height);
$scene->ambient = 0.3;
$scene->vlight = [-1,1,2];
$scene->rproj = 0.05;

$cube1 = new GameCube3D(-2, 0, 1, 0, -1.9, 0, [255,255,255]);
$cube2 = new GameCube3D(1.7, 0, -1, M_PI/2, 0.8, 0, [255,255,255]);
$scene->Add($cube1);
$scene->Add($cube2);
$scene->Render();

$gd = imagecreatetruecolor($width, $height);
imageantialias($gd, true);
imagefilledrectangle($gd, 0, 0, $width, $height, $color_bkg);//$bk);

$scene->Draw($gd);

imagepng($gd);
imagedestroy($gd);

Результат

Кубики

Замечания

  1. Это решение "заточено" под вывод кубиков, хотя можно развить и дальше.
  2. Реализован упрощенный тест глубины для вывода только видимых граней, который применим только к кубам. При пересечении нескольких кубов победит сильнейший.
  3. Решение не использует сторонних библиотек, матриц и векторных классов.
  4. Простая модель освещения с направленным светом.
  5. Я схитрил, поэтому вместо наложения текстур я нарисовал точки на каждой грани. Правда есть идея как накладывать текстуры.
  6. Считаю, что рендеринг 3D-сцен на сервере неперспективное направление, WebGL и Three.js к моим рекомендациям.
→ Ссылка