Lightmap
Запечённое освещение для геометрии, которая никогда не двигается. Запекание один раз, на этапе разработки, трассирует солнце, небо, лампы и их отскоки на каждую статичную поверхность в атласные текстуры в люксах; в рантайме статика берёт весь свой диффузный свет из них. Статичное освещение тогда не стоит ничего за кадр, его тени остаются резкими на любом расстоянии, а shadow map реального времени должна держать только то, что движется (врагов, оружие игрока) — отчего их тени тоже становятся чётче.
Файлы сцен — обычный путь
Уровню, который является файлом сцены, нужен один блок и никакого кода:
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.
Рабочий процесс
- Дай предметам UV для лайтмапа (один раз на GLB):
lecodes assets doctor assets/props --lightmap-uv -o assets/props/. Это добавляет атласTEXCOORD_1(xatlas) в каждый файл; скиннед-меши пропускаются — персонаж со скелетом никогда не стоит в одной позе. Добавь флаг в ассет-скрипт проекта, чтобы пересборка его сохраняла. - Пометь уровень —
env.lightmapв файле сцены (выше) илиLightmap.add+Model.load { lightmap: true }в коде (ниже). - Запеки:
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 — титульный экран никогда не доходит до сцены. - Запускай приложение где угодно: запекание применяется или тихо ничего не делает там, где запекания ещё нет или хост старше этой возможности.
Сцены, собранные руками — API
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()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
Material.lightmapShading = 'lite' // 'full' (по умолчанию) | 'lite' — до загрузки уровняВыбор лайтмап-шейдера для всего движка. 'full' — собственный шейдер цели (на десктопе:
бикубическая выборка атласа, восемь углов проб, блик солнца на металле); 'lite' — шейдер,
который отгружают телефоны (билинейная выборка, один угол пробы, без блика солнца; тени
движущихся на статиках остаются) — примерно на 30 % меньше на пиксель на интегрированном GPU,
картинка, которую показывает телефон. Он читается при первом создании лайтмап-материала, что
Model.load({ lightmap }) делает один раз на мир: задай его при загрузке, как Texture.maxSize —
более позднее изменение подхватит следующий уровень. Хост без lite-материала (телефон: его
собственный уже этот шейдер) предупреждает один раз и берёт полный.
Отладочные виды
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на узле