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]); } }