Criei um organism para renderizar modelos 3D (GLTF) diretamente em posts MDX usando React Three Fiber. O foco é visualização de peças paramétricas (CAD), não 3D comum — por isso a câmera padrão é ortográfica/isométrica.
Stack
- @react-three/fiber (v8) - Abstração declarativa do ThreeJS para React
- @react-three/drei - Helpers (Bounds, Center, OrbitControls, useGLTF, Environment)
- three (v0.160.0) - Engine 3D base
- lucide-react - Ícones (Lock/Unlock)
- next/dynamic com
ssr: false- R3F não funciona no servidor
Arquitetura do componente
text
components/organisms/
├── Model3D.tsx # Wrapper com dynamic import (SSR disabled)
└── Model3DCanvas.tsx # Implementação real do viewerModel3D.tsx (Wrapper)
Dynamic import sem SSR:
tsx
const Model3DCanvas = dynamic(
() => import('./Model3DCanvas').then((mod) => ({ default: mod.Model3D })),
{ ssr: false }
)Model3DCanvas.tsx
Dois componentes principais:
Model - Renderiza o modelo 3D:
useGLTFpara carregar GLTF- Clona cena e centraliza geometria via bounding box
- Aplica materiais dinamicamente
useFramepara rotação automática
Model3D - UI completa:
- Sistema de abas (wireframe, solid, material)
- Toggle de interatividade (Lock/Unlock)
- Configuração de Canvas
Conceitos técnicos
Centralização do modelo
Modelos GLTF podem ter pivot point em qualquer lugar:
tsx
const clonedScene = useMemo(() => {
const clone = scene.clone()
const box = new THREE.Box3().setFromObject(clone)
const center = box.getCenter(new THREE.Vector3())
clone.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
child.position.sub(center)
}
})
return clone
}, [scene])Sistema de materiais
Factory functions para criar materiais sob demanda:
tsx
const materialPresets: Record<MaterialPreset, ()=> THREE.Material> = {
default: () => new THREE.MeshStandardMaterial({
color: 0xcccccc, metalness: 0.1, roughness: 0.8,
}),
metal: () => new THREE.MeshStandardMaterial({
color: 0x888888, metalness: 0.9, roughness: 0.2,
}),
glass: () => new THREE.MeshPhysicalMaterial({
color: 0xcccccc, transmission: 0.9, thickness: 0.5,
}),
}
Presets de câmera
tsx
const cameraPresets = {
cad: { orthographic: true, position: [5, 5, 5], zoom: 50 },
organic: { orthographic: false, position: [0, 0, 5], fov: 50 },
}Iluminação
tsx
<ambientLight intensity={viewMode= 'wireframe' ? 1 : 0.5} />
<directionalLight position={[10, 10, 5]} intensity={...} />
{viewMode !== 'wireframe' && <Environment preset="city" />}Auto-fit da câmera
tsx
const bounds = useBounds()
useEffect(() => {
bounds.refresh().clip().fit()
}, [bounds, clonedScene])Desafios encontrados
SSR não funciona com R3F
R3F depende de WebGL/canvas (APIs do browser). Solução: next/dynamic com SSR desabilitado.
Incompatibilidade de versões
R3F v9 requer React 19. Com React 18:
json
"@react-three/fiber": "^8",
"@react-three/drei": "^9",
"three": "^0.160.0"Touch conflitando com scroll no mobile
OrbitControls captura touch mesmo desabilitado. Solução: renderização condicional:
tsx
{showControls && interactiveMode && (
<OrbitControls enablePan={false} enableZoom={false} makeDefault />
)}Sistema de abas
3 modos de visualização:
- wireframe - Sempre disponível, wireframe verde (#09E22C)
- solid - Sempre disponível, material
default - material - Só aparece se prop
materialfor passada
tsx
const hasMaterialTab = material && material !== 'default'
const currentMaterial = viewMode === 'material' && material ? material : 'default'Uso no MDX
jsx
<Model3D src="/models/object.gltf" scale={1.5} material="metal" />Props
src(string, required) - Caminho para o arquivo GLTFscale(number, default: 1) - Escala do modeloheight(string, default: "400px") - Altura do viewerautoRotate(boolean, default: true) - Rotação automáticamaterial(MaterialPreset) - Preset de material (habilita aba extra)camera(CameraPreset, default: "cad") - Preset de câmeradefaultMode(ViewMode, default: "wireframe") - Modo inicial
Material presets
default, metal, plastic, glass, gold, copper
Camera presets
cad- Ortográfica/isométrica (peças paramétricas)organic- Perspectiva (objetos orgânicos)