Прошлый пост, «Дни с седьмого по двенадцатый», закончился 27 октября в 18:52 на версии 0.32.0. К тому моменту пещеру освещала светящаяся в ней порода, свет отражался через объём зондов, а следующей вехой должна была стать поверхность — солнце, небо, дальнее поле до километра, вода, облака и погода. Этот пост доходит до 11 ноября и версии 0.46.0, и ни один план про поверхность за это время так и не начался. Почти две недели ушли на одну задачу: свет в пещере не хотел стоять на месте.

Весь код в этом проекте пишут агенты, а я одобряю планы и смотрю на результат. Мне давно хотелось найти предел современных агентов — место, где без человека им дальше не пройти, сколько бы времени у них ни было. На освещении я в такое место упёрся: эту задачу агенты не могли сдвинуть, пока я сам не сел перед окном и не начал смотреть.

Как и раньше, все кадры и замеры сняты на моём ноутбуке с RTX 3080 Laptop GPU и Ryzen 9; там, где написано Cezanne, речь о встроенном Radeon того же ноутбука. Движок делается вместе с первой игрой, но этот пост — только о движке.

Каким был свет на двенадцатый день

Почти весь свет в пещере статичен: светящаяся руда и кристаллы никуда не двигаются, и вместе с камерой перемещается только налобный фонарь. Рендерер этим никак не пользовался. Каждый кадр проход света заново, для каждого пикселя, случайно выбирал несколько светящихся граней, пускал к каждой теневой луч и получал шумную оценку освещения. Шум успокаивали два механизма:

  • временная история в пикселях экрана, которая смешивала новую оценку с прошлыми кадрами;
  • объём зондов — сетка точек в пространстве, собирающих отражённый свет. Каждый кадр зонд оставлял себе почти всё старое значение и подмешивал немного нового, процента три. Такое смешивание называется экспоненциальным средним.
flowchart TB
rnd["каждый кадр: случайные светящиеся грани, по теневому лучу на каждую"] --> est["шумная оценка на пиксель"]
est --> hist["история в экранных пикселях: смешать с прошлыми кадрами"]
hist --> den["шумоподавление: размыть остаток шума"]
probes["сетка зондов: собрать отражённый свет, каждый кадр оставить около 97% старого"] --> comp
den --> comp["композитинг: то, что вы видите"]
turn["камера повернулась: у новых на экране пикселей истории нет"] -.-> hist

Понадобятся ещё два понятия. Мир хранится чанками, а каждый чанк делится на кирпичи — маленькие кубы вокселей, по которым проходит GPU. Апскейлер FSR 2 рисует кадр в двух третях разрешения и каждый кадр сдвигает камеру на долю пикселя, чтобы восстановить детали. Этот сдвиг называется джиттером, и из-за него края слегка шевелятся от кадра к кадру.

0047: порог пройден, а всё равно плохо

Первым после прошлого поста шёл план 0047, «Свет излучателей стоит на месте». Излучателями я дальше называю светящуюся породу. План удлинил экранную историю для её света с 32 кадров до 256, хранил историю в полном float и сбрасывал её только там, где этот свет действительно изменился.

По замерам план сработал. На неподвижной стене стенд раньше насчитывал 74–78 мерцающих пятен за шаг замера, а теперь 1,50, и свет за убранной пробкой породы устанавливался за один кадр вместо 430. У каждого плана есть порог приёмки — число, до которого он должен дойти, чтобы считаться готовым. Для 0047 порог я выбрал сам, не больше трёх пятен за шаг, и план его прошёл. 29 октября он закрылся версией 0.36.0.

Оставалась фаза с пометкой human, в которой я открываю сборку в окне и смотрю сам. Мой вердикт был такой: «Стало гораздо лучше, но всё равно плохо». С отражённым светом освещение устанавливалось секундами, а на быстрых поворотах и при копании появлялись мелкие яркие точки. Обе проблемы ушли в следующий план. Число у плана было хорошим, а картинка — нет.

29 октября: десять шагов у окна

План 0049, «Свет устанавливается быстро, молодые пиксели без точек», начался со стенда. Прежде чем что-то исправлять, агенты хотели измерить проблему headless, в числах, чтобы потом сравнивать кандидатов между собой. Стенд держал камеру неподвижно в двух ракурсах на уровне, поворачивал её, копал, включал и выключал фонарь и в каждой серии записывал попиксельную статистику по 8192 кадрам.

