Ray, Plane и Noise
Три небольших класса 3D-математики/утилит. Ray + Plane — набор для tap-to-place: преврати
экранный тап в луч камеры, пересеки его с плоскостью земли, размести в точке попадания. Для рейкаста
против тел, а не математической плоскости, используй
Physics.raycast (он принимает origin/dir у Ray). Noise — нативный
сэмплер Перлина/фрактала для рельефа, покачивания и органичного движения.
Обзор
const ground = new Plane([0, 1, 0], [0, 0, 0]) // Y-up плоскость через начало координат
scene.addEventListener('click', ev => {
const ray = scene.camera.getRay(ev.clientX, ev.clientY)
const t = ground.intersectRay(ray)
if (t !== null) marker.position = ray.getPoint(t) // размести в точке тапа по полу
})Ray
new Ray(origin: Vec3Like, dir: Vec3Like) // dir не обязан быть нормализован
ray.origin // Vec3
ray.dir // Vec3
ray.getPoint(t) // Vec3 — origin + t·dirОбычный источник — камера: scene.camera.getRay(screenX, screenY) строит луч пикинга от камеры через
экранный пиксель (логические px — ev.clientX/clientY подставляются напрямую). См.
Camera.
параметр getPoint(t) — в единицах длины dir, мировые единицы только когда dir нормализован.
dir лучей камеры берётся из направления взгляда хоста; нормализуй (ray.dir.normalize()) прежде,
чем трактовать t как дистанцию в метрах.
Plane
Бесконечная геометрическая плоскость, хранимая как единичная normal + скалярное смещение d
(dot(normal, point)).
new Plane(normal: Vec3Like, point: Vec3Like) // normal нормализуется за тебя
Plane.fromPoints(a, b, c): Plane // через три точки (нормаль из обхода)
Plane.fromCoefficients(a, b, c, d): Plane // из ax + by + cz = d
plane.normal // Vec3, единичной длины (только чтение)
plane.d // number (только чтение)
plane.intersectRay(ray): number | null // параметр t ≥ 0 или null (параллельно / позади начала)
plane.intersectLine(ray): number | null // то же, но попадания позади начала разрешены (t может быть < 0)
plane.signedDistance(point): number // положительное на стороне нормали
plane.projectPoint(point): Vec3 // ближайшая точка на плоскостиОба метода пересечения возвращают параметр луча t, не точку — передай его в ray.getPoint(t).
// держим персонажа над наклонной платформой
const surface = Plane.fromPoints(p0, p1, p2)
if (surface.signedDistance(hero.position) < 0) {
hero.position = surface.projectPoint(hero.position)
}Noise
Нативный сэмплер FastNoise Перлина/фрактала. Детерминирован: одни и те же координаты всегда возвращают одно значение, поэтому анимируй, двигая точку сэмпла (напр. используй время как одну ось).
const noise = new Noise()
noise.get(x, y) // сэмпл 2D-шума
noise.get3d(x, y, z) // сэмпл 3D-шума
noise.frequency = 0.01 // get/set; по умолчанию 0.01 — пространственный масштаб (выше = мельче)
noise.octaves = 4 // get/set; по умолчанию 1 — фрактальные слои (>1 добавляет детали)
noise.fractalLacunarity = 2 // get/set; по умолчанию 2 — шаг частоты между октавами
noise.fractalGain = 0.5 // get/set; по умолчанию 0.5 — шаг амплитуды между октавами// мягкое покачивание и качание в покое
const n = new Noise()
n.frequency = 0.5
let t = 0
setLoop(dt => {
t += dt
ghost.y = 1 + n.get(t, 0) * 0.2
ghost.eulerAngles = [0, n.get(t, 100) * 15, 0]
})Подводные камни
// ✗ трактовать результат intersectRay как точку попадания
marker.position = ground.intersectRay(ray) // это скаляр t
// ✓
const t = ground.intersectRay(ray)
if (t !== null) marker.position = ray.getPoint(t)
// ✗ ждать, что плоскость заблокирует Physics.raycast — Plane это математика, не тело
// ✓ дай полу Shape (+ Physics { motion: 'static' }), чтобы сделать его кликабельным/твёрдымСмотрите также
- Физика и формы —
Physics.raycastпротив реальных тел,RayHit. - Camera —
scene.camera.getRay(x, y). - События указателя — откуда берутся
clientX/clientY. - Vec3 и математика — семантика значений возвращаемых векторов.