Connexion
FR
games/open-lemmings / src / View / Camera3d.php · commit 04e2e2a
<?phpnamespace App\View;use JumPHP\Gl\Mat4;/** * The camera of the 3D view, pure math (no GL: tested without a window). The level is the plane z = 0 in 3D, x to * the right, level y downwards (3D y = -level y); the terrain is extruded from z = -DEPTH / 2 to +DEPTH / 2. * * At yaw 0 the plane z = 0 is seen exactly like the 2D view (camX, camY, zoom): the same level point under the same * view pixel. A yaw turns the camera around the vertical axis through the middle of the view, and tilts it down a * little (PITCH_PER_YAW) so that the tops of the ground show. */final class Camera3d{    public const VIEW_W = 480;    public const VIEW_H = 270;    public const FOV = 30.0;    public const DEPTH = 24.0;    /** the camera looks down by this many degrees per degree of yaw */    public const PITCH_PER_YAW = 0.3;    /** @var float[] */    public array $eye;    /** @var float[] */    public array $target;    /** @var float[] unit vectors of the camera: right, up, forward (towards the target) */    public array $right;    public array $up;    public array $forward;    public float $distance;    private float $tanHalf;    /** @var float[]|null */    private ?array $mvp = null;    public function __construct(public readonly float $camX, public readonly float $camY, public readonly float $zoom, public readonly float $yaw,                                float $shakeY = 0.0)    {        $this->tanHalf = tan(deg2rad(self::FOV) / 2);        $tx = $camX + self::VIEW_W / 2 / $zoom;        $ty = $camY + self::VIEW_H / 2 / $zoom + $shakeY;        $this->distance = self::VIEW_H / 2 / $zoom / $this->tanHalf;        $a = deg2rad($yaw);        $p = deg2rad(abs($yaw) * self::PITCH_PER_YAW);        $this->target = [$tx, -$ty, 0.0];        $d = $this->distance;        $this->eye = [$tx + $d * sin($a) * cos($p), -$ty + $d * sin($p), $d * cos($a) * cos($p)];        $f = [-sin($a) * cos($p), -sin($p), -cos($a) * cos($p)];        $r = [cos($a), 0.0, -sin($a)];        $this->forward = $f;        $this->right = $r;        // up = right x forward        $this->up = [$r[1] * $f[2] - $r[2] * $f[1], $r[2] * $f[0] - $r[0] * $f[2], $r[0] * $f[1] - $r[1] * $f[0]];    }    /** @return float[] projection x view, for the shaders */    public function mvp(): array    {        return $this->mvp ??= Mat4::multiply(            Mat4::perspective(self::FOV, self::VIEW_W / self::VIEW_H, $this->distance * 0.05, $this->distance * 30),            Mat4::lookAt($this->eye, $this->target),        );    }    /**     * A level point to view pixels (0..480, 0..270). @return float[]|null [x, y] or null when behind the camera     */    public function project(float $x, float $y, float $z = 0.0): ?array    {        $p = Mat4::project($this->mvp(), [$x, -$y, $z], self::VIEW_W, self::VIEW_H);        return $p === null ? null : [$p[0], $p[1]];    }    /**     * The level point under a view pixel: the ray from the camera through it, cut by the plane z = $planeZ (0: the     * middle of the level's depth, where the Lumikins walk). @return float[]|null [x, y] level coordinates     */    public function unproject(float $vx, float $vy, float $planeZ = 0.0): ?array    {        $dir = $this->ray($vx, $vy);        if (abs($dir[2]) < 1e-6) {            return null;        }        $t = ($planeZ - $this->eye[2]) / $dir[2];        if ($t <= 0) {            return null;        }        return [$this->eye[0] + $t * $dir[0], -($this->eye[1] + $t * $dir[1])];    }    /** The direction (not normalised) of the ray from the eye through a view pixel, in 3D. @return float[] */    public function ray(float $vx, float $vy): array    {        $nx = $vx / (self::VIEW_W / 2) - 1;        $ny = 1 - $vy / (self::VIEW_H / 2);        $sx = $nx * $this->tanHalf * self::VIEW_W / self::VIEW_H;        $sy = $ny * $this->tanHalf;        $dir = [];        for ($i = 0; $i < 3; $i++) {            $dir[$i] = $this->forward[$i] + $this->right[$i] * $sx + $this->up[$i] * $sy;        }        return $dir;    }    /**     * The level columns that can be seen (the corners of the level part of the view, on the front and back     * planes), widened by $margin. @return float[] [x0, x1]     */    public function visibleX(float $levelViewH = 232, float $margin = 24): array    {        $x0 = INF;        $x1 = -INF;        foreach ([[0, 0], [self::VIEW_W, 0], [0, $levelViewH], [self::VIEW_W, $levelViewH], [0, $levelViewH / 2], [self::VIEW_W, $levelViewH / 2]] as [$vx, $vy]) {            foreach ([-self::DEPTH / 2, self::DEPTH / 2] as $z) {                $p = $this->unproject($vx, $vy, $z);                if ($p === null) {                    return [-INF, INF];                }                $x0 = min($x0, $p[0]);                $x1 = max($x1, $p[0]);            }        }        return [$x0 - $margin, $x1 + $margin];    }    /** Distance along the view direction (bigger = nearer the camera): back-to-front sorting. */    public function nearness(float $x, float $y, float $z = 0.0): float    {        return -($x * $this->forward[0] - $y * $this->forward[1] + $z * $this->forward[2]);    }}