Стенд подтвердил, что при неподвижной камере свет так и не устанавливается. В ракурсе vein от 65 до 88 процентов всего дрожания картинки приходилось на свет, собранный зондами, а после любого изменения этот свет успокаивался только через 1064–2012 кадров. Про поворот камеры в отчёте было сказано: «Быстрый поворот в этих ракурсах лишнего шума не дал». То есть никаких точек.

В тот же день я провёл у окна сессию из десяти шагов. Оркестрирующий агент подготовил скрипт, который на каждом шаге открывал сборку с нужными флагами, а я записывал, что вижу. Эти записи сохранились дословно:

  • Шаг 1, камера неподвижна: «Изменение очень слабое и мелкое, и я вижу, что всё немного меняется, хотя я ничего не делаю. По-моему, такого быть не должно, когда пользователь и все источники света стоят на месте»
  • Шаг 7, в движении: «Мерцание довольно сильное, когда я двигаю камерой, сначала куча мелких ярких точек, через 5–7 секунд всё уходит»
  • Шаг 9: «Когда фонарь выключен, я успеваю досчитать до пяти, пока свет установится. Когда двигаю камерой, всегда вижу эти яркие точки, и это сильно ломает погружение. Точки со временем уходят, но выглядит это очень плохо»

Я видел точки каждый раз, когда двигал камеру, а стенд поворачивал камеру и не видел ни одной. Агенты так и записали в файл обратной связи: «На повороте стенд дефекта не видит».

Заодно сессия нашла второй дефект. На шагах 3, 5 и 6 я видел тёмные клинья в углах уступов и мелкие яркие треугольники там, где сходятся грани сглаженной поверхности. На шаге 6 отражённый свет был выключен, а треугольники остались, — значит, дело было не в свете, а в самой поверхности. Под них завели отдельный план, 0050, и 31 октября он закрылся водонепроницаемой контурной поверхностью. Стенд считал и треугольники, и точки одним и тем же мерцанием, а мне, чтобы их различить, хватило одного взгляда.

Что глаз разделил 29 октября

Мой скриншот с шага 5 сессии: ракурс patch, фонарь выключен, отражённый свет включён.

Коричневый пол пещеры из гладких граней, в точках, где грани сходятся, — крошечные яркие треугольники, по середине — едва заметные кольцевые полосы
Яркие треугольники в углах граней: это поверхность, а не свет. После плана 0050 их нет

Из архива сессии в репозитории движка, кадрировано.

В тот вечер я ввёл правило на следующие две недели: пока свет не исправлен, больше ничего не двигается, и поверхность, дальнее поле и вода подождут. В поправке к плану я написал: «Хочу сосредоточиться на настоящем исправлении мерцания света; без него двигаться дальше нет смысла».

Порог, который так и не утвердили

Вместе со стендом агенты предложили и пороги. Для неподвижной камеры порог был формулой: с отражённым светом картинка может дрожать не больше чем в 1,25 раза сильнее, чем без него, или максимум на 0,010 больше. Это нормальный инженерный порог, и смысл у него простой: отражённый свет не должен заметно добавлять дрожания к тому, что уже есть.

Моё правило было проще и строже: когда ничего не движется, ничего не меняется. Ноль — или так близко к нулю, что человек, который сидит неподвижно, разницы не видит. Агенты записали в файл обратной связи, что моё правило «строже предложенного» порога и что порог «нужно вывести заново, около нуля», но так его и не вывели. 3 ноября план 0049 сняли, и его пороги остались предложениями, которых я не утвердил.

Предложенный порог всегда отсчитывается от того, что уже есть: лучше, чем было, или ненамного хуже, чем в другом режиме. А достаточно ли «лучше, чем было», — это другой вопрос, и стенд на него ответить не может.

3 ноября: картинка на главной

3 ноября я открыл README движка. Главная картинка там — снимок зала-пещеры, первое, что видит любой, кто заходит в репозиторий. Я увидел на ней артефакты и написал: «Похоже, снимок сделан раньше, чем установился весь свет, — это ровно та проблема, о которой я говорю».

Агенты это проверили. Рендерер неподвижных снимков по умолчанию накапливал 64 кадра, поэтому они отрисовали тот же снимок, накопив 64, 512, 2048 и 8192 кадра, и сравнили каждый пиксель со снимком на 8192. Снимок на 64 кадрах побайтно совпал с картинкой из README, и больше чем у половины его пикселей, у 53,5 процента, разница с эталоном превышала 2 уровня яркости из 255. Хуже того, на 2048 кадрах картинка была не ближе к эталону, чем на 512: примерно у четверти пикселей разница так и оставалась больше двух уровней. В файле обратной связи это записали так: «Перестаёт улучшаться и так и не сходится». Я ответил: «Вот ровно об этом я и говорю».

