Фотобудка для STL/3MF
Что это вообще такое?
Это браузерный просмотрщик 3D-моделей формата STL и 3MF. Закинул файл, покрутил, настроил материал, сделал скриншот, ушёл. Данные никуда не уходят.
Основные фичи:
- 📂 Drag & Drop загрузка (или кнопка, если ты олдскул)
- 🎨 18+ пресетов материалов — от шёлкового PLA до стекла и хрома
- 🌍 HDRI-окружение с кучей карт на выбор
- 💡 8 схем освещения (студийный, кибер, ринглайт и т.д.)
- 🖨️ Симуляция слоёв FDM-печати — да, прямо в рендере!
- 📷 Скриншоты сцены и модели (с обрезкой фона)
- 📐 Отображение габаритов в мм
- 🌐 5 языков (RU, EN, ES, CN, JP)
Под капотом
Для тех, кто любит копаться в кишках — вот что там внутри.
Three.js и модульная архитектура
Вся графика построена на Three.js 0.180.0. Использую ES-модули через import map:
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.180.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.180.0/examples/jsm/"
}
}
</script>Никаких бандлеров, никакого Webpack — просто нативные модули браузера. В 2025-м это работает везде.
Загрузчики моделей
Для STL — стандартный STLLoader. Для 3MF — ThreeMFLoader. Оба файла читаются как ArrayBuffer через FileReader, парсятся на клиенте:
reader.onload = (ev) => {
const buffer = ev.target.result;
if (isSTL) {
const geometry = loaderSTL.parse(buffer);
geometry.center();
geometry.computeVertexNormals();
// ...
} else if (is3MF) {
const group = loader3MF.parse(buffer);
group.rotation.x = -Math.PI / 2; // Потому что 3MF любит Y-up
// ...
}
};3MF-файлы приходят с Y-up ориентацией, поэтому их сразу поворачиваем. STL центрируем и пересчитываем нормали (потому что некоторые экспортёры их криво генерят).
Материал — MeshPhysicalMaterial с допилами
Главный материал — MeshPhysicalMaterial. Это самый навороченный PBR-материал в Three.js:
const material = new THREE.MeshPhysicalMaterial({
color: state.color,
roughness: 0.5,
metalness: 0.0,
clearcoat: 0.1, // Лаковое покрытие
clearcoatRoughness: 0.3,
transmission: 0.0, // Прозрачность (для стекла)
thickness: 0.2, // Толщина (для преломления)
ior: 1.45, // Коэффициент преломления
sheen: 0.0, // Шёлковый отблеск
sheenRoughness: 0.5,
side: THREE.DoubleSide,
});18 пресетов — это просто объекты с заранее накрученными параметрами. Хром — metalness: 1.0, roughness: 0.03. Стекло — transmission: 0.95, ior: 1.52. И так далее.
Симуляция слоёв печати
Киллер фича, которая позволяет придать модели вид как у изделия созданного с помощью FDM принтера. Через onBeforeCompile инжектим кастомный код в шейдер:
material.onBeforeCompile = (shader) => {
// Добавляем униформы
shader.uniforms.uLayerHeight = { value: 0.2 }; // мм
shader.uniforms.uLayerStrength = { value: 0.35 };
shader.uniforms.uLayerActive = { value: false };
// Вершинный шейдер: передаём Y-координату
shader.vertexShader = shader.vertexShader.replace(
"#include <common>",
`#include <common>
varying float vWorldY;`
);
shader.vertexShader = shader.vertexShader.replace(
"#include <worldpos_vertex>",
`#include <worldpos_vertex>
vWorldY = (modelMatrix * vec4(transformed, 1.0)).y;`
);
// Фрагментный шейдер: искажаем нормаль
shader.fragmentShader = shader.fragmentShader.replace(
"#include <normal_fragment_maps>",
`#include <normal_fragment_maps>
if (uLayerActive) {
float k = 6.2831853 / uLayerHeight; // 2π / высота слоя
float dys = cos(vWorldY * k);
// Эффект сильнее на вертикальных поверхностях
float verticalFactor = 1.0 - abs(normal.y);
normal.y += dys * uLayerStrength * verticalFactor;
normal = normalize(normal);
}`
);
};Как это работает: берём Y-координату вершины в мировом пространстве, прогоняем через косинус с периодом равным высоте слоя. Получаем волну. Эту волну добавляем к нормали, имитируя микрорельеф слоёв.
Важный момент — verticalFactor. На горизонтальных поверхностях (крышка модели) слоёв не видно, там normal.y ≈ 1. На вертикальных стенках — видно хорошо.
Постобработка
Голый рендер — это скучно. Поэтому есть пайплайн постобработки:
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
// SAO — Screen-space Ambient Occlusion
const saoPass = new SAOPass(scene, camera, false, true);
saoPass.params = {
saoBias: 0.5,
saoIntensity: 0.03,
saoScale: 50,
saoKernelRadius: 20,
saoBlurRadius: 8,
};
composer.addPass(saoPass);
// FXAA — антиалиасинг
const fxaaPass = new ShaderPass(FXAAShader);
composer.addPass(fxaaPass);
composer.addPass(new OutputPass());SAO добавляет мягкие тени в углублениях — модель сразу выглядит объёмнее. FXAA сглаживает зубчики на краях (потому что antialias: false в рендерере — для скорости).
HDRI и освещение
Для реалистичных отражений используются HDRI-карты (формат .hdr). Загружаем через RGBELoader:
rgbeLoader.load("venice_sunset_1k.hdr", (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture; // Для отражений
scene.background = texture; // Опционально — как фон
});Плюс есть 8 схем освещения — это просто наборы DirectionalLight, SpotLight, PointLight с разными позициями и цветами. “Кибер” режим — это синий ключевой свет + пурпурный обратный. “Ринглайт” — точечный источник спереди + подсветка снизу.
Скриншоты
Две кнопки — “Сцена” и “Модель”. Первая делает скриншот как есть. Вторая — убирает фон, рендерит, и обрезает прозрачные пиксели:
function trimCanvas(canvas) {
const ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Ищем границы непрозрачных пикселей
let minX = w, minY = h, maxX = 0, maxY = 0;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (imageData.data[(y * w + x) * 4 + 3] > 0) { // Альфа > 0
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
// ...
}
}
}
// Вырезаем нужную область
const cutCanvas = document.createElement("canvas");
cutCanvas.width = maxX - minX + padding * 2;
// ...
return cutCanvas;
}Результат копируется в буфер обмена через navigator.clipboard.write(). Если браузер не поддерживает — скачивается как файл.
UI/UX
Дизайн — glassmorphism с полупрозрачными панелями и blur-эффектом:
.ui-panel {
background: rgba(20, 20, 30, 0.75);
backdrop-filter: blur(24px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 20px;
}Анимированные градиентные пятна на фоне через ::before и ::after с filter: blur(100px).
Панель настроек — lil-gui (наследник dat.gui). Лёгкая, минималистичная, хорошо кастомизируется через CSS-переменные.
Адаптив под мобилки — всё перестраивается: нижняя панель становится вертикальной, кнопки увеличиваются под палец (min 44px touch target), тултипы скрываются.
Что ещё
- Z-Up режим — поворачивает модель так, как она будет в слайсере. Полезно для превью перед печатью.
- Габариты — показывает размеры bounding box в мм. Обновляются в реальном времени при вращении модели.
- Автоповорот — для демонстрации или лени.
- Автодетект языка — смотрит
navigator.languageи выбирает подходящий.
Зачем это нужно
- Быстро глянуть модель — без установки софта, без загрузки на сервер.
- Сделать красивый рендер — для соцсетей, документации, продажи модели.
- Показать заказчику — кидаешь ссылку + скриншот, выглядит профессионально.
- Проверить габариты — влезет ли на стол принтера.
Попробовать
Закидывайте свои модели, крутите настройки, делайте скриншоты. Если найдёте баги или есть идеи — пишите на admin@guilliman.ru.
