Перейти к содержимому

Как клиент рисует

Меняется. Следите: #16, #330, #337, #344, #506

Эта страница - карта для контрибьютора, который хочет читать код рендерера. Она в нескольких строках объясняет идею каждой части и отправляет к заметкам, где лежат правила и числа, поэтому сами заметки не пересказывает.

Рисуют два крейта. mcrs_minecraft_render рисует ландшафт и небо, а mcrs_minecraft_entity_render - мобов. Рендерер относится к слою 6 в карте крейтов, поэтому он работает с данными нижних слоев и не содержит сетевого, клиентского или серверного кода.

Ландшафт лежит в нескольких больших буферах GPU

Заголовок раздела «Ландшафт лежит в нескольких больших буферах GPU»

Отдельного буфера на каждый чанк у рендерера нет. Он записывает меши в несколько больших буферов GPU, их называют аренами. Арен три, и в них лежат квады, модели и грани. Аллокатор упаковывает блоки к нулевому смещению, а арена растет шагами и только до самого старшего занятого байта. На 96 колонках арены заполнены примерно на 50% от 4,3 ГБ, то есть 768 МБ квадов, 2,5 ГБ моделей и 1 ГБ граней, а весь ландшафт рисуется 7 вызовами отрисовки. Числа взяты из раздела Render distance в PERF.md и сняты на M4 Max.

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

Отсечение (culling) отбрасывает геометрию, которую камера не видит. В mcrs этим занимается вычислительный проход на GPU, а CPU в отсечении не участвует. Проход проверяет каждую группу квадов относительно вида и сам записывает аргументы отрисовки. На 96 колонках отсечение занимает на GPU от 0,46 до 0,49 мс в сценариях PERF.md, которые сняты на M4 Max с macOS в 2560x1440, а частоту GPU удерживает MCRS_GPU_HOT=16384.

Окклюзия проверяется по глубине прошлого кадра

Заголовок раздела «Окклюзия проверяется по глубине прошлого кадра»

Группа может попадать в вид и все равно прятаться за холмом. В DECISIONS.md измерено, как часто это случается: на 96 колонках 7,96 млн из 15,3 млн нарисованных треугольников были за ландшафтом прошлого кадра. Тест окклюзии убирает такие треугольники. Он работает по пирамиде глубины, построенной из прошлого кадра, и идет в два прохода.

Первое отсечение скрывает то, что скрывает старая пирамида, и оставляет кандидатами группы, которые скрыл только этот тест. Затем рендерер перестраивает пирамиду из глубины текущего кадра, и второе отсечение возвращает кандидатов, которые все-таки видны, во второй набор вызовов отрисовки. Благодаря второму проходу ничего не появляется с опозданием на кадр.

Проход окклюзии стоит времени, даже когда рисовать нечего, поэтому он запускается, только когда в памяти есть группы. Его можно выключить через MCRS_OCCLUSION=0 и увидеть, что он дает. На 96 колонках он снижает кадр GPU с 6,0 мс до 3,7 мс.

Дизайн в docs/entity-render.md рассчитан на 100 000 мобов в одном мире без просадки кадра. Это цель заметки, измеренного результата за ней пока нет. В заметке есть раздел про стресс-сцены и пороги, а числа для каждого размера лежат в docs/entity-render-budget.md.

ECS хранит состояние каждого моба, а GPU держит проекцию этого состояния в постоянном слоте. CPU пишет в слот, только когда что-то меняется, то есть приходит пакет, проходит тик или меняется ввод скрипта. Остальное делает GPU, причем только для видимых мобов: он отсекает, считает позы, расширяет грани и рисует. Позы задает Molang, язык выражений, на котором написаны определения клиентских сущностей, а mcrs_molang_wgsl переводит каналы Molang в шейдерный код WGSL.

Заметка описывает три пути выполнения, и рендерер выбирает один из них при старте по возможностям графического адаптера.

Путь Что ему нужно
Full Compute-шейдеры и vertex storage
Compatibility Режим совместимости WebGPU
Minimal Без compute, для WebGL2

Сама веб-сборка до устойчивого кадра не доходит.4

Далекому мобу не нужен полный риг, то есть набор костей, который задает позу модели. Для каждой пары из модели и текстуры рендерер запекает небольшой набор видов. В наборе 16 поворотов по yaw и 9 углов возвышения по 64 на 64 текселя, и он занимает около 4,7 МБ. Далекий моб после этого рисуется как импостор, то есть одним квадом, который показывает ближайший вид. Набор запекается лениво, когда модель и текстура впервые появляются на активном мобе. У полных ригов есть лимит, а у импосторов его нет. Заметка фиксирует цену каждого шага и решения, которые не помогли.

Тема Файл
Почему рендерер работает так, с числами DECISIONS.md
Время кадра, память и методика PERF.md
Дизайн рендеринга сущностей docs/entity-render.md
Модель нагрузки и бюджет сущностей docs/entity-render-budget.md
Что клиент умеет сегодня Клиент сегодня

Запланированы еще две работы: ограничение памяти на 96 колонках и рендеринг внутри приложения-хоста, которому принадлежат окно и камера.56

  1. В работе#16 Первая задача перезапуска рендерера: раскладки, объявленные один раз для Rust и WGSL. ↩

  2. В работе#37 Последняя задача перезапуска рендерера: тонированные оверлеи сводятся в один спрайт. ↩

  3. В работе#330 Отслеживает сравнение отложенного рендерера с базовой линией до перестройки. ↩

  4. В работе#344 Отслеживает веб-клиент, который доходит до устойчивого кадра. ↩

  5. В работе#337 Отслеживает ограничение памяти клиента на 96 колонках. ↩

  6. В работе#506 Отслеживает рендеринг, встроенный в приложение-хост, которому принадлежат окно и камера. ↩

Источники и проверка

Проверено: 26.4-snapshot-7, 51d45c26a, 2026-11-09

Переведено с версии 51d45c26a

Следить: #16, #330, #337, #344, #506