Як згенерувати 3D-модель телефону в браузері
Інструкція: 3D-модель телефону в браузері з Three.js і CSG. Корпус із bevel, вирізи для кнопок і динаміків, Web Worker і швидке віднімання всіх отворів.

Щоб згенерувати 3D-модель телефону в браузері, витягніть прямокутник із заокругленими кутами на глибину пристрою, а потім булевими операціями (CSG) виріжте передню панель, кнопки, динаміки й роз'єм. Увесь пристрій тоді описують кілька чисел у міліметрах, і нова модель означає зміну цих чисел, а не моделювання в 3D-програмі.
Це корисно всюди, де потрібно багато схожих пристроїв або де їх має налаштовувати користувач: конфігуратори, мокапи, прев'ю продуктів. Приклад із практики: Ecranify, інструмент для 3D-мокапів для скриншотів в App Store і Google Play, у якому немає жодної готової 3D-моделі. Нижче покроковий процес разом із вимірюванням, яке показує, де на обчисленнях можна заощадити найбільше.
Що вам знадобиться
- Three.js для сцени, камер, світла і передусім геометрії. Геометрія в Three.js це mesh, тобто набір трикутників.
- Бібліотека three-bvh-csg для булевих операцій над mesh. Вона пришвидшує їх просторовим індексом (BVH, bounding volume hierarchy), тому не перевіряє кожен трикутник проти кожного.
- Опис пристрою як дані, наприклад JSON із розмірами корпусу, радіусом кутів, розміром bevel і списком елементів на боках. Усі позиції задавайте в міліметрах від центру пристрою.
Крок 1: Витягніть прямокутник у 3D
Намалюйте 2D-прямокутник із заокругленими кутами в реальному розмірі, наприклад 163,4 × 78 мм, і витягніть його (екструзія) на глибину 8,75 мм. Отримаєте пластину з гострими гранями. Таких телефонів не буває, тому під час екструзії увімкніть ще й bevel, тобто заокруглення ребра. Three.js робить обидва кроки за один раз.


Зверніть увагу: bevel додається назовні. Якщо потрібен точний зовнішній розмір, зменште 2D-форму та глибину на розмір bevel.
b = min(bevel, глибина / 2)
форма = заокруглений прямокутник(ширина - 2b, висота - 2b, радіус кута - b)
корпус = витягни(форма, глибина - 2b, bevel b)
Гладкість дуг (тесселяцію) визначає кількість сегментів, на які ділиться дуга. Більше сегментів означає гладшу форму й більше трикутників. Це число згодом визначить швидкість усього обчислення, тож ми повернемося до нього в кроці 7.
Крок 2: Виріжте заглиблення для дисплея
CSG (constructive solid geometry) складає тіла з простіших тіл за допомогою трьох операцій: об'єднання, різниця й перетин. Різниця працює як свердло: одне тіло забирає матеріал з іншого.
Дисплей телефону не врівень із рамкою, він лежить у неглибокому заглибленні. Щоб його зробити, створіть другу копію корпусу, меншу на 1,4 мм, залиште з неї лише передню частину завтовшки 0,7 мм (перетин із великим паралелепіпедом) і відніміть цю частину від корпусу. Потім покладіть у заглиблення темну площину дисплея.


Дві практичні деталі:
- Паралелепіпед для перетину має бути справді великим. Йому достатньо обрізати глибину, але за шириною й висотою він має перевищувати весь пристрій. Не виводьте його з глибини: наприклад, 20-кратна глибина для тонкого високого телефону менша за його висоту, і вгорі та внизу залишиться необрізана рамка.
- Збільште різальне тіло на кілька проміле. Коли два тіла мають точно однакову грань, CSG не може визначити, яка сторона всередині. Масштаб 1,002 цьому запобігає, і оком його не видно.
Крок 3: Підготуйте бічні елементи як свердла
Кожен елемент на боці робиться за тим самим рецептом з іншим свердлом. Кнопка, наприклад, складається з трьох тіл: заокруглений прямокутник, трохи глибший за кнопку, віднімається від рамки, край отвору отримує невелику фаску, а в отвір вставляється трохи менше тіло, яке злегка виступає з рамки.


Динаміки це ряд маленьких циліндрів, роз'єм це заокруглений прямокутник. У кожен отвір вставте щось темне: тонкий диск у решітку, пластинку в роз'єм. Порожня дірка в тонкій оболонці виглядає як помилка, дірка з темним заповненням виглядає як залізо.

