Меши
Mesh — геометрия одного объекта: вершины, треугольники, границы, флаги материала. Хранится она не «как в учебнике», а упакованной: позиции квантованы и сгруппированы в чанки по ~64 треугольника, поэтому вершину движок читает за 3–6 байт вместо 12–36 у обычных форматов — на ESP32 без PSRAM это и память, и скорость (конвейер вершин каждый кадр тащит в разы меньше байт). Из-за этого же примитивы (Sphere, Box, Torus, …) и модели из конвертера почти ничего не добавляют в heap.
Подсистема разделена на формат данных меша, ресурс-меш с владением, ленивыми границами и кодировщиком build(), а также MeshBuilder для пользовательской геометрии. Старое имя Mesh::Builder осталось его псевдонимом.
Игре обычно нужны три вещи: создать меш, посчитать границы и подключить к сцене через MeshInstance (он сам соберёт трансформ и кэши). Всё остальное — по желанию.
Создание меша
Четыре способа — от простого к ручному:
| Способ | Когда брать |
|---|---|
new Sphere(radius, segments) и остальные примитивы |
Любая стандартная форма |
MeshBuilder |
Геометрия строится кодом: стены, уровни, процедурка |
Mesh::create(verts, …) |
Уже есть готовые массивы Vertex/Face16 |
new Cube(…) / модели из Convert.py |
Данные упакованы заранее, читаются из rodata без heap |
Примитивы: Cube, Box, Pyramid, Octahedron, Plane, Circle, Cylinder, Cone, Sphere, Torus, Capsule, Helix, TrefoilKnot + модели (Teapot, Suzanne, Bunny). У круглых (Circle, Cylinder, Cone) сегменты зажимаются в 3–64, у Sphere — 3 уровня детализации. Cube/Pyramid/Octahedron читаются из flash-данных вообще без heap и построения.
MeshBuilder — меш из кода
Пушите вершины и грани в любых float-координатах, finish() упаковывает всё в меш. Масштаб подгоняется под авторские координаты. Нормали: по умолчанию сглаживание считается само (среднее по смежным граням с учётом площади), а если нужны жёсткие рёбра — задайте нормаль явно методом normal(...) прямо перед вершиной.
| Метод | Описание |
|---|---|
MeshBuilder(hasUV = false, hasNormals = true) |
Нужны ли UV и нормали |
uint16_t vertex(x, y, z, u = 0, v = 0) |
Добавить вершину, получить индекс (при нехватке памяти — 0xFFFF) |
void normal(x, y, z) |
Явная нормаль для следующей vertex(...) — плоские грани, рёбра коробок. Остальные вершины получают авто-сглаживание |
void triangle(a, b, c) |
Треугольник по трём индексам |
void quad(a, b, c, d) |
Квадрат двумя треугольниками: (a,b,c) и (a,c,d) |
uint16_t vertexCount() / faceCount() |
Сколько уже добавлено |
Mesh *finish(worldSize = 0) |
Собрать меш; worldSize > 0 масштабирует его до этого размера поперёк, иначе остаются авторские координаты |
finish() возвращает nullptr, если нечего упаковывать. После finish() билдер обнуляется и готов строить следующий меш — удобно для пулов однотипной геометрии. Готовый меш освобождается Mesh::destroy(mesh), не delete — примитивы и модели удаляются тем же вызовом.
Mesh::create — меш из массивов
Mesh *m = Mesh::create(verts, vertCount, faces, faceCount, /*hasUV=*/true);
Массивы — авторский формат Vertex/Face16 (позиции уже в квантованном int16, см. раздел «Упаковка нормалей»). create сам пакует их в чанки; исходные массивы остаются вашими. Размер в мире задаётся autoScale(size) — «меш размером size поперёк».
Размер и границы
| Метод | Описание |
|---|---|
void autoScale(size) |
Растянуть меш до size поперёк (вызов один раз после создания) |
const Vector3 ¢er() / float radius() |
Центр и радиус охватывающей сферы (считаются лениво, один раз) |
void getBounds(center, radius) |
То же одной функцией |
void getLocalExtents(halfExtents) |
Полуразмеры AABB — для ящиков, коллизий, спавна |
Границы нужны пипикингу, фрустуму и теням — движок берёт их сам; свои считать вручную не надо. Считаются лениво при первом обращении; если хочется убрать даже этот разовый хитч с кадра (например, перед стартом уровня) — вызовите ensureBounds() заранее, это прогрев без побочных эффектов.
Внешний вид и флаги
| Метод | Описание |
|---|---|
setTexture(tex) / getTexture() / isTextured() |
Материал-текстура |
setWantsTexture(true) |
Рисовать «шахматкой», даже если текстуру ещё не назначили |
setCastShadows(false) |
Отключить отбрасывание теней (по умолчанию включено) |
setSingleColorLighting(true) |
Осветить весь меш одним цветом — дёшево, для болванок |
Подмешы
Модель может быть покрашена кусками: numSubMeshes(), subMeshColor(i) — цвет куска, subMeshFaceEnd(i) — где он заканчивается по граням. Заполняется конвертером моделей.
Обход геометрии
mesh->forEachPosition([&](const Vector3 &p) { /* мировые координаты вершины */ });
mesh->forEachFace([&](uint32_t face, uint32_t p0, uint32_t p1, uint32_t p2,
uint32_t a0, uint32_t a1, uint32_t a2) { /* индексы вершин и атрибутов */ });
Для физики (хуллы, фиттинг), запекания и собственных генераторов. Индексы глобальные по всему мешу.
Низкоуровневый доступ (рендер и инструменты)
Упакованный поток открыт, но игре он почти никогда не нужен: чанки (chunkData(), getChunk(i)), грани (faceData()), атрибуты (chunkAttrs(), attrUV(), attrNormal()), декод позиций (chunkPosDecode(), chunkPosition()). Из полезного:
| Метод | Описание |
|---|---|
numFaces() / numVertices() / numChunks() |
Сколько чего в меше |
hasUV() / hasNormals() / hasSubMeshes() |
Что в меше есть |
chunkXform(chunk, qScale, world) + chunkPoint(...) |
Декод позиции, совмещённый с трансформом: 1 раз на чанк, без лишних умножений на вершину. Так рендер получает мировые точки из упакованных данных |
maxChunkPosCount() / maxChunkAttrCount() |
Размер наибольшего чанка — под буферы |
Раскладку MeshChunk/Face задаёт конвертер моделей. Поля должны совпадать побайтно, поэтому после изменения формата переконвертируйте все модели.
Пример
// Пандус — меш из кода, без массивов
MeshBuilder b(/*hasUV=*/false, /*hasNormals=*/true);
uint16_t g0 = b.vertex(-1.0f, 0.0f, -1.0f);
uint16_t g1 = b.vertex( 1.0f, 0.0f, -1.0f);
uint16_t g2 = b.vertex( 1.0f, 0.0f, 1.0f);
uint16_t g3 = b.vertex(-1.0f, 0.0f, 1.0f);
uint16_t t1 = b.vertex( 1.0f, 0.6f, -1.0f); // верх пандуса
uint16_t t2 = b.vertex( 1.0f, 0.6f, 1.0f);
b.quad(g3, g2, g1, g0); // пол
b.quad(g0, g1, t1, t2); // наклонная плоскость (нормали посчитаются сами)
Mesh *ramp = b.finish(); // размеры — как нарисовано (2×2 м)
MeshInstance *inst = new MeshInstance(ramp);
inst->setPosition(4.0f, 0.0f, 2.0f);
g_sceneInstances.push_back(inst); // дальше обычный путь рендера
// ... при выгрузке уровня
Mesh::destroy(ramp);
Инстансы
MeshInstance — один объект в сцене: меш + трансформ (позиция, кватернион-поворот, масштаб) + цвет + свечение + лайтмап. Создаётся new MeshInstance(mesh) и живёт, пока нужен сцене. Трансформ и мировые границы кэшируются с версионированием: пока инстанс стоит на месте, рендер не пересчитывает по нему ничего.
Трансформ
| Метод | Описание |
|---|---|
setPosition(Vector3) / setPosition(x, y, z) |
Поставить в точку |
setRotation(Quaternion) |
Поворот кватернионом |
setEuler(pitchDeg, yawDeg, rollDeg) |
Поворот в градусах: наклон X, рыскание Y, крен Z |
setScale(Vector3) / setScale(x, y, z) / setScale(uniform) |
Масштаб: покомпонентно или равномерно |
setTransform(pos, rot, scl) |
Всё разом |
setWorldMatrix(AffineMatrix) |
Продвинутый: задать матрицу целиком (масштаб извлечётся) |
pos() / getRotation() / getScale() |
Прочитать текущее |
transform() |
Итоговая матрица мира (собирается лениво, кэшируется) |
Сеттеры трансформа всегда помечают кэш грязным, даже если значение не изменилось, — это честная цена «двинул объект».
Внешний вид
| Метод | Описание |
|---|---|
setColor(Color) / color() |
Тинт меша. Повтор той же краски бесплатный — кэш освещения не перестраивается |
setEmissive(enabled, glowColor = WHITE, intensity = 1.2f, shape = kEmissiveShapeAuto) |
Свечение: пятно по размерам меша. shape — значения перечисления GlowShape; kEmissiveShapeAuto (255) — форма подбирается сама |
setEmissiveBoxSize(w, h, d) |
Своя габаритная коробка свечения (иначе берётся из меша) |
isEmissive() |
Будет светиться в этом кадре (флаг + видимость + меш на месте) |
setShadingOverride(mode) |
Свой шейдинг вместо глобального |
setVisible(bool) / isVisible() |
Выключить отрисовку, не удаляя объект |
Свет и тени
| Метод | Описание |
|---|---|
setBlobShadow(true) / getBlobShadow() |
Дешёвая круглая тень-пятнышко вместо честной (честные тени меша отключаются mesh->setCastShadows(false)) |
setLightmap(atlas, uv, rect...), hasLightmap() |
Запечённый лайтмап (заполняет конвертер/запекатель — вручную обычно не нужно) |
setIgnoreBakedProbes(bool) |
Не брать пробный свет из запечки для этого объекта |
Границы
center() и radius() — центр и радиус охватывающей сферы в мире (кэшируются, движок сам гоняет по ним фрустум-куллинг, пикинг и тени). Пересчитываются автоматически при любом движении.
Разбросанные пропсы — PlacementSet
Много однотипных объектов (камни, деревья, мусор) одним проходом: массив Placement по 12 байт (индекс пропа, позиция в int16, yaw, цвет) + список мешей. Заполняется конвертером моделей, рисуется через drawProps. Флаг kPlacementVisible включает точку.
Пример
MeshInstance *crate = new MeshInstance(new Box(1.0f, 1.0f, 1.0f));
crate->setPosition(2.0f, 0.5f, -1.0f);
crate->setEuler(0.0f, 30.0f, 0.0f); // рыскание 30°
crate->setScale(0.8f); // равномерный масштаб
crate->setColor(Color::rgb(180, 120, 60));
crate->setBlobShadow(true);
// мигаем светом окна: тот же цвет повторно — дёшево
crate->setEmissive(frame % 2 == 0, Color::rgb(255, 220, 120), 1.5f);
r.draw(crate);
Память и сотни инстансов
Бюджет одного инстанса: 256 байт на сам объект + ленивые кэши при первом рендере: вершинный блок (6–24 байта на вершину меша — мировой и экранный набор выделяется по мере нужды), 2 байта на грань под кэш освещения, 4 байта на чанк под полосный кэш. Для мелкого пропа это порядка 0.5–1 КБ на инстанс.
Когда нужны сотни однотипных объектов (камни, деревья, мусор, декор) — не плодите MeshInstance, а соберите PlacementSet: 12 байт на объект (позиция в int16, yaw, цвет) + несколько общих мешей. Экономия ~50–100×, и рендер идёт одним проходом. Индивидуальные анимации/свечение на такие толпы всё равно не смотрят.
Дополнительные рычаги: mesh->setCastShadows(false) убирает участие в теневом проходе; у emissive/uniform-пропсов движок и так не хранит мировые вершины (кэш вдвое меньше); setSingleColorLighting(true) на меше упрощает освещение.
Базовые примитивы
Готовые простые формы: создал — обёрнул в инстанс — рисуй. Каждый класс наследует Mesh и работает со всем остальным API как обычный меш: цвет, свечение, тени, текстуры, пикинг — через MeshInstance (см. раздел 13). Все размеры — в мировых единицах, форма центрирована вокруг своего нуля.
Делайте один меш на форму и много инстансов, а не меш на объект: инстанс весит ~256 байт + ленивые кэши, меш — свои данные. Тысяча ящиков из одного Box дешевле тысячи Box.
Формы
| Класс | Конструктор | Что даёт |
|---|---|---|
Cube |
Cube(size = 1) |
Куб size×size×size с UV. Ноль кучи, ноль построения — геометрия лежит во флеше |
Box |
Box(w = 1, h = 1, d = 1) |
Бокс произвольных пропорций с UV и плоскими нормалями по граням |
Pyramid |
Pyramid(size = 1) |
Квадратная пирамида с основанием size и высотой size. Из флеша |
Octahedron |
Octahedron(size = 1) |
Октаэдр с диагональю size. Из флеша |
Plane |
Plane(w = 2, d = 2, subdiv = 1, uvScale = 1) |
Двусторонняя плоскость XZ (смотрит вверх и вниз). subdiv — сетка 1..64 (больше = мельче триангуляция для света/воды), uvScale — сколько раз текстура тайлится по всей поверхности |
Circle |
Circle(radius = 1, segments = 16) |
Двусторонний диск XZ, 3..64 сегментов |
Cylinder |
Cylinder(r = 1, h = 2, segments = 16, uvU = 1, uvV = 1) |
Цилиндр по Y с гладкими боковыми нормалями и плоскими крышками, 3..64 сегментов |
Cone |
Cone(r = 1, h = 2, segments = 16, uvU = 1, uvV = 1) |
Конус по Y, остриём вверх, гладкие бока, плоское дно, 3..64 сегментов |
Параметры uvU/uvV у Cylinder/Cone и uvScale у Plane — множители UV для тайлящихся текстур; по умолчанию текстура ложится ровно один раз.
Пример
Mesh *ground = new Plane(30.0f, 30.0f); // пол 30×30
Mesh *crate = new Box(1.0f, 1.0f, 1.0f); // один меш ящика
Mesh *barrel = new Cylinder(0.4f, 1.0f, 12); // бочка
MeshInstance *floorInst = new MeshInstance(ground);
floorInst->setColor(Color::rgb(70, 90, 60));
MeshInstance *crate1 = new MeshInstance(crate); // два ящика — один меш
crate1->setPosition(-2.0f, 0.5f, 0.0f);
MeshInstance *crate2 = new MeshInstance(crate);
crate2->setPosition(2.0f, 0.5f, 1.0f);
crate2->setEuler(0.0f, 45.0f, 0.0f);
r.draw(floorInst);
r.draw(crate1);
r.draw(crate2);
Производительность
Cube/Pyramid/Octahedron— геометрия в rodata: их «создание» — это присвоение двух полей, кучи они не касаются вообще. Это самые дешёвые меши движка.- Остальные строятся один раз при создании: квантование в int16 + упаковка нормалей в 2 байта. Временные буферы выделяются ровно под сегменты и освобождаются сразу.
- Для референса: цилиндр на 16 сегментов здесь — 64 грани и ~30 уникальных вершин в чанке; «наивная» генерация (каждое кольцо стороны отдельным рядом вершин на каждое деление высоты) раздувает это в ~9 раз по вершинам и ~16 по граням. В Pip3D такого нет — позиции внутри чанка дедуплицируются, боковая поверхность — два кольца независимо от «высоты в сегментах».
- Ничего из этого файла не лежит в IRAM — и правильно: примитивы работают один раз при создании сцены. Горячий (кадровый) путь рендера — конвейер, а не генераторы форм.
Капсула
Capsule — цилиндр с полусферическими шапками, классическая форма для персонажей, пуль, кнопок, фишек. Наследует Mesh и работает со всем API как обычный меш (инстанс, цвет, тени — раздел 13). Нормали гладкие по всей поверхности, UV развёрнуты по длине дуги: шов один, на полюсах текстура не стягивается в точку.
Конструктор
| Параметр | Что задаёт |
|---|---|
radius |
Радиус капсулы |
height |
Полная высота по Y, включая шапки |
segments = 12 |
Деления по окружности (3..64) |
rings = 6 |
Кольца триангуляции на каждую полусферу (1..16) |
height ≤ 2*radius — цилиндрической части нет, получается UV-сфера радиуса radius. Временные буферы выделяются ровно под выбранные segments/rings и освобождаются сразу после постройки; охватывающая сфера записывается готовой (полюса учтены точно).
Пример
Mesh *pawn = new Capsule(0.4f, 1.4f, 12, 6); // корпус пешки
Mesh *orb = new Capsule(0.5f, 1.0f, 16, 8); // height == 2*radius: сфера
MeshInstance *inst = new MeshInstance(pawn);
inst->setPosition(0.0f, 0.7f, 0.0f);
inst->setColor(Color::rgb(220, 180, 90));
r.draw(inst);
Производительность
- На 12 сегментов и 6 кольцах: 180 вершин и 288 граней — против ~675 вершин и 1152 граней у «наивной» генерации с полным набором одинаковых колец на каждое деление высоты цилиндрической части. Меньше вершин — быстрее каждый кадр.
- Позиции внутри чанка дедуплицируются: полюс схлопывается в одну точку, шовный столбец — в позиции основного кольца.
- Построение — один проход упаковки чанков, без повторного хэширования; квантование координат без вызовов libm.
Спираль
Helix — трубка-пружина, свитая по винтовой линии: пружины, спирали ламп, кабели, бархатные прутки. Наследует Mesh, работает со всем API как обычный меш (раздел 13). Трубка идёт по parallel-transport фрейму — сечение не закручивается вокруг оси, и трубка не «сучится» на крутых витках. Оба торца закрыты плоскими крышками.
Конструктор
| Параметр | Что задаёт |
|---|---|
radius |
Радиус винтовой линии (ось спирали — Y) |
height |
Полная высота по Y |
turns |
Число витков |
segments = 48 |
Делений на всю длину пути (3..128) |
tubeSegments = 8 |
Граней в сечении трубки (3..32) |
uvScaleU / uvScaleV |
Тайлы текстуры вдоль пути / вокруг трубки |
tubeRadius = -1 |
Радиус самой трубки; по умолчанию 0.3*radius |
UV: u — вдоль пути, v — по окружности сечения; шов замкнут дублирующим столбцом, текстура ложится без растяжений. Крышки имеют свои нормали и полярные UV.
Пример
Mesh *spring = new Helix(0.35f, 1.8f, 3.0f, 48, 8); // пружина
Mesh *coil = new Helix(0.6f, 0.8f, 5.0f, 64, 10, 1, 1, 0.06f); // плотный кабель
MeshInstance *s = new MeshInstance(spring);
s->setPosition(0.0f, 0.9f, 0.0f);
s->setColor(Color::rgb(190, 195, 205));
r.draw(s);
Производительность
- Дефолтная спираль (3 витка, 48×8) — 784 грани и ~460 вершин, ~7 мс построения один раз при старте сцены. Дальше меш рендерится как обычный чанковый: 13 чанков дают мелкое зерно полосного куллинга — невидимые витки не обрабатываются.
- Тригонометрия пути и сечений — по таблицам (LUT), квантование координат без libm, упаковка меша — один проход.
- Не нужна труба целиком — уменьшайте
segments/tubeSegments: цена построения и рендера растёт пропорционально их произведению.
Сфера
Sphere — равномерная икосфера: треугольники одного размера по всей поверхности (без сплюснутых полюсов широтно-долготной сетки), гладкие нормали. Наследует Mesh, работает со всем API как обычный меш (раздел 13). UV развёрнуты равнопромнуто, шов и полюса разрежены корректно — текстуры ложатся без складок.
Конструктор
| Параметр | Что задаёт |
|---|---|
radius |
Радиус сферы |
quality = 8 |
Уровень детализации — ступени, не сегменты: 1..8 → ~80 граней, 9..16 → ~320, 17+ → ~1280. Промежуточные значения округляются вниз до ступени |
Ступени выбраны не случайно: каждое подразделение икосферы учетверяет число граней, плавной шкалы между ними не существует. Для коллизий/физики хватает низкого уровня, для крупных объектов на экране — среднего; высокий берите точечно.
Пример
Mesh *ball = new Sphere(0.5f, 16); // 320 граней - игровой объект
Mesh *detail = new Sphere(1.2f, 32); // 1280 граней - крупный план
MeshInstance *b = new MeshInstance(ball);
b->setColor(Color::rgb(90, 190, 255));
r.draw(b);
Производительность
- Сетка равномерная: при равном числе граней икосфера покрывает сферу качественнее широтно-долготной (у той полюса в 4-6 раз плотнее экватора) — меньше граней за ту же картинку.
- Построение: подразбиение с хэш-кэшем рёбер (без квадратичного поиска), UV через быстрые аппроксимации
atan2/asin(ошибка ~0.0002 радиана — на порядок ниже шага UV-квантования), один проход упаковки чанков. Служебные буферы — точного размера, стек не используется. - По сравнению с «наивной» UV-сферой на 16 сегментов (561 вершина / 1024 грани, ~20 КБ кучи) — 320 граней и втрое меньше памяти, при том же времени построения.
Тор
Torus — бублик вокруг оси Y: кольца, шины, кольца порталов, зрачки-«пончики». Гладкие нормали по всей поверхности, замкнут по обоим направлениям. Наследует Mesh, работает со всем API как обычный меш (раздел 13).
Конструктор
| Параметр | Что задаёт |
|---|---|
majorRadius |
Радиус осевой окружности (до центра трубки) |
minorRadius |
Радиус самой трубки |
segments = 16 |
Колец вокруг большого круга (3..64) |
tubeSegments = 8 |
Граней в сечении трубки (3..64) |
uvScaleU / uvScaleV |
Тайлы текстуры по большому кругу / вокруг трубки |
UV замкнуты шовной строкой и столбцом — текстура стыкуется без обратной протяжки по обоим кругам.
Пример
Mesh *ring = new Torus(1.0f, 0.3f); // классический бублик
Mesh *donut = new Torus(2.6f, 0.65f, 24, 12); // гладкий крупный тор
MeshInstance *r1 = new MeshInstance(ring);
r1->setPosition(0.0f, 1.2f, 0.0f);
r1->setEuler(0.0f, 0.0f, 0.0f); // лежит плашмя (ось Y)
r1->setColor(Color::rgb(220, 170, 60));
r.draw(r1);
Производительность
- Построение — два LUT-прохода синуса/косинуса (по кольцам и сечениям) и один проход упаковки чанков; квантование без libm.
- Вершин в итоговом меше ровно
segments × tubeSegmentsпозиций: шовные строка и столбец схлопываются дедупликацией в атрибуты, не раздувая геометрию. - Внешний радиус
majorRadius + minorRadiusзаписан в границы при создании — куллинг и пикинг точны без пересчёта.
Узел-трилистник
TrefoilKnot — трубка, свитая по кривой узла-трилистника: декоративные кольца, логотипы, «магические» артефакты. Замкнутый свип без шва скрутки: фрейм переносится параллельным транспортом вокруг всей петли, рассогласование угла на стыке измеряется и размазывается равномерно назад по витку — трубка сходится сама в себя ровно. Наследует Mesh, работает со всем API как обычный меш (раздел 13).
Конструктор
| Параметр | Что задаёт |
|---|---|
scale |
Общий размер (радиус трубки = 0.55*scale, пропорции узла фиксированы) |
segments = 64 |
Делений на всю петлю (3..128). Для чистой геометрии нужно ≥ ~24: кривая узла крутая, при более грубом сэмплинге поверхность самопересекается |
tubeSegments = 12 |
Граней в сечении трубки (3..64) |
uvScaleU / uvScaleV |
Тайлы вдоль петли / вокруг трубки |
UV замкнуты швом по обоим направлениям; стык первого и последнего ряда точный благодаря коррекции закрутки.
Пример
Mesh *knot = new TrefoilKnot(0.5f, 64, 12); // классический узел
MeshInstance *k = new MeshInstance(knot);
k->setPosition(0.0f, 1.0f, 0.0f);
k->setEuler(0.0f, t * 20.0f, 0.0f); // медленно вращаем
k->setColor(Color::rgb(255, 180, 50));
r.draw(k);
Производительность
- Дефолт (64×12, 1536 граней) строится ~14 мс один раз; основная цена — параллельный транспорт фрейма и упаковка чанков, а не тригонометрия.
- Границы точные: самый дальний точки пути — ровно
3*scaleот центра (доказуемо по параметризации), плюс радиус трубки0.55*scale→ сфера3.55*scaleбез запаса. - Не нужен детальный узел на весь экран — снижайте
segments/tubeSegments: цена пропорциональна их произведению.