Эту картинку агенты отрисовали с настройкой по умолчанию и поставили на главную. Никто из них не спросил, хватит ли 64 кадров свету, которому, по их же стенду, нужны тысячи.

Для этого поста я заново отрисовал тот же снимок двумя сборками — от 3 ноября (4bf567f) и сегодняшней — с тем же ракурсом, в 1280 × 720, с выключенным фонарём, экспозицией 8 и видом по умолчанию с FSR 2.

Зал до и после

Снимок hall из витрины, ничего не движется; отрисован для этого поста на RTX 3080 собственным рендерером неподвижных снимков движка.

Пещера с высокими коричневыми колоннами; мелкие белые светящиеся кристаллы на колоннах ничего вокруг не освещают; голубая дальняя стена в шуме и тёмных дырах
До: 4bf567f, 3 ноября, 64 кадра, как в README
Та же пещера; светящиеся кристаллы теперь бросают тёплый свет на колонны вокруг; голубая дальняя стена гладкая, с мягкими тенями
После: 3c0897c, 11 ноября, 2048 кадров

Сборка 3 ноября на 2048 кадрах выглядит как её же снимок на 64, только с меньшим шумом; жёсткие пятна остаются. Команда: hero --source showcase --shot hall --lamp off --exposure 8.

Разницу проще всего увидеть на белых кристаллах на колоннах. До изменений это яркие пятна, а камень вокруг них остаётся тёмным; после каждый кристалл сидит в тёплом ореоле собственного света.

Но один кадр прячет то, на что я жаловался, — то, как картинка продолжает меняться, когда ничего не движется. Поэтому дальше показана попиксельная разница между 64-м и 2048-м кадром каждой сборки, умноженная на 8. Чёрный цвет значит, что ничего не изменилось.

Что ещё меняется, когда ничего не движется

64-й кадр против 2048-го для одного и того же неподвижного снимка, худший из трёх каналов.

Серая картинка разницы, на которой видна вся пещера: дальняя стена яркая от изменений, колонны и пол — средне-серые
До: плывёт вся сцена
Почти чёрная картинка разницы, тонкие яркие линии только по краям и на дальней стене
После: меняются только края

Чтобы убедиться, что оставшиеся края — это джиттер апскейлера, а не свет, я отрисовал обе сборки ещё раз в нативном разрешении без джиттера, а потом ещё и в ракурсе vein на уровне.

Пиксели, которые меняются между 64-м и 2048-м кадром, когда ничего не движется

1280 × 720, RTX 3080. Пиксель считается, если хоть один канал отличается больше чем на 2 уровня из 255.

  • до, 3 ноября
  • после, 11 ноября
Зал, вид по умолчанию: FSR 2 с джиттером 55,3% 10,7%
Зал, нативное разрешение без джиттера 51,8% 0%
Ракурс vein на уровне, вид по умолчанию 37,1% 1%

Измерено для этого поста. В нативном разрешении 64-й и 2048-й кадры новой сборки совпадают побайтно. Средняя разница на пиксель в зале: 5,39 и 1,37 уровня в виде по умолчанию, 5,12 и 0,00 в нативном; у жилы 3,94 и 0,12.

Показать числа
до, 3 ноябряпосле, 11 ноября
Зал, вид по умолчанию: FSR 2 с джиттером 55,3%10,7%
Зал, нативное разрешение без джиттера 51,8%0%
Ракурс vein на уровне, вид по умолчанию 37,1%1%

В виде по умолчанию остаётся только джиттер на силуэтах, и им займётся следующий план, 0061, про сглаживание.

Поворот, которого стенд не отрисовал

Но и неподвижный снимок — не совсем то, на что я смотрел 29 октября: точки я видел, когда камера двигалась. Поэтому для поста я написал одноразовый вариант рендерера, который делает то, чего, судя по всему, не делал поворот стенда. Сначала камера 512 кадров смотрит на 90 градусов в сторону, пока свет не установится, потом за 10 кадров, примерно за шестую долю секунды при 60 кадрах в секунду, поворачивается на зал, и рендерер сохраняет кадры сразу после поворота.

Сразу после быстрого поворота

Левые колонны снимка hall в полном разрешении после поворота на 90° за 10 кадров. Кадр 1 — первый кадр после конца поворота; кадр 1024 — установившийся эталон.

