Lightmap

Запечённое освещение для геометрии, которая никогда не двигается. Запекание один раз, на этапе разработки, трассирует солнце, небо, лампы и их отскоки на каждую статичную поверхность в атласные текстуры в люксах; в рантайме статика берёт весь свой диффузный свет из них. Статичное освещение тогда не стоит ничего за кадр, его тени остаются резкими на любом расстоянии, а shadow map реального времени должна держать только то, что движется (врагов, оружие игрока) — отчего их тени тоже становятся чётче.

Файлы сцен — обычный путь

Уровню, который является файлом сцены, нужен один блок и никакого кода:

TypeScript
export default defineScene({
  env: {
    skybox: { texture: asset('../assets/sky.ktx') },
    lightmap: {
      data: asset('../assets/lightmap/lightmap.bake'),
      light: asset('../assets/lightmap/lightmap-light.ktx2'),
      aux: asset('../assets/lightmap/lightmap-aux.ktx2'),
      probes: asset('../assets/lightmap/lightmap-probes.ktx2'),   // опционально: пробы отражений
      volume: asset('../assets/lightmap/lightmap.lgrid'),         // опционально: световая сетка для движущихся
    },
  },
  nodes: {
    ground: { mesh: { kind: 'plane', normal: [0, 1, 0] }, scale: 40 },
    barrel: { model: asset('../assets/props/barrel.glb'), aspects: [use(Shape), use(Physics)] },
  },
})

С заданным env.lightmap статики выводятся, а не объявляются: каждый узел model / mesh — статика лайтмапа — приёмник и окклюдер в запекании, без отбрасывания теней в реальном времени, как только запекание применилось — если только что-то его не двигает: аспект Physics (dynamic, значение Physics по умолчанию, или kinematic) либо CharacterController. Дети движущегося тоже движущиеся. Декорация без физики статична; бочка выше сохраняет свою тень в реальном времени. lightmap: true | false на узле переопределяет правило для исключений (анимируемый кодом предмет без физики → false, кинематическая платформа, которая на деле никогда не двигается → true); поддеревья префабов наследуют вердикт обёртки; поддеревья make() — это код, и они регистрируют себя через Lightmap.add. Статичный узел terrain: строится с запечённым материалом террейна и запекается как один прямоугольник. Ключи в запекании — пути узлов (perimeter/metalfence1), так что переименование или перемещение означает перезапекание. Запекание применяется, когда load() построил каждый узел — и именно там его запускает lecodes lightmap bake. Редактор сцен рендерит тени в реальном времени (режим редактирования игнорирует блок) и показывает вердикт переключателем «Baked lighting» на узлах model/mesh.

Рабочий процесс

  1. Дай предметам UV для лайтмапа (один раз на GLB): lecodes assets doctor assets/props --lightmap-uv -o assets/props/. Это добавляет атлас TEXCOORD_1 (xatlas) в каждый файл; скиннед-меши пропускаются — персонаж со скелетом никогда не стоит в одной позе. Добавь флаг в ассет-скрипт проекта, чтобы пересборка его сохраняла.
  2. Пометь уровень — env.lightmap в файле сцены (выше) или Lightmap.add + Model.load { lightmap: true } в коде (ниже).
  3. Запеки: lecodes lightmap bake. Приложение запускается в десктопном хосте; когда уровень заканчивает загрузку, creator-bake трассирует сцену на GPU, и команда пишет в assets/lightmap/ (--out, основа имени файла --name, по умолчанию lightmap): lightmap.bake (раскладка, JSON), lightmap-light.ktx2 (освещённость в люксах, BC6H), lightmap-aux.ktx2 (видимость неба, направленная доля и направление света), lightmap-probes.ktx2 (пробы отражений) и lightmap.lgrid (световая сетка для движущихся). Запекание, которому нужно больше одной страницы атласа, пишет -light_1.ktx2, -aux_1.ktx2, … — перечисли каждую страницу по порядку. Хост пишет файлы KTX2 сам: KTX-Software не нужен. Сначала он создаёт файлы-заглушки, так что уже самой первой компиляции есть что staged-ить. Запеканию нужен десктопный хост, собранный с CUDA toolkit, и карта NVIDIA (LECODES_DESKTOP_EXE для локальной сборки). Приложение, собранное руками, пропускает свои меню, пока Lightmap.baking равен true — титульный экран никогда не доходит до сцены.
  4. Запускай приложение где угодно: запекание применяется или тихо ничего не делает там, где запекания ещё нет или хост старше этой возможности.

Сцены, собранные руками — API

TypeScript
const scene = new Scene()
scene.add(Light.sun())
scene.open()

