Simulador web de perfilómetros láser para modelos 3D (digitalización, modelos 3D, piezas industriales...). Emula el proceso real de adquisición 3D: plano láser, ROI, perfiles discretos, oclusiones, movimientos de objeto/sensor y reconstrucción con transformaciones inversas.
Stack: Three.js (WebGL/WebGPU), Vite, Express, YAML declarativo.
- Por qué no basta con convertir una malla
- Inicio rápido
- Cómo funciona la simulación
- Arquitectura
- Configuración YAML
- Intersección y visibilidad
- Interfaz y flujo de trabajo
- Exportación y reconstrucción 3D
- Galería
- Docker
- Desarrollo local
- Servir desde Windows / WSL
- Documentación adicional
| Aspecto | Conversión trivial de malla | Este simulador |
|---|---|---|
| Adquisición | Puntos directos de la geometría | Emulación del perfilómetro (perfil a perfil) |
| Oclusión | No modelada | Superficie visible vs. geometría completa |
| Celda de escaneo | No configurable | Sensores, poses, ROI, movimientos |
| Salida | Nube genérica | Perfiles y reconstrucción orientados a inspección industrial |
Capacidades principales:
- Múltiples sensores con pose, ROI y movimientos propios
- Movimientos de objeto y sensor: rotación/traslación en X, Y, Z; simultáneos o secuenciales
- Transformaciones inversas para reconstrucción 3D en espacio real
- Mallas sin modificar: se colocan en escena tal cual
- Validación automática de configuración YAML
- Exportación combinada y por sensor (CSV, TXT, RAW, ZIP con reconstrucción)
./init.shAbre http://localhost:8123.
init.sh construye la imagen, libera el puerto 8123 si hace falta, arranca el contenedor y monta volúmenes para public/configs (lectura/escritura) y public/models (solo lectura), de modo que cambios en YAML y modelos se reflejan sin reconstruir la imagen.
Configuración de ejemplo:
cp public/configs/simulator.example.yaml public/configs/simulator.yaml- Se cargan sensores y objeto desde
simulator.yaml. - Para cada perfil
i:- Se calcula la pose del objeto según sus movimientos.
- Para cada sensor:
- Se calcula la pose del sensor según sus movimientos.
- Se intersecta el plano láser (ROI trapezoidal) con la malla.
- Se aplica ruido sintético opcional a cada punto capturado (
SensorNoiseService). - Se almacenan los puntos del perfil (por sensor).
- Al finalizar, se restauran las poses iniciales.
Para cada perfil capturado se conoce la pose del objeto en ese instante. La exportación aplica la transformación inversa a cada punto y obtiene su posición en el espacio 3D real.
Detalle matemático completo: assets/RECONSTRUCCION_3D.md.
- Superficie visible (por defecto): filtra por distancia al sensor en cada sector del perfil; solo queda el punto más cercano (oclusión y caras internas descartadas de forma eficiente).
- Rayos X: todas las intersecciones con la geometría, útil para depuración.
En geometrías muy cóncavas pueden quedar residuos puntuales, análogo al ruido impulsional de sensores reales.
Patrón MVVM con separación clara de responsabilidades.
UI (index.html)
→ SimulationViewModel
→ Model: Sensor, PointCloud
→ Services: intersección, transformaciones, carga, exportación, validación
→ ThreeView (solo render 3D)
| Capa | Módulos |
|---|---|
| Model | Sensor.js, PointCloud.js |
| ViewModel | SimulationViewModel.js, commands (Start / Stop / Reset) |
| View | ThreeView.js |
| Services | EdgePlaneIntersectionService, FacePlaneIntersectionService, RaycastingIntersectionService, TransformationService, ScanExportService, ModelLoader, ConfigValidationService, ProfileRenderer2D, SensorNoiseService |
Flujo: controles UI → comandos ViewModel → modelos/servicios → notificación a ThreeView → actualización de escena y canvas 2D de perfiles.
Las mallas cargan un BVH (three-mesh-bvh) para acelerar raycasts de oclusión.
Archivo activo: public/configs/simulator.yaml.
Referencia comentada: public/configs/simulator.example.yaml.
| Sección | Contenido |
|---|---|
scene, camera, lights |
Escena Three.js |
models.object |
Ruta del objeto a escanear |
sensors[] |
id, model, pointsPerProfile, pose, roi, movements, noise (opcional) |
object |
initialPose, movements |
simulation |
intersectionMethod, defaultProfiles, offsetZ, rendererBackend, acquisitionNoise, etc. |
sensors:
- id: 'sensor_1'
model: '/models/gocator.glb'
pointsPerProfile: 1024
pose:
position: [0, 0, 0]
rotation: [0, 0, 0]
roi:
yMax: 0.05
yMin: -0.025
x0: -0.15
x1: -0.135
x2: 0.135
x3: 0.15
movements:
- type: 'rotation'
axis: 'z'
value: 90
duration: 1000
startProfile: 0
object:
initialPose:
position: [0, -0.25, 0]
rotation: [0, 0, 0]
movements:
- type: 'rotation'
axis: 'x'
value: 360
duration: 4096
startProfile: 0ConfigValidationService comprueba campos obligatorios, tipos, ROI trapezoidal válido y parámetros de movimientos. Errores y advertencias en consola del navegador.
Selector en la GUI (persistido en YAML como simulation.intersectionMethod):
| Método | Descripción |
|---|---|
| Edge | Plano-aristas. Rápido, aproximado. |
| Face | Plano-caras con muestreo denso. Más detalle. |
| Raycasting | Rayo por muestra del perfil. Mejor para oclusión/superficie visible. |
Prefiltro de visibilidad (visibilityPrefilterEnabled): en Edge/Face descarta candidatos traseros antes del raycast de oclusión.
Backend de render: Auto (preferir WebGPU), WebGL o WebGPU. Si WebGPU no está disponible, fallback automático a WebGL.
El simulador puede añadir ruido sintético a cada punto capturado, emulando la incertidumbre de sensores reales. Por ahora está implementado el ruido gaussiano; la arquitectura (SensorNoiseService) permite extender a ruido impulsional, estructurado u otras distribuciones.
Tras la intersección geométrica y antes de almacenar/exportar el perfil. El ruido se aplica en coordenadas locales del sensor y el punto resultante se devuelve a coordenadas del mundo.
- Eje Y local (profundidad): incertidumbre típica del perfilómetro láser.
- Ejes X/Z locales: opcionales mediante
stdDev: { x, y, z }.
Global (todos los sensores salvo override):
simulation:
acquisitionNoise:
enabled: true
type: gaussian
stdDev: 0.00005 # 50 µm en profundidad (metros)
seed: 42 # opcional, resultados reproduciblesPor sensor (sobreescribe la configuración global):
sensors:
- id: sensor_1
noise:
enabled: true
stdDev: 0.0001 # 100 µm solo en este sensorEn Modo de Escaneo:
- Ruido gaussiano en adquisición: activar/desactivar.
- σ profundidad (µm): desviación típica en micrómetros (se persiste en
simulator.yaml).
Los valores se guardan en simulation.acquisitionNoise al cambiar los controles.
- Elegir o cargar modelo (GLB, GLTF, OBJ, STL, PLY).
- Ajustar parámetros en YAML o con Recargar Configuración.
- Iniciar / detener / reiniciar escaneo.
- Ver perfiles 1D por sensor (navegación ◀ ▶).
- Guardar Escaneo → ZIP con exportaciones.
- Velocidad 0.1x–5x, loop continuo.
- ROI y sensores se actualizan con el movimiento.
- Modo edición con gizmos (traslación / rotación / escala).
- Selector objeto o sensor.
- Guardar Pose Inicial → escribe en
simulator.yaml(solo pose inicial, no movimientos).
- Wireframe o mesh (solo afecta al render, no a la intersección).
ScanExportService genera un ZIP con:
| Archivo | Contenido |
|---|---|
*_raw.txt |
Perfiles en binario float32 (X, Y, Z local) |
*_data.csv |
Coordenadas desenrolladas con offset Z por perfil |
*_xyz.txt |
Mismo contenido en texto |
*_reconstructed.ply |
Nube 3D con transformaciones inversas |
*_<sensorId>_*.csv/txt |
Perfiles individuales si hay varios sensores |
Script auxiliar post-proceso: scripts/reunravell.py (reajuste de incrementos Z en TXT exportados).
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
|
Imagen multi-stage (Dockerfile): builder (Vite npm run build) + runner (Node Alpine + scripts/server.js en puerto 8123).
./init.shdocker build -f Dockerfile -t 3d-scanner-simulator-image .
docker run -d --name 3d-scanner-simulator-container -p 8123:8123 \
-v "$(pwd)/public/configs:/app/public/configs:rw" \
-v "$(pwd)/public/models:/app/public/models:ro" \
3d-scanner-simulator-imageDetener y eliminar:
./stop.sh
# o
docker stop 3d-scanner-simulator-container && docker rm 3d-scanner-simulator-containerAlternativa rápida (sin volúmenes): ./start.sh.
Requisitos Docker (instalación):
sudo groupadd docker && sudo usermod -aG docker $USER && sudo systemctl restart docker.dockerignore excluye artefactos innecesarios del contexto de build.
| Endpoint | Función |
|---|---|
GET /healthcheck |
Estado del servicio |
GET /configs/* |
YAML sin caché |
GET /api/models |
Lista de modelos en public/models |
POST /api/save-config |
Guardar simulator.yaml |
Requisitos: Node.js 18+, npm.
npm install
npm run dev # http://localhost:8123 (Vite, HMR)
npm run build # producción → dist/
npm run preview # previsualizar build- HMR en HTML, CSS y JS.
public/configs/ypublic/models/se leen del disco; cambios en YAML con Recargar Configuración o recarga de página.- En desarrollo, Vite expone los mismos endpoints de modelos y guardado de config que el servidor de producción.
Alternativa histórica sin Docker: servir desde WSL y exponer el puerto en la LAN de Windows.
Desde CMD como administrador (referencia):
netsh interface portproxy add v4tov4 listenaddress=172.16.102.205 listenport=8123 connectaddress=172.20.209.226 connectport=8123
netsh interface portproxy show all
netsh interface portproxy delete v4tov4 listenaddress=172.16.102.205 listenport=8123Tipo de red actual:
Get-NetConnectionProfilePermitir puerto 8123 solo en red de dominio:
New-NetFirewallRule -DisplayName "Vite 8123 Domain" -Direction Inbound -LocalPort 8123 -Protocol TCP -Action Allow -Profile Domain
New-NetFirewallRule -DisplayName "Block Vite 8123 on Public and Private" -Direction Inbound -LocalPort 8123 -Protocol TCP -Action Block -Profile Public,Private
Start-Service iphlpsvc
Remove-NetFirewallRule -DisplayName "Vite 8123" -ErrorAction SilentlyContinue
Get-NetFirewallRule | Where-Object { $_.DisplayName -like "*8123*" } | Format-Table DisplayName, Enabled, Direction, Action.wslconfig con networking mirrored (referencia):
[network]
networkingMode=mirroredUFW en WSL:
sudo apt install ufw
sudo ufw allow 8123Vite accesible en red:
npm run dev -- --hostRegla permisiva (no recomendada):
New-NetFirewallRule -DisplayName "Allow All Vite 8123" -Direction Inbound -LocalPort 8123 -Protocol TCP -Action Allow -Profile Any
Remove-NetFirewallRule -DisplayName "Allow All Vite 8123"- Reconstrucción 3D: teoría y matemáticas
- TODO / roadmap (e.g. paralelización con Web Workers)
- Múltiples sensores, ROI y movimientos complejos
- Transformaciones inversas y exportación por sensor
- Validación YAML, visualización en tiempo real, edición manual de poses
- Tres servicios de intersección + prefiltro de visibilidad
- Backend WebGPU/WebGL, carga de modelos y API REST de configuración