Тёмно-коричневые колонны, усыпанные мелкими яркими оранжевыми и голубыми крапинками; светящиеся кристаллы — плоские белые пятна
До, кадр 1
Те же колонны, ярких крапинок меньше, но они всё ещё рассыпаны по камню
До, кадр 16
Те же колонны после установки, глаже, кристаллы по-прежнему ничего вокруг не освещают
До, кадр 1024
Коричневые колонны с чистыми воксельными ступенями, каждый светящийся кристалл в тёплом ореоле, крапинок нет
После, кадр 1
Тот же вид, почти не отличается от кадра 1
После, кадр 16
Тот же вид после установки, почти не отличается от кадра 1
После, кадр 1024

Отрисовано для этого поста одноразовой копией примера hero из движка, потом удалённой. Новая сборка считает свет для всей витрины заранее, до первого кадра; в окне он досчитывается по ходу, так что часть мира, которую камера ещё не видела, может прийти с опозданием.

Крапинки на левой колонне — те самые яркие точки, которые я видел при каждом движении камеры. Они есть в первом кадре после поворота и никуда не деваются к шестнадцатому.

Насколько каждый кадр после поворота далёк от установившегося

Доля пикселей, отличающихся больше чем на 2 уровня от 1024-го кадра той же сборки, весь кадр 1280 × 720.

  • до, 3 ноября
  • после, 11 ноября
Кадр 1 59,2% 28,9%
Кадр 4 56,8% 24%
Кадр 16 51,3% 12,6%
Кадр 64 38,4% 11,4%

Остаток около 11% у новой сборки — джиттер апскейлера на краях, как и на неподвижном снимке выше.

Показать числа
до, 3 ноябряпосле, 11 ноября
Кадр 1 59,2%28,9%
Кадр 4 56,8%24%
Кадр 16 51,3%12,6%
Кадр 64 38,4%11,4%

Именно такую полосу кадров я предлагал агентам снять 5 ноября — об этом чуть ниже.

Одна и та же ручка

С 27 октября по 5 ноября свет прошёл через шесть планов, с 0047 по 0057. Вот они на одной шкале вместе с тем, чем закончился каждый.

Планы света, с 27 октября по 11 ноября

Каждый план и его итог. Даты — когда началась работа или было принято решение.

  • построено и осталось
  • построено, потом заменено
  • снято
  • мои слова, которые сменили направление
  1. 27–29 окт 0047: история света излучателей на 256 кадров Порог пройден. «Стало гораздо лучше, но всё равно плохо».
  2. 28 окт 0048: убрать смещение от клампа в лучах зондов Так и не построен. Снят 4 ноября, когда зонды перестали нести свет излучателей.
  3. 28 окт – 3 ноя 0049: стенд, потом исправления установки света и точек Стенд построили, исправления — нет. Снят.
  4. 29 окт «Пока свет не исправлен, больше ничего не двигается»
  5. 29 окт – 4 ноя 0051: три эксперимента рядом Выбран кэш в мировом пространстве по граням вокселей; зонды несут только отражённый свет фонаря.
  6. 3–4 ноя 0056: что на самом деле движется в итоговой картинке Большинство скачков между кадрами — джиттер FSR 2. Ограничение яркости излучателей отвергнуто.
  7. 4–5 ноя 0057: таблица по граням как единственное хранилище Стоит на месте; в движении — тёмные пятна повсюду. «Ощущение, что путь неверный».
  8. 5 ноя «Неважно, какая производительность, — сделать то, что выглядит по-настоящему отполированным»
  9. 5–9 ноя 0059: свет считается заранее, по чанкам, при загрузке и при правке «Кажется, качество наконец-то есть!»
  10. 10–11 ноя 0060: очередь расчёта света идёт за взглядом Отмену работы для пролетевших чанков оставили: «без неё всё начинает лагать».

По планам в docs/plans/done/ репозитория движка и их журналам.

Лучше всего эту неделю объясняет диагноз агента-архитектора, с которого начинается план 0051, одобренный 29 октября:

Мерцание — структурное. Всё на экране, кроме налобного фонаря, — статичный свет, а рендерер заново оценивает его со свежими случайными числами каждый кадр и потом хранит оценку там, где она не может устояться: в экранных пикселях, которые теряют её при каждом повороте, и в зондах, смешанных экспоненциальным средним, которое никогда не сходится. «Неподвижно в покое» и «быстро после изменения» — два конца одной ручки, длины истории, и каждый план начиная с 0024 крутил эту ручку.

