Level — процедурная расстановка
Расставь много копий одной вещи по шаблону — вдоль линии, по сетке, по кольцу, рассеянно по площади
или по вручную выбранному списку точек — и получи их обратно как ОДИН узел-группу Node. Сделано для
фабрик make() в файлах сцен: линия забора или
лес — это один узел сцены с парой редактируемых аргументов вместо сотен записей с координатами.
Работает и из обычного кода — добавь возвращённую группу в сцену.
Обзор
// level/props.ts — фабрики make()
export const fence = (args: { posts?: number, length?: number }) =>
Level.line({
from: [0, 0, 0], to: [args.length ?? 10, 0, 0], count: args.posts ?? 6,
build: () => Mesh.box({ size: [0.1, 1, 0.1] }), name: 'post',
})
export const forest = (args: { count?: number, seed?: number }) =>
Level.scatter({
size: [40, 40], count: args.count ?? 30, minSpacing: 2.5, seed: args.seed ?? 1,
avoid: [{ x: 0, z: 0, r: 5 }], // держать спавн свободным
build: asset('../assets/tree.glb'), collider: 'convex',
})
// yard.scene.ts
import { fence, forest } from './level/props'
export default defineScene({
nodes: {
fence: { make: make(fence, { posts: 6, length: 10 }), position: [-5, 0, 3] },
trees: { make: make(forest, { count: 30, seed: 7 }) },
},
})Каждая координата, которую принимает хелпер, локальна для возвращённой группы; узел сцены,
который её make()-ает, владеет мировым трансформом (выше post1 оказывается в мировой
[-5, 0, 3]).
Хелперы
Level.line(opts: LevelLineOptions): Promise<Node> // вдоль отрезка — заборы, стены, ряды фонарей
Level.grid(opts: LevelGridOptions): Promise<Node> // решётка cols × rows — ящики, парковки
Level.ring(opts: LevelRingOptions): Promise<Node> // окружность или дуга — колонны, каменные круги
Level.scatter(opts: LevelScatterOptions): Promise<Node> // сидированный случайный разброс в прямоугольнике — леса, обломки
Level.place(opts: LevelPlaceOptions): Promise<Node> // явный список точек
Level.rng(seed: number): () => number // сидированный ГПСЧ, который они используют (mulberry32), [0, 1)Каждый возвращает простую, безымянную, неподвешенную группу, чьи дети — размещения. Из фабрики
make() её монтирует загрузчик сцены; из кода — scene.add(await Level.grid({ … })).
// line — count: размещения, концы включены (count > 1); или step: шаг в м от `from`
// к `to`, последнее ≤ `to`. Ни того ни другого → 2 (концы). Шаг измеряется в XZ; y интерполируется.
{ from: Vec3Like, to: Vec3Like, count?: number, step?: number }
// grid — cols вдоль X, rows вдоль Z, с центром в `center`
{ cols: number, rows: number, spacing: number | [alongX, alongZ], center?: Vec3Like } // center по умолчанию [0, 0, 0]
// ring — start: градусы, 0 = +X, растёт к +Z; arc: градусы (360 = полное кольцо, последняя точка
// на один шаг раньше первой; дуга ставит оба конца)
{ radius: number, count: number, center?: Vec3Like, start?: number, arc?: number, facing?: 'in' | 'out' }
// по умолчанию 0 по умолчанию 360 по умолчанию 'in'
// scatter — прямоугольник `size` [x, z] (одно число = квадрат) вокруг `center`, в плоскости на `y`
{ size: number | [number, number], count: number, center?: Vec3Like, y?: number, // y по умолчанию: y центра
minSpacing?: number, // м между любыми двумя размещениями
avoid?: { x: number, z: number, r: number }[] | ((x: number, z: number) => boolean) } // круги, или true = отклонить
// place — [x, y, z] или [x, y, z, yawDegrees] на точку
{ at: number[][] }scatter сэмплирует с отклонением в бюджете count × 30 попыток; когда minSpacing / avoid
оставляют слишком мало места, он ставит, что поместилось, и предупреждает
([Level.scatter] placed 3/10 — the area is too tight for minSpacing/avoid).
Общие опции
build: string | ((spot: LevelSpot) => Node | null | Promise<Node | null>) // что ставить
collider?: 'mesh' | 'convex' | 'box' | 'none' // только для build по URL; по умолчанию 'none'
yaw?: number | 'random' | 'face' | 'align' // градусы вокруг Y или правило; умолчание у каждого хелпера своё
jitter?: number | [maxX, maxZ] // случайное смещение по XZ на точку, м (по каждой оси ±)
seed?: number // по умолчанию 1
name?: string // префикс имени детей
type LevelSpot = { index: number, position: [x, y, z], yaw: number, rng: () => number }buildкак URL (asset('./crate.glb')): GLB загружается один раз и клонируется на каждую точку.colliderтогда прикрепляетShape+Physics { motion: 'static' }к каждой копии —'mesh'точные треугольники,'convex'их оболочка,'box'бокс, подогнанный под границы меша (physics.md).buildкак функция получает каждую точку — еёindexс нуля, её локальнуюposition(с учётом jitter), еёyawв градусах иrng, сидированный поток для вариаций на точку (отдельный от потока раскладки) — и возвращает узел для размещения илиnull, чтобы пропустить точку. Она сама прикрепляет свои аспекты (включая коллайдеры);colliderне применяется.yaw:'face'поворачивает «вперёд» каждого узла (+Z) вдоль направления хелпера,'align'кладёт ось X модели вдоль него (панели заборов и стен),'random'— целое число градусов из потока, число — этот угол. Умолчания:line'align';ring'face'= к центру (facing: 'out'= от него);gridиplace'face', что там равно 0;scatter'random'.- Имена — префикс + индекс точки с единицы (
post1,post2, …); пропущенная точка оставляет свой номер неиспользованным. Префикс —name, иначе имя файла из URL, иначеitem.
хелпер пишет position и — когда yaw не 0 — eulerAngles = [0, yaw, 0] на узел, который возвращает
твой build, поверх того, что задал билдер. Чтобы наклонить или сместить размещение, верни группу с
позированным узлом внутри.
передавай name всякий раз, когда код ищет размещения по имени. Там, где нативный хост загружает
ассеты из локальных файлов (desktop run, render --desktop / --sim, приложение, которое везёт
ассеты с собой), URL asset() — внутренний handle id:, а не путь, и имена по умолчанию получаются
как этот handle плюс индекс (id:1011, id:1012, … под render --sim) вместо tree1,
tree2, ….
Детерминизм
Те же опции и seed → та же раскладка, при каждом запуске и на каждом хосте — редактор сцен
перезапускает фабрику make() при каждой правке аргумента, и ничего не перемешивается. Каждый хелпер
берёт свою раскладку (jitter, случайный yaw, точки scatter) из собственного потока seed; spot.rng —
другой поток того же сида, так что то, что функция build с ним делает, никогда не сдвигает раскладку.
Level.rng(seed) — тот же генератор, для собственной расстановки, которая должна оставаться
воспроизводимой рядом с ними:
const rng = Level.rng(42)
const scale = 0.8 + rng() * 0.4 // [0.8, 1.2), одно и то же при каждом запускев уровне с запечённым освещением (env.lightmap) вывод make() — код, а не узлы сцены — регистрируй
статику сам через Lightmap.add (lightmap.md).
Смотрите также
- scene-files.md — узлы
make(fn, args), формат файла сцены - model.md — что загружает build по URL
- physics.md —
Shape/Physics, что прикрепляетcollider