const main = async () => {
  const crate = await Model.load(asset('./assets/props/crate.glb'), { lightmap: true })
  const ground = Mesh.plane({ material: Material.lit({ color: '#9a9488' }), normal: [0, 1, 0], scale: 40 })
  const holder = new Node()
  holder.add(crate)
  scene.add(holder, ground)
  Lightmap.add(holder)                 // статика: приёмник И окклюдер
  Lightmap.add(ground)                 // Mesh.plane / Mesh.box тоже работают
  const info = await Lightmap.load(scene, {   // после того как статичная сцена построена
    data: asset('./assets/lightmap/lightmap.bake'),
    light: asset('./assets/lightmap/lightmap-light.ktx2'),
    aux: asset('./assets/lightmap/lightmap-aux.ktx2'),
    probes: asset('./assets/lightmap/lightmap-probes.ktx2'),   // опционально
    volume: asset('./assets/lightmap/lightmap.lgrid'),         // опционально
  })
  if (info) console.log(`baked: ${info.applied} of ${info.total} statics, ${info.pages} page(s)`)
}
main()
TypeScript
Lightmap.add(node: Node, key?: string): Node       // Model, Mesh или любой узел, их содержащий
Lightmap.track(node: Node, on = true)              // ДВИЖУЩИЙСЯ, созданный кодом, берёт световую сетку
Lightmap.load(scene, files, options?): Promise<LightmapInfo | null>
Lightmap.baking: boolean                           // true под `lecodes lightmap bake`
Lightmap.moverShadows: boolean                     // тени движущихся от солнца на статиках; по умолчанию true
Lightmap.clear()                                   // забыть каждую регистрацию (пересборка сцены)

files: {
  data: string                  // <stem>.bake
  light: string | string[]      // <stem>-light[_n].ktx2, каждая страница по порядку
  aux: string | string[]        // <stem>-aux[_n].ktx2, страницы как у `light`
  probes?: string               // <stem>-probes.ktx2 — отсутствует: статики отражают только SH неба
  volume?: string               // <stem>.lgrid — отсутствует: движущиеся сохраняют IBL сцены
  direct?, indirect?            // ОТЛАДКА: страницы запекания с `--split`
}

options: {
  lightScale?: number                          // ОТЛАДОЧНЫЙ множитель на люксы атласа (по умолчанию 1)
  emissiveNits?: number                        // ДЛЯ ЗАПЕКАНИЯ: кд/м² эмиссии 1.0 (--emissive-nits важнее)
  emissiveBake?: boolean                       // false = эмиссивные поверхности ничего не освещают (по умолчанию true)
  transmit?: Record<string, number | { through?, diffuse? }>   // ДЛЯ ЗАПЕКАНИЯ: материалы, через которые проходит свет
}

LightmapInfo = { size, texel, pages, applied, total, missing: string[] }   // null = ничего не применено