План 0024 — это план освещения с шестого дня. Длинная история даёт неподвижную картинку, которая медленно реагирует на изменения, а короткая реагирует быстро, но шумит. Целую неделю агенты предлагали, реализовывали, измеряли и закрывали планы, и каждый немного менял длину истории — в другом месте и с другой защитой. Сам по себе каждый план был разумен и проходил свой порог или подходил к нему близко, а следующий снова крутил ту же ручку.

Выходит, увидеть круг агенты могли, но только когда его уже нарисовали, а сойти с него сами не смогли. Данные для этого были и раньше. Стенд 0049 уже показал, что больше всего дрожания даёт свет из зондов, и всё же в списке кандидатов того же плана стоял зонд, который при каждом обновлении оставляет себе 99 процентов старого значения, — то есть ещё более длинная история. В плане 0047 такая настройка отставала от переключения фонаря примерно на 30 секунд.

0051: «первое настоящее улучшение!»

План 0051 искал выход в формате пробы. В нём было три эксперимента, каждый включался своей настройкой; стенд их замерял, а потом их показывали мне:

  • A, сойтись и замереть: зонды и экранная история становятся бегущими средними, которые сходятся и перестают обновляться, пока что-то не изменится.
  • B, кэш в мировом пространстве: свет излучателей копится по граням куба в хэш-таблице с ключом «воксель и грань», и поворот камеры ничего не теряет.
  • C: свет фонаря больше не попадает в зонды.

