/**
* Окно видимости частиц: AABB полиэдра из полуплоскостей (сляб орто-камеры,
* пересечённый с горизонтальным слоем частиц). Точный перебор вершин —
* пересечений троек плоскостей: ограничений 8, комбинаций 56, дёшево.
*/
type Plane = readonly [readonly [number, number, number], number];
/** AABB допустимой области {δ: n·δ ≤ b для всех плоскостей} по осям x, z. */
export function polyAABB(planes: readonly Plane[]): {
minX: number; maxX: number; minZ: number; maxZ: number;
} {
let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
const n = planes.length;
for (let i = 0; i < n; i++)
for (let j = i + 1; j < n; j++)
for (let k = j + 1; k < n; k++) {
const p = solve3(planes[i]![0], planes[i]![1], planes[j]![0], planes[j]![1], planes[k]![0], planes[k]![1]);
if (!p) continue; // вырожденный набор нормалей
let ok = true;
for (const [nv, b] of planes)
if (nv[0]! * p[0]! + nv[1]! * p[1]! + nv[2]! * p[2]! > b + 1e-6) { ok = false; break; }
if (!ok) continue;
minX = Math.min(minX, p[0]!); maxX = Math.max(maxX, p[0]!);
minZ = Math.min(minZ, p[2]!); maxZ = Math.max(maxZ, p[2]!);
}
return { minX, maxX, minZ, maxZ };
}
/** Пересечение трёх плоскостей n_i·δ = b_i (Крамер); null — если нормали
* зависимы (определитель ~0). */
function solve3(
n1: readonly [number, number, number], b1: number,
n2: readonly [number, number, number], b2: number,
n3: readonly [number, number, number], b3: number,
): [number, number, number] | null {
const det =
n1[0]! * (n2[1]! * n3[2]! - n2[2]! * n3[1]!) -
n1[1]! * (n2[0]! * n3[2]! - n2[2]! * n3[0]!) +
n1[2]! * (n2[0]! * n3[1]! - n2[1]! * n3[0]!);
if (Math.abs(det) < 1e-9) return null;
const dx =
b1 * (n2[1]! * n3[2]! - n2[2]! * n3[1]!) -
n1[1]! * (b2 * n3[2]! - n2[2]! * b3) +
n1[2]! * (b2 * n3[1]! - n2[1]! * b3);
const dy =
n1[0]! * (b2 * n3[2]! - n2[2]! * b3) -
b1 * (n2[0]! * n3[2]! - n2[2]! * n3[0]!) +
n1[2]! * (n2[0]! * b3 - b2 * n3[0]!);
const dz =
n1[0]! * (n2[1]! * b3 - b2 * n3[1]!) -
n1[1]! * (n2[0]! * b3 - b2 * n3[0]!) +
b1 * (n2[0]! * n3[1]! - n2[1]! * n3[0]!);
return [dx / det, dy / det, dz / det];
}
/** Полуразмеры и слой — параметры окна частиц. */
export interface WindowParams {
/** Базис камеры: r — экранная горизонталь, u — вертикаль, f — взгляд. */
r: V3; u: V3; f: V3;
/** Полуразмеры фрустума: половина ширины и высоты. */
a: number; h: number;
/** Глубины ближней/дальней плоскостей относительно цели камеры. */
k0: number; k1: number;
/** Горизонтальный слой частиц относительно цели камеры. */
dy0: number; dy1: number;
}
type V3 = readonly [number, number, number];
export interface WindowBox { minX: number; maxX: number; minZ: number; maxZ: number }
/** Окно видимости частиц: AABB сляба ортокамеры на слое частиц. Смещение δ
* от цели камеры видно на экране ⇔ |r·δ|≤a, |u·δ|≤h, k0 ≤ f·δ ≤ k1; слой
* добавляет δ.y ∈ [dy0,dy1]. Окно шире наземного следа кадра: высокую
* частицу видно дальше от цели, сляб уходит в глубину до дальней плоскости.
* Все ограничения — в форме n·δ ≤ b (нижние границы — с переворотом нормали). */
export function particleWindow(p: WindowParams): WindowBox {
return polyAABB([
[[-p.r[0]!, -p.r[1]!, -p.r[2]!], p.a], [p.r, p.a],
[[-p.u[0]!, -p.u[1]!, -p.u[2]!], p.h], [p.u, p.h],
[[-p.f[0]!, -p.f[1]!, -p.f[2]!], -p.k0], [p.f, p.k1],
[[0, -1, 0], -p.dy0], [[0, 1, 0], p.dy1],
]);
}