Файл сцены принимает те же ключи в env.lightmap (файлы и опции в одном объекте).

  • Ключи. Каждой зарегистрированной статике нужно стабильное имя в запекании: по умолчанию name узла плюс порядковый номер в порядке регистрации (container#3); передай key, когда имена нестабильны. Узел, содержащий несколько моделей, получает key/0, key/1, … load сообщает о ключах, присутствующих только с одной стороны — изменение раскладки означает перезапекание. Клоны наследуют { lightmap: true }, так что одного Model.load + clone() на тип предмета достаточно. Об узле, не содержащем ни Model, ни Mesh, выдаётся предупреждение, и он пропускается.
  • load резолвится в null — и уровень рендерится незапечённым — когда запекания ещё нет (файлы-заглушки), файлы нельзя прочитать (предупреждение называет их), у хоста нет поддержки лайтмапов (предупреждение один раз) или приложение запущено под lecodes lightmap bake, где этот вызов и запускает запекание.
  • Что хранят атласы. light = освещённость текселя в люксах — солнце, небо, лампы и их отскоки (мягкие полутени берутся из углового размера солнца и радиуса ламп, а не из размытия); aux = видимость неба, направленная доля и направление света. Статика не получает ничего от солнца или ламп реального времени (движок держит их выключенными для запечённых renderable через световые каналы); движущиеся, стекло и всё загруженное без lightmap: true остаются в реальном времени. Лампа, которая должна освещать статики и вживую (Light.point({ baked: false })), не входит в запекание и ставится на канал статик; запечённая лампа держится тёмной в реальном времени, пока загружена световая сетка — см. Light.
  • Движущиеся берут окружающий свет из световой сетки (volume) в том месте, где они находятся, каким бы ни был их шейдер: файл сцены помечает каждый узел, который двигает аспект Physics или CharacterController, код помечает то, что создаёт, через Lightmap.track (безопасно до загрузки запекания; on = false возвращает поддерево IBL сцены).
  • Тени движущихся на статиках. Запекание, записавшее карту солнца, позволяет движущемуся затемнять запечённую землю под собой (шейдер лайтмапа читает там shadow map реального времени). Lightmap.moverShadows = false выключает это вживую — самый дешёвый уровень освещения: запечённая поверхность тогда не принимает никакой тени реального времени, а движущийся всё ещё затеняет себя и других движущихся.
  • Свет запечён — направление, цвет и интенсивность солнца, небо, лампы и раскладка. Изменил любое из них → перезапекай. Экспозиция остаётся живой.
  • Какие материалы. Лайтмапленные примитивы GLB работают на lit PBR-шейдере, который читает базовый цвет glTF, metal-rough, occlusion, emissive и карты нормалей на UV0 с их множителями; материалы alpha-MASK берут его маскированного близнеца. Материал, которому нужно больше — карта на UV1, трансформации текстур, BLEND (стекло), KHR_materials_unlit, spec-gloss, clearcoat, sheen, transmission, volume, specular — отклоняется со строкой лога [lightmap] …, называющей, чего ему не хватает, и рендерится стандартным шейдером, освещённый в реальном времени, незапечённый — и продолжает отбрасывать тень реального времени поверх той, что хранит атлас.
  • Приёмники Mesh сэмплируют атлас через Geometry.uv1: Mesh.box раскладывает свои шесть граней пропорционально их площадям, Mesh.cylinder разворачивает бок и крышки, собственный uv плоскости уже является чартом; собственная Geometry может задать uv1 сама (отсутствует = uv, так что мешу с масштабированным или тайловым UV он нужен). Lit-материал Mesh-приёмника становится лайтмап-материалом с теми же цветом, картой, шероховатостью и металличностью, когда запекание применяется.

Уровень шейдинга — Material.lightmapShading

TypeScript
Material.lightmapShading = 'lite'    // 'full' (по умолчанию) | 'lite' — до загрузки уровня

Выбор лайтмап-шейдера для всего движка. 'full' — собственный шейдер цели (на десктопе: бикубическая выборка атласа, восемь углов проб, блик солнца на металле); 'lite' — шейдер, который отгружают телефоны (билинейная выборка, один угол пробы, без блика солнца; тени движущихся на статиках остаются) — примерно на 30 % меньше на пиксель на интегрированном GPU, картинка, которую показывает телефон. Он читается при первом создании лайтмап-материала, что Model.load({ lightmap }) делает один раз на мир: задай его при загрузке, как Texture.maxSize — более позднее изменение подхватит следующий уровень. Хост без lite-материала (телефон: его собственный уже этот шейдер) предупреждает один раз и берёт полный.

Отладочные виды

TypeScript
Lightmap.debug(mode: LightmapDebugMode | number, params?: { luxRange?: [lo, hi] })   // 'off' = назад
Lightmap.debugModes: readonly LightmapDebugMode[]  // имена по порядку — для ручки, которая их перебирает
Lightmap.probe(screenX, screenY): LightmapProbe | null   // запечённый тексель под точкой экрана (логические px)
Lightmap.debugProbes(on: boolean, radius = 0.2): number  // зеркальный шар на каждой пробе отражений

debug переключает каждую лайтмапленную поверхность в вид ДАННЫХ на всём уровне: irradiance (одно освещение на белом материале), albedo, normal, shadingNormal, share, skyVis, atlas (шахматка текселей на экземпляр), direction, lux (псевдоцвет по luxRange, log10, по умолчанию [0, 5]), relief, probe, probeMap, skyPath, probePath; direct / indirect привязывают страницы запекания с --split, перечисленные в files. probe отвечает { key, entity, node, material, page, texel, uv1, world, distance } — тексель, который ищется через lecodes lightmap inspect; null, когда под точкой нет ничего запечённого. debugProbes возвращает, сколько шаров он поставил.

Опции запекания (lecodes lightmap bake)

--out assets/lightmap · --name lightmap основа имени файла (одна на запечённый уровень) · --entry main.ts файл, который вызывает Lightmap.load · --size 4096 сторона страницы атласа · --texel 0.05 метров на тексель и --pages 4 страниц заполнить, прежде чем запекание укрупнит тексель · --sun-rays 64 / --sky-rays 2048 / --light-rays 64 / --bounce-rays 128 / --bounces 3 качество · --denoise no-sun денойзер OptiX (none|indirect|no-sun|all) · --sun-angle 0.53 диаметр солнца в градусах (больше = мягче края) · --probe-spacing 4 пробы отражений (0 = нет) · --volume 1 ячейка световой сетки в метрах (0 = нет) · --dry-run только раскладка атласа · --frames 900 запас на загрузку. Полный список с обоснованием каждого флага: lecodes lightmap bake --help; lecodes lightmap stats | diff | inspect читают готовое запекание.

Запекание работает только в десктопном хосте (это инструмент разработки). Сторона потребления определяется по наличию: хост без неё предупреждает один раз (Lightmap: this host has no lightmap support) и рисует уровень незапечённым.

Смотрите также

  • foliage.md — уровень растительности этого семейства: запечённый свет земли на карточках, которые качаются, гнутся и растворяются (Model.load { foliage: true })
  • terrain.md — террейн как приёмник запекания
  • light.md — лампы baked / bakeArea, тени на запечённом уровне
  • scene-files.md — env.lightmap, lightmap: true | false на узле