4 ноября в зале, со всеми тремя включёнными, я написал: «Первое настоящее улучшение! Точек нет». С одним B и выключенным отражённым светом вышло так: «Кажется, мне нравится… но проблема быстрого поворота вернулась, и когда копаю, свету в яме нужно время, чтобы установиться». А на уровне снова была «та же проблема :(».

Стенд точек по-прежнему не видел. В разделе плана про дальнейшие шаги так и написано: «Точки владельца headless не воспроизводятся», а в журнале — «Ничто из этого — не те точки при каждом движении камеры, о которых говорит владелец… В фазе 4 их считают глазами». Фазой 4 был я.

Выбрали вариант B, с выключенным отражённым светом для излучателей, и с тех пор зонды несут только отражённый свет фонаря. Тогда же я написал, что «мы явно делаем что-то не так, и логику всё равно надо переделывать».

0056: пурпурные кляксы

Параллельно план 0056 выяснял, что из движущегося в итоговой картинке на самом деле свет. Оказалось, что большинство крупных скачков между кадрами — это джиттер FSR 2 на силуэтах и ярких излучателях, а жёлтые точки — свет на изломах поверхности.

Одним из кандидатов в этом плане было ограничение яркости излучателей: в оценке у светящейся грани появлялся потолок. Это обычный приём против ярких выбросов, и по числам скачков действительно стало меньше. Но в окне по всей пещере расползлись пурпурные кляксы:

Ограничение яркости излучателей в окне

Мой скриншот из плана 0056, шаг 7: зал с включённым ограничением. Текст HUD обрезан.

Голубая с оранжевым стена пещеры со сталактитами, по камню разбросаны плоские розово-пурпурные кляксы разного размера
Ограничение убрало скачки и добавило кляксы

Я это ограничение отверг: «артефакты на контрастных вокселях».

Тот же план сравнивал два диапазона для дальней нормали шейдинга, то есть расстояния, на котором дальние поверхности переходят на сглаженное освещение. В headless-замерах у диапазона [1, 2] скачков при неподвижной камере было примерно в семь раз больше, чем у [2, 6], но в окне я выбрал именно [1, 2]: «Думаю, дальняя нормаль 1..2 лучше всех». А про медленную панораму, на которой агенты сравнивали варианты, я заметил: «Если камера движется так медленно, свет успевает установиться, но настоящие игроки так медленно не ходят».

0057: неверный путь

План 0057 вырос из варианта B: таблица по граням стала единственным хранилищем света излучателей, без всякой экранной истории, с защитой в мировом пространстве и предзагрузкой граней вокруг камеры. Картинка стояла на месте, и в зале план прошёл свои пороги. А потом я полетел по пещерам:

Таблица по граням в окне

Мой скриншот из плана 0057, шаг 3a: ракурс vein, быстрое движение. Текст HUD обрезан.

Коричневый тоннель пещеры; дальняя стена вокруг светящейся рудной жилы покрыта зернистыми тёмными пятнами там, где свет ещё не заполнился
Грани, увиденные впервые, начинают тёмными и заполняются медленно

Записи из той сессии: «Когда я лечу по пещерам, везде вижу тёмные пятна — свет устанавливается медленно»; «Фонарь вообще ничего не добавляет, картинка настолько плохая, что это не важно»; «В целом ощущение, что путь неверный».

В той же сессии я подсказал агентам, как увидеть это самим: «Предлагаю снять полосу скриншотов после нескольких нажатий B — вы это умеете — и сравнить каждый кадр, проблему увидите сразу». В журнале плана подсказка записана, а сразу за ней стоит пометка «записано, не выполнено». Рендерить кадры и сравнивать их попиксельно агенты умели — двумя днями раньше они сделали ровно это с картинкой из README, — но здесь этого никто не сделал. Подсказка так и осталась в журнале, и полоса кадров выше — первый раз, когда её отрисовали.

К тому моменту я был довольно сильно расстроен и почти потерял надежду.

5 ноября: в другую сторону

В конце сессии по 0057 оркестратор спросил, что делать дальше. Сначала я ответил: «И то и другое: чинить сейчас, проектировать параллельно», а потом вместо вердикта задал вопрос:

А что если пойти по-другому — неважно, какая производительность, но сделать то, что выглядит по-настоящему отполированным, а потом посмотрим, где можно срезать?

Я спросил это, чтобы перестать ходить по кругу. Всё это время мы пытались одновременно получить нужный вид и уложиться в бюджет, и у нас не получалось. Другой путь — идти последовательно: сначала вид, потом бюджет.

Так и был устроен план 0059. Сначала эталон: посчитать свет излучателей для каждой видимой поверхности со сколь угодно большим числом выборок, вообще не думая о бюджете, и посмотреть, тот ли это вид. В фазе 1 расчёт эталона занимал от 94 до 148 секунд до первого кадра, смотря по ракурсу, против 1,6 секунды у таблицы. План рассчитывал, что одна выборка стоит 4,3–4,6 наносекунды, но это число замерили внутри старого прохода света, а в отдельном запуске выборка стоила 36–64. Выпускать такое, конечно, никто бы не стал.

6 ноября я посмотрел на эталон. План спрашивал: «Это тот вид, от которого будем урезать?», и я ответил: «(j) Да, от этого вида и будем урезать». Каким был тот первый вид, я сейчас уже не помню. Помню только, что до этих последних изменений свет мерцал всё время.

Дальше пошли урезания, и каждое я смотрел в отдельной сборке. Общие сиды не прошли свой порог на швах, и их заменили 512 независимыми выборками. Стекло стало фильтром, а кольца чанков сравнили с отдельными кирпичами — победили кирпичи. Для кирпича выбрали ячейки по 8 вокселей и 4096 выборок на направление: «8/4096 нормально, остальные слишком шумные». Появилась живая очередь, которая сначала считает то, что нужно камере, сначала грубо, потом точнее; после копания кольца вокруг пересчитываются, и новый свет проявляется плавно. Часть вариантов по дороге отбросили, потому что они выглядели не так, — ушли и фильтр, и ячейки по граням, и кольца.

8 ноября агенты записали в журнале плана смену процесса: «Для работы над светом, пока в плане 0059 не сделан выбор, проверка глазом — это окно владельца». На исследовательских фазах пороги не замеряли и тесты картинки не писали. 10 ноября это стало общим правилом харнесса — режимом исследования: если план определяется видом, фазы до моего окна идут без тестов картинки, а тесты пишутся уже после выбора, по выбранному виду.

До: вид и бюджет одновременно

flowchart TB
b1["агент пишет план с порогами"] --> b2["линия реализует"]
b2 --> b3["headless-стенд: порог пройден"]
b3 --> b4["ревью при закрытии: чисто"]
b4 --> b5["моё окно: всё равно плохо"]
b5 --> b1

После: сначала вид, потом бюджет

flowchart TB
a1["эталон: без бюджета, сколько угодно выборок"] --> a2["моё окно: это тот вид?"]
a2 --> a3["урезать одно"]
a3 --> a4["моё окно: вид тот же?"]
a4 -- "да" --> a3
a4 -- "выбрано" --> a5["тесты и пороги пишутся по выбранному виду"]

9 ноября я смотрел в окно уже с копанием и записал: «Кажется, качество наконец-то есть! На быстром полёте чуть меняется, но не страшно» и «Копание нормально». В фазе 14 таблицу по граням, предзагрузку и всё, что их питало, удалили, и вышла версия 0.44.0.

Какой свет теперь

Статичный свет больше не оценивается каждый кадр: он считается один раз, когда чанк загружается или меняется, и сохраняется.

flowchart TB
load["чанк загрузился, или копание его изменило"] --> queue["живая очередь: сначала то, что нужно камере, грубо, потом точнее; работу для пролетевших чанков отменяем"]
queue --> store["хранилище по чанкам: каждая ячейка в 8 вокселей хранит свет с шести направлений, 4096 выборок на направление кирпича"]
store --> read["каждый кадр: прочитать хранилище, без случайных чисел"]
lamp["фонарь: считается точно на каждый пиксель"] --> den["шумоподавление: только часть от фонаря"]
probes["зонды: только отражённый свет фонаря"] --> comp
read --> comp["композитинг"]
den --> comp

Если сравнить с первой схемой, для всего неподвижного больше нет случайной оценки на каждый пиксель в каждом кадре, а экранная история для света излучателей пуста. Зонды, которые давали основную часть мерцания, теперь несут только отражённый свет фонаря. Хранилище считается так, будто яркость каждого светящегося блока равна единице, и масштабируется при чтении, поэтому если сделать материал ярче, пересчитывать ничего не придётся. Случайным остаётся только фонарь, потому что он движется, и его шум, как и раньше, проходит через шумоподавление.

29 октября я успевал досчитать до пяти, пока свет установится. Теперь после копания у жилы свет устанавливается за 7 кадров, то есть за 147 миллисекунд, и ни один кадр при этом не длиннее 33 мс. Свет, посчитанный на RTX и на Radeon, совпадает с точностью до 0,204 одного уровня яркости. Весь кадр в покое в зале стал дешевле, а в дальнем ракурсе не изменился: 14,63 против 14,64 мс на RTX.

Весь кадр в покое, в зале

Таблица по граням из плана 0057 против кирпичей с заранее посчитанным светом из плана 0059. Headless, нативное разрешение, фонарь выключен; RTX в 2530 × 1378, Cezanne в 1280 × 720.

  • таблица по граням (0057)
  • заранее посчитанный свет (0059)
RTX 3080 Laptop 27,311 мс 17,031 мс
Cezanne (встроенный Radeon) 91,145 мс 32,739 мс

Из журнала плана 0059, фаза 7a.

Показать числа
таблица по граням (0057)заранее посчитанный свет (0059)
RTX 3080 Laptop 27,311 мс17,031 мс
Cezanne (встроенный Radeon) 91,145 мс32,739 мс

Уложились, правда, не во всё, и план это записывает. Первый кадр должен был появляться за 3 секунды, а в моём окне появлялся за 2,2–11,6; в порог шумоподавления в 3,0 мс тоже не уложились. Кроме того, кирпич, до которого очередь ещё не дошла, по-прежнему рисуется чёрным, и на медленном адаптере это выглядит так: «через несколько секунд дальние пещеры всё ещё чёрные», как я написал 11 ноября. Этим займётся следующий план, 0067: в нём появится сводка света на блок, и кирпич, для которого света ещё нет, будет брать его у блока вокруг, а не оставаться чёрным.

0060: очередь по взгляду

Последний план света за этот период пришёл прямо из моего окна в конце 0059. Я тогда записал: «Вдалеке свет не пересчитывается, пока я не подойду ближе» и «Мы должны уметь отменять это, когда камера пролетела мимо». План 0060 сделал и то и другое: очередь стала упорядочивать работу по тому, куда смотрит камера, и отменять её для чанков, мимо которых камера уже пролетела.

Порядок по взгляду видимой разницы не дал — ни в замерах, ни на мой глаз. А отмена дала, и я написал: «Без неё всё начинает лагать, так что оставляем». Версия 0.45.0 вышла 11 ноября.

Предел

Я искал место, где агенты останавливаются, и в этой работе у него оказалось три стороны.

Они не могли это увидеть. Агенты судят о картинке по числам, которые сами решили посчитать, и по кадрам, которые сами решили отрисовать. Читать изображение модель умеет, но смотрит только на то, что велит отрисовать план, а ни один план не отрисовал быстрый поворот так, как летаю я. Журналы дважды говорят об этом прямо: «headless не воспроизводятся» и «их считают глазами». А полосе кадров выше хватило одного одноразового примера и поворота за 10 кадров — нужно было только, чтобы кто-то решил посмотреть.

Они не могли решить, что хорошо. Каждый порог, который предлагали агенты, отсчитывался от того, что уже есть. Правило «когда ничего не движется, ничего не меняется» решил я, и стенд не мог прийти к нему замерами. Пурпурные кляксы, дальняя нормаль с семикратными скачками, эталон, которому до первого кадра нужно было две с половиной минуты, — каждый раз число и картинка расходились, и решать, кто прав, приходилось мне.

Они ходили по кругу. Шесть планов, каждый разумный, и каждый крутил длину истории. Диагноз агента-архитектора был верным, но появился только после недели работы и после моего правила, что больше ничего не двигается, пока свет не исправлен. А переход от «вид и бюджет одновременно» к «сначала вид, потом бюджет» начался с моего вопроса.

Эти три стороны подпитывают друг друга: не видя дефекта, не оценишь исправление, а не умея оценить исправление, пробуешь следующий вариант того же исправления, потому что каждый вариант улучшает число, которое ты видишь.

При этом всё остальное за эти две недели шло обычным путём. Когда вид был выбран, урезания реализовали быстро, замерили честно, а промахи записали. Исправление поверхности, очередь, темп кадра и стоимость тестов прошли обычный цикл: план, линии, ревью, закрытие. Предел получился узким. Он ровно там, где ответ зависит от человека, который смотрит на движущуюся картинку и решает, правильная ли она.

Харнесс вокруг этого предела уже поменялся. Режим исследования стал правилом: в плане, который определяется видом, моё окно идёт раньше тестов, а тесты пишутся по тому, что я выбрал.

Остальное за две недели

  • Поверхность (0050, 0053, 0054), 31 октября и 3 ноября. Тёмные клинья и яркие треугольники из сессии 29 октября оказались трещинами в контурной поверхности. План 0050 нашёл их пробой и закрыл, и в окне их больше не было. План 0053 пытался вплавить в гладкую поверхность детали шириной в один воксель, но в окне я увидел «ту же проблему». План 0054 построил более гладкую поверхность по полю плотности вокселей; она обходится примерно втрое дороже по времени кадра на GPU и осталась опцией, а видом по умолчанию стали обычные кубы.
  • Чёткость (0055), 3 ноября. Процедурную детализацию поверхности убрали из вида по умолчанию, FSR 2 стал апскейлером по умолчанию («fsr2 остаётся»), и появилась дальняя нормаль шейдинга.
  • Кадр окна (0064), 11 ноября. Окно теперь по умолчанию рисует так быстро, как может, а не по такту композитора. Таймеры и счётчики читаются через кольцевые буферы, так что CPU больше не ждёт GPU, а синхронизация мира получила бюджет времени. Под Hyprland на RTX медиана кадра стала 10,6–13,5 мс вместо 13,0–17,2, а на программном адаптере за 300 кадров блокирующих чтений с GPU стало 0 вместо 608. После этого на уровне я записал: «Картинка нормальная, когда свет и алиасинг устанавливаются, но лагает, fps низкий», так что работы здесь ещё хватает.
  • Стоимость тестов (0066), 10–11 ноября. Тесты дольше 10 секунд переехали в отдельный длинный прогон, и основной гейт тестов теперь идёт 505 секунд при бюджете в 12 минут.
  • Чистка горячих путей (0044), 10–11 ноября, в боковой линии: аудиоколбэк больше не освобождает и не обнуляет ничего большого, плюс мелкие исправления в коде мира, симуляции и сети.
  • Сущности (0062, 0063), набросаны 10 ноября. Воксельные модели из жёстких частей, анимация по ключевым кадрам в арифметике с фиксированной запятой и риги в текстовых файлах. Первые фазы влиты 11 ноября; я подтвердил позу покоя и то, что у рига ось z смотрит вверх.

Что дальше

По свету открыты четыре плана. План 0067 введёт бюджет расчёта света на секунду вместо доли на кадр и уберёт черноту там, где света ещё нет. План 0061 займётся сглаживанием при заранее посчитанном свете, то есть теми краями, которые ещё меняются на картинках разницы выше. План 0065 уберёт лишнюю работу в расчёте света, а 0058 добавит подвижные источники как список аналитических источников света — он одобрен, но ещё не построен. После них придёт очередь сущностей, а потом и вехи с поверхностью, которая должна была начаться две недели назад.

Как это шло

Харнесс остался тем же, что в прошлом посте: оркестрирующая сессия, линии планов, каждая в своём worktree, блокировка GPU, фазы с пометкой human, которые останавливаются на скрипте запуска для моего окна, и свежий агент-архитектор на ревью каждого закрытия. Изменились две вещи. В планах, которые определяются видом, режим исследования сдвинул тесты: теперь они пишутся после моего выбора. А всё, что я говорю у окна, теперь попадает в репозиторий дословно — для света это отдельный файл LIGHT-FEEDBACK.md, — чтобы следующий план мог это прочитать.