solids as $solid) { if (($solid['triangles'] ?? []) !== []) { foreach ($solid['triangles'] as $vertex) { self::vertex($out, $vertex['p'], [ $vertex['color'][0] / 255, $vertex['color'][1] / 255, $vertex['color'][2] / 255, ], $vertex['n']); } } else { self::boxInto($out, $solid['min'], $solid['max'], $solid['color']); } } return $out; } /** @return float[] */ public static function decals(array $decals, string $material): array { // Legacy poster3615 placements are intentionally absorbed into the // procedural DeepSeek atlas. The old troll texture is never rendered. if ($material === 'poster3615') return []; $out = []; $ordinal = 0; foreach ($decals as $decal) { $decalMaterial = (string)($decal['material'] ?? ''); if ($material === 'posterAd') { if ($decalMaterial !== 'posterAd' && $decalMaterial !== 'poster3615') continue; } elseif ($decalMaterial !== $material) { continue; } [$x, $y, $z] = $decal['position']; [$nx, $ny, $nz] = $decal['normal']; $halfW = $decal['width'] * 0.5; $halfH = $decal['height'] * 0.5; // Cardinal wall tangent. This orientation keeps readable posters left-to-right. $rx = $nz; $rz = -$nx; $offset = 0.012; $cx = $x + $nx * $offset; $cy = $y + $ny * $offset; $cz = $z + $nz * $offset; $leftX = $cx - $rx * $halfW; $leftZ = $cz - $rz * $halfW; $rightX = $cx + $rx * $halfW; $rightZ = $cz + $rz * $halfW; $bottom = $cy - $halfH; $top = $cy + $halfH; $normal = [$nx, $ny, $nz]; $color = [1.0, 1.0, 1.0]; [$u0, $v0, $u1, $v1] = $material === 'posterAd' ? self::posterAtlasUv($decal, $ordinal) : [0.0, 0.0, 1.0, 1.0]; $ordinal++; self::vertex($out, [$leftX, $bottom, $leftZ], $color, $normal, [$u0, $v1]); self::vertex($out, [$leftX, $top, $leftZ], $color, $normal, [$u0, $v0]); self::vertex($out, [$rightX, $top, $rightZ], $color, $normal, [$u1, $v0]); self::vertex($out, [$leftX, $bottom, $leftZ], $color, $normal, [$u0, $v1]); self::vertex($out, [$rightX, $top, $rightZ], $color, $normal, [$u1, $v0]); self::vertex($out, [$rightX, $bottom, $rightZ], $color, $normal, [$u1, $v1]); } return $out; } /** Deterministic atlas cell from world placement: same level = same posters, different layout = different mix. */ private static function posterAtlasUv(array $decal, int $ordinal): array { [$x, $y, $z] = $decal['position']; $variant = ((int)round(abs($x) * 37.0 + abs($y) * 17.0 + abs($z) * 53.0) + $ordinal * 7) % (PosterFactory::ATLAS_COLS * PosterFactory::ATLAS_ROWS); $col = $variant % PosterFactory::ATLAS_COLS; $row = intdiv($variant, PosterFactory::ATLAS_COLS); $u0 = $col / PosterFactory::ATLAS_COLS; $u1 = ($col + 1) / PosterFactory::ATLAS_COLS; $v0 = $row / PosterFactory::ATLAS_ROWS; $v1 = ($row + 1) / PosterFactory::ATLAS_ROWS; return [$u0, $v0, $u1, $v1]; } /** @return float[] */ public static function enemy(string $type): array { $out = []; if ($type === 'runner') { self::boxInto($out, [-0.24,-0.25,-0.32], [0.24,0.12,0.32], [75,170,130]); self::boxInto($out, [-0.16,0.10,-0.16], [0.16,0.40,0.16], [170,238,128]); self::boxInto($out, [-0.36,-0.55,-0.13], [-0.14,-0.20,0.13], [48,95,76]); self::boxInto($out, [0.14,-0.55,-0.13], [0.36,-0.20,0.13], [48,95,76]); return $out; } if ($type === 'brute') { self::boxInto($out, [-0.46,-0.55,-0.35], [0.46,0.65,0.35], [102,76,152]); self::boxInto($out, [-0.29,0.65,-0.24], [0.29,1.0,0.24], [202,147,81]); self::boxInto($out, [-0.70,-0.32,-0.22], [-0.46,0.65,0.22], [170,100,47]); self::boxInto($out, [0.46,-0.32,-0.22], [0.70,0.65,0.22], [170,100,47]); return $out; } self::boxInto($out, [-0.30, -0.55, -0.20], [0.30, 0.35, 0.20], [120, 50, 48]); self::boxInto($out, [-0.20, 0.35, -0.17], [0.20, 0.72, 0.17], [190, 142, 92]); self::boxInto($out, [-0.28, 0.66, -0.12], [0.28, 0.80, 0.12], [58, 168, 150]); self::boxInto($out, [-0.48, -0.42, -0.12], [-0.30, 0.20, 0.12], [91, 42, 54]); self::boxInto($out, [0.30, -0.30, -0.12], [0.52, 0.13, 0.12], [91, 42, 54]); return $out; } /** Original low-poly pickup meshes; no external artwork. */ public static function pickup(bool $health): array { $out = []; if ($health) { self::boxInto($out, [-0.20,-0.08,-0.08], [0.20,0.08,0.08], [235,65,75]); self::boxInto($out, [-0.08,-0.20,-0.08], [0.08,0.20,0.08], [235,65,75]); } else { self::boxInto($out, [-0.10,-0.16,-0.10], [0.10,0.16,0.10], [70,230,225]); } return $out; } public static function pulse(): array { $out = []; for ($i = 0; $i < 24; $i++) { $a = $i * M_PI / 12; $b = ($i + 1) * M_PI / 12; foreach ([[sin($a),0,cos($a)], [sin($b),0,cos($b)], [sin($a)*0.94,0,cos($a)*0.94], [sin($b),0,cos($b)], [sin($b)*0.94,0,cos($b)*0.94], [sin($a)*0.94,0,cos($a)*0.94]] as $p) { self::vertex($out, $p, [0.2,0.95,1.0], [0,1,0]); } } return $out; } /** @param float[] $min @param float[] $max @param int[] $rgb */ private static function boxInto(array &$out, array $min, array $max, array $rgb): void { [$x0, $y0, $z0] = $min; [$x1, $y1, $z1] = $max; $c = [$rgb[0] / 255, $rgb[1] / 255, $rgb[2] / 255]; $faces = [ [[$x0,$y0,$z0],[$x0,$y1,$z0],[$x1,$y1,$z0],[$x1,$y0,$z0],[0,0,-1]], [[$x1,$y0,$z1],[$x1,$y1,$z1],[$x0,$y1,$z1],[$x0,$y0,$z1],[0,0,1]], [[$x0,$y0,$z1],[$x0,$y1,$z1],[$x0,$y1,$z0],[$x0,$y0,$z0],[-1,0,0]], [[$x1,$y0,$z0],[$x1,$y1,$z0],[$x1,$y1,$z1],[$x1,$y0,$z1],[1,0,0]], [[$x0,$y1,$z0],[$x0,$y1,$z1],[$x1,$y1,$z1],[$x1,$y1,$z0],[0,1,0]], [[$x0,$y0,$z1],[$x0,$y0,$z0],[$x1,$y0,$z0],[$x1,$y0,$z1],[0,-1,0]], ]; foreach ($faces as [$a, $b, $c0, $d, $n]) { self::vertex($out, $a, $c, $n); self::vertex($out, $b, $c, $n); self::vertex($out, $c0, $c, $n); self::vertex($out, $a, $c, $n); self::vertex($out, $c0, $c, $n); self::vertex($out, $d, $c, $n); } } /** @param float[] $p @param float[] $c @param float[] $n */ private static function vertex(array &$out, array $p, array $c, array $n, ?array $uv = null): void { [$u, $v] = $uv ?? self::uv($p, $n); array_push($out, $p[0], $p[1], $p[2], $c[0], $c[1], $c[2], $n[0], $n[1], $n[2], $u, $v); } /** Cheap planar mapping: one 64px-ish repeat every two world units. */ private static function uv(array $p, array $n): array { $ax = abs($n[0]); $ay = abs($n[1]); $az = abs($n[2]); if ($ay >= $ax && $ay >= $az) return [$p[0] * 0.5, $p[2] * 0.5]; if ($ax >= $az) return [$p[2] * 0.5, $p[1] * 0.5]; return [$p[0] * 0.5, $p[1] * 0.5]; } }