Растительность
Растительность, которая движется и стоит дешевле. Модель, загруженная как foliage, берёт шейдер, чьи вершины гнутся на ветру, отклоняются от того, что сквозь них проходит, и редеют с расстоянием, а каждая копия получает чуть иной оттенок. Это уровень семейства материалов lightmap — на запечённом уровне растение берёт запечённый свет земли под собой — а копии одного ассета делят один экземпляр материала, так что уровень из тысяч карточек травы и листвы рендерится быстрее, чем на стандартном шейдере.
Всё здесь — состояние уровня, которое держит движок: ветер и опции вида применяются к каждой foliage-модели, нынешней и будущей; изгибатели читаются из их живых трансформов каждый кадр; растворение принадлежит ассету, так что каждая копия этой модели его разделяет.
Обзор
// level.scene.ts — трава загружается как foliage; env.foliage — ветер уровня
import { Hero } from './hero'
export default defineScene({
env: {
foliage: { wind: { direction: [1, 0.3], speed: 6, gust: 0.4 }, touch: 0.6, variation: 0.35 },
},
nodes: {
grass: { model: asset('./assets/grass.glb'), foliage: { fade: [30, 40] } },
player: { mesh: { kind: 'capsule' }, position: [0, 1, 0], aspects: [use(Hero)] },
},
})
// hero.ts — капсула раздвигает траву
export class Hero extends Aspect<'hero', Node> {
onAttach(): void {
Foliage.bend(this.node, 0.6)
}
}Пометка модели как foliage
Model.load(src, { foliage: true }): Promise<Model> // код
{ model: asset('./grass.glb'), foliage: true } // узел файла сцены
{ model: asset('./grass.glb'), foliage: { fade: [30, 40] } } // + растворение по расстоянию (ниже)Foliage-модели не нужны ни lightmap: true, ни собственный прямоугольник атласа: её запечённый
свет — свет земли под каждым пикселем (запечённый террейн), а без
запекания её освещают небесный ambient сцены и солнце. Клоны наследуют уровень. lecodes assets scene сам помечает Unity-kind как foliage: true, когда один из его материалов лежит в папке
растительности или так назван (Materials/Trees/…, M_Grass_01), никогда по режиму альфы;
--foliage <re> называет kind-ы явно.
Уровень выбирается на ассет: каждый материал alpha-MASK (карточки) берёт маскированного близнеца, непрозрачные (стволы) — непрозрачного, и оба качаются.
хост, чей рантайм не несёт материала foliage, загружает модель через уровень
lightmap, когда она ещё и статика лайтмапа (lightmap: true, статичный узел запечённой
сцены), иначе стандартным шейдером; хост без моста foliageSet* игнорирует каждый вызов
Foliage. У десктопного хоста есть и то и другое.
Ветер и вид — Foliage.configure / Foliage.wind
Foliage.configure(options: {
wind?: FoliageWind | false // ниже; false = штиль
touch?: number // м, на которые верхушка небольшого растения отклоняется от изгибателя в его центре; по умолчанию 0.6, 0 = нет
variation?: number // 0..1 дрейф оттенка на копию; по умолчанию 0.35
sunWrap?: number // 0..1 обёрнутое солнечное освещение (тонкие листья пропускают свет); по умолчанию 0.5
shiver?: number // × shiver drag каждого материала (собственное движение листьев); по умолчанию 1, 0 = нет
translucency?: number // 0..1 солнце сквозь тонкий лист, освещённый сзади (крона в контровом свете светится); по умолчанию 0.35
leafShadow?: number // 0..1 какую долю тени источника принимает карточка листа (кора принимает всю); по умолчанию 0.6
skyAo?: number // 0..1 насколько запечённая повершинная видимость неба затемняет ambient; по умолчанию 0.7
canopy?: number // 0..1 нормаль шейдинга карточки: её собственная плоскость (0) … оболочка кроны (1); по умолчанию 0.7
normalMap?: number // сила карты нормалей карточек; по умолчанию 0.5
edgeFade?: number // карточка, видимая с ребра, растворяется на такой доле |N·V|; по умолчанию 0.15, 0 = нет
coverage?: number // alpha × (1 + coverage × mip level): далёкая крона не редеет; по умолчанию 0.25
}): void
Foliage.wind(wind: FoliageWind | false): void // только ветер, вживую
type FoliageWind = {
direction?: [x, z] // по земле, нормализуется движком; по умолчанию [1, 0.3]
speed?: number // м/с — постоянный наклон растений и как быстро дрейфует поле; по умолчанию 8.3, 0 = неподвижно
turbulence?: number // 0..2 случайная доля скорости; по умолчанию 0.4
gust?: number // 0..1 сила порывов; по умолчанию 0.35
gustSpeed?: number // м/с скорость, с которой движется узор порывов; по умолчанию 5.6
}Ветер — это поле, а не синусоида: прокручивающееся шумовое поле, читаемое у корня каждого
растения, наклоняет всё растение вокруг корня — угол растёт с расстоянием от корня, так что крона
ели движется дальше, чем травинка — а более мелкое поле, читаемое в каждой вершине, двигает листья
(дрожь). Фиксированного периода нет: turbulence уводит растение от среднего ветра, порывы едут на
медленном третьем поле и проходят по уровню со скоростью gustSpeed. То, что отличается у каждого
растения, идёт из модели: drag, жёсткость, постоянный наклон и shiver drag его материалов
лежат в extras материалов GLB (lecodes.wind, записывается lecodes assets scene из
Unity-материала), иначе — значение по умолчанию по размеру растения.
Каждое переданное поле заменяет текущее значение; остальные сохраняются. Блок env.foliage файла
сцены проходит через configure до загрузки моделей. Всё это живое — Foliage.wind({ speed: 20 })
из консоли пробует бурю.
уровень читает COLOR_0 GLB растительности как ДАННЫЕ, никогда как оттенок:
(видимость неба, впадины коры, 1, вес ветра). lecodes assets scene запекает его на каждом
kind растительности при импорте (видимость неба из каждой вершины — внутренность кроны и ствол
под ней берут меньше ambient; вес ветра — собственный у пака, то, что пак деревьев запекает как
ствол 0 / хвоя ~0.4, или квадрат высоты по растению). Дрожь взвешивается этим весом ветра; модель
без цветов вершин не получает окклюзии и берёт вес по высоте: кончики движутся, основание —
никогда.
покачивание работает на стенных часах движка, так что продолжает двигаться, пока игровые часы на паузе — меню паузы над качающимся лесом, так задумано.
Касание — Foliage.bend
Foliage.bend(node: Node, radius = 0.7): () => void // возвращает функцию снятия регистрации
Foliage.unbend(node: Node): voidИзгибатель — узел, чья живая мировая позиция раздвигает растительность: капсула героя, враг,
катящийся валун. Растение отклоняется целиком — укоренённое в основании, полностью у
верхушки — когда изгибатель оказывается в пределах radius от его отпечатка. Движок измеряет
каждый ассет при загрузке: растения ниже ~2 м получают полный наклон, реакция затухает к ~5 м, так
что трава и кусты расступаются вокруг тебя, а дерево не скручивается, когда ты задеваешь его
ствол.
Движок также отслеживает скорость каждого изгибателя (сглаженную за ~0.2 с) и возле центра растения наклоняет его вдоль направления движения, а не прямо от изгибателя: куст, через который ты бежишь, пригибается вперёд и оседает позади тебя, а не переворачивается, когда ты пересекаешь его середину. Изгибатель, стоящий внутри растения неподвижно, оставляет его почти прямым.
Регистрировать можно любое число узлов; каждый кадр действуют 8 ближайших к камере. Уничтоженный узел выпадает сам.
Растворение по расстоянию — Foliage.fade
Foliage.fade(model: Model, start: number, end: number): void // метры от камеры; end 0 = нетМежду start и end карточки редеют (альфа опускается ниже порога маски тексель за текселем), а
за end копия целиком уходит из кадра — LOD-проход убирает её из сцены, так что она не стоит
отрисовки. Задумано для наземного покрова при свободной камере: трава и клевер на [30, 40],
ничего на деревьях. Диапазон принадлежит ассету: каждая копия этой модели, нынешняя и будущая,
его разделяет; в файле сцены foliage: { fade: [start, end] } на одном узле задаёт его для всех.
Камера, которая никогда не видит дальше диапазона (игра с видом сверху), ничего от него не
выигрывает — копии вне экрана и так отсечены.
Вариация на копию
Дрейф оттенка (variation) берётся из хэша позиции каждой копии внутри шейдера, а не из
параметра на копию: копии одного ассета продолжают делить один экземпляр материала, что и
позволяет движку дёшево рисовать их тысячами. Убавь его у вида, где текстура уже несёт диапазон
цвета.
Карточки дешевле — lecodes assets doctor --trim-cards
Карточка растительности — это в основном прозрачные тексели, которые маскированный шейдер
отбрасывает после шейдинга, и на уровне из тысяч карточек этот отброшенный fill — весь кадр.
--trim-cards [--trim-margin N] обрезает каждый alpha-MASK-квад до прямоугольника его непрозрачных
текселей (+ N текселей, по умолчанию 2) и выбрасывает пустые квады — визуально ничего не меняется.
Он читает базовый цвет PNG/JPEG, так что запускай его вместе с --ktx2, а не после него
(lecodes assets scene так и делает, на каждом kind).
Подводные камни
- Модель с
foliage: true, чьи материалы в логе хоста читаются как стандартно-шейдированные ([glb] material … -> ubershader), взяла стандартный шейдер: либо материал, который уровень отклоняет (правила lightmap.md «Какие материалы» — строка[lightmap] …называет, чего ему не хватает), либо хост без этого уровня. Это не ошибка файла сцены. Foliage.bendна модели героя, а не на капсуле его контроллера, следует за визуальным корнем, который может отставать от капсулы или опережать её; регистрируй узел, который двигает физика.
Смотрите также
- lightmap.md — семейство, к которому принадлежит этот уровень: запечённые тени, световой объём, уровни
- terrain.md — земля, чей запечённый свет берут растения
- model.md — опции
Model.load, клоны, LOD - scene-files.md —
foliageна узле,env.foliage