для кожного елемента в списку бічних елементів:
свердло = форма елемента (циліндр, заокруглений прямокутник, ...)
розмісти свердло на ребрі за позицією в мм
додай свердло до списку вирізів
якщо елемент має заповнення: додай заповнення до сцени
Додати кнопку тоді означає додати запис у дані. Елементів швидко більшає: iPad Pro 13″ має 58 отворів динаміка, 4 кнопки, 2 антени й роз'єм, тобто понад 60 віднімань від одного корпусу.
Крок 4: Не свердліть передні деталі
Виріз камери, острівець у верхній частині дисплея й індикатор домашнього екрана пласкі. Різати для них геометрію марна робота. Нанесіть їх як decal, тобто зображення, спроєктоване на поверхню. Проєктуйте його на приховану площину на всю передню панель і задавайте позицію в тих самих міліметрах, що й усе інше. Виняток: кругла кнопка «Додому» старших iPhone. Вона має справжню глибину, яку видно збоку, тому проходить через CSG.
Крок 5: Перенесіть обчислення у Web Worker
JavaScript у браузері працює в одному потоці. Той самий потік рахує, малює сторінку й реагує на кліки. Поки працює CSG, сторінка зависає, і spinner не допоможе, бо йому потрібен той самий потік.
Тому запускайте всю збірку пристрою у Web Worker, тобто в окремому фоновому потоці. Worker отримує опис пристрою, повертає готові масиви вершин, і сторінка лишається плавною. Коли параметр змінюється, завершіть старий worker і запустіть новий, щоб він не дораховував застарілий стан.
Результат із worker з'являється не відразу, тому додайте миттєве прев'ю. Показуйте елемент, який редагується, як синього напівпрозорого «привида»: простий паралелепіпед або циліндр із кількома десятками трикутників, який можна збудувати в головному потоці одразу. Коли worker закінчить, привид зникне.
Крок 6: Відніміть усі отвори одразу
Найпряміший спосіб: віднімати елементи по черзі, отвір 1 від корпусу, потім отвір 2 від результату й так далі. Але кожне віднімання заново будує просторовий індекс усього корпусу, а це найдорожча частина обчислення. З десятками елементів час росте дуже швидко.
Краще спершу зібрати всі свердла для даного тіла, об'єднати їх в один mesh і відняти один раз.
вирізи = []
для кожного елемента: додай його свердло до вирізи
об'єднаний = об'єднай усі вирізи в один mesh
корпус = корпус мінус об'єднаний
якщо об'єднання не вдалося: віднімай вирізи по одному
Вимірювання на спрощеній копії процесу (Three.js 0.170, three-bvh-csg 0.0.17, Node 22, хмарний сервер із 2 ядрами Intel Xeon 2,1 GHz, медіана з 5 запусків):
| Тіло | Отвори | По одному | Одразу |
|---|---|---|---|
| телефон, 17 676 трикутників | 25 | 2,47 с | 0,26 с |
| планшет, 17 676 трикутників | 58 | 5,55 с | 0,89 с |
| планшет, 68 108 трикутників | 58 | 22,8 с | 3,1 с |
Одна умова: об'єднання й одне віднімання дає той самий результат, що й послідовне віднімання, лише якщо свердла не перекриваються. Елементи розташовані в різних місцях, тож зазвичай це так. Але якщо користувач посуне два елементи один на одного, у місці перекриття може з'явитися дрібний дефект.
Крок 7: Тримайте кількість сегментів як параметр
Тесселяція, якої достатньо для показу, для CSG часто надто густа. З 64 сегментами на кут і 32 на bevel корпус має 68 108 трикутників. Половини (32 і 16) на ребрі 2 мм не видно, але корпус має лише 17 676 трикутників, і третій рядок таблиці показує, що це робить із часом.


Дрібним елементам із радіусом до 2 мм (отвори динаміка, гвинти) дайте половину сегментів, мініатюрам у галереї ще менше. Тому не задавайте якість як константу в коді, а робіть її параметром збірки.
Рендер на сервері без відеокарти
Якщо ту саму сцену потрібно рендерити й на сервері, наприклад як резерв для експорту, вона працює в Chrome без екрана (headless), який без відеокарти малює програмно. Там кожна неефективність множиться. В Ecranify найважчий планшет без описаних змін упирався в ліміт 15 хвилин. Зі спільними вирізами, меншими текстурами, нижчою тесселяцією й cache готової геометрії за hash опису пристрою рендер триває близько пів хвилини, а повторний близько 5 секунд. Подробиці з інтерактивними прикладами є в англомовній статті на ecranify.com.
Якщо ви розв'язуєте схожу задачу з 3D у браузері або потрібен застосунок на замовлення, напишіть нам.


