tech explorers, welcome!

Categoría: 3D

Soporte para mando PS5

Simplicidad en su máxima expresión.

Hoy comparto este simple pero efectivo y elegante soporte para mando de PS5.

Algunas imágenes/diseños de concepto:

Se sostiene por sí solo sin tornillos ni pegamento. Se cuelga directamente del lateral de la PS5 y es expandible por diseño (probablemente quepan hasta 4 mandos en cada lado).

Algunas fotos reales:

Y, por último, los .stl listos para imprimir:

https://theroamingworkshop.cloud/demos/PS5-DualSense-Holder-A_v1.stl

https://theroamingworkshop.cloud/demos/PS5-DualSense-Holder-B_v1.stl

🎅 Feliz Navidad! 🎁

También disponible en Cults3D como descarga gratuita 💌

Batman con su pistola gancho colgando de tu salón

La verdad es que imprimir con resina es otro nivel. A pesar de las complicaciones de la limpieza, el resultado es realmente increíble, y hoy te enseño mi primera impresión con la Anycubic Photon Mono X2: un Batman para colgar de tu salón con su pistola gancho.

Modelos

La impresión que ves consta de dos modelos gratuitos. Por ello, mi enorme agradecimiento a los autores y mi referencia a su trabajo:

Lo único que he hecho yo es añadirle un esqueleto al modelo y darle la postura deseada, uniéndole luego la pistola a la mano. Asique aquí tienes mi modelo para que lo puedas imprimir tal y como se muestra en las imágenes.

Vista previa

Extras

Para completar el modelo, deberás crearle una capa y un gancho o pinza con el cual colgarlo.

Capa

Para la capa yo he recortado un trozo de tela de una prenda de deporte antigua. Solemos tener prendas de este estilo en color negro y suelen tener también una buena textura y brillo que dan el pego.

Empieza cortando un rectángulo y luego dale forma.

En la parte superior, enrolla un alambre o un clip que te permita ajustar la capa alrededor del cuello de la figura.

Gancho

Simplemente busca un enganche que te venga bien para colgarlo. Yo he usado una pinza a la cual he atado un fino hilo negro, lo que me permite enrollarlo sobre sí para ajustar la altura. De esta forma lo puedo colgar de algún libro de una estantería.

Y así tienes tu genial Batman colgando de tu salón. Espero te guste! Otras ideas o comentarios? Al twitter!

🐦 @RoamingWorkshop

El impacto de un incendio forestal en 3D con información geográfica en Cesium JS

Tras varios artículos sobre el uso de información satélite vamos a ver cómo unirlo (casi) todo en un ejemplo práctico e impactante. Y es que el tamaño de los últimos incendios ocurridos en España han llamado mucho mi atención y no era capaz de hacerme una idea de lo brutales que han sido (aunque nada que ver con otros sitios como Chile, Australia, o EEUU). Pero hagamos el ejercicio sin gastar demasiados GB de información geográfica.

Lo que quiero es mostrar la extensión del incendio ocurrido en Asturias en marzo, pero quiero también intentar mostrar el impacto retirando los árboles afectados por el fuego. ¡Vamos allá!

Descarga de datos

Usaré un Modelo Digital de Superficies (que incluye árboles y estructuras), una ortofoto tomada durante el incendio, y un Modelo Digital del Terreno (al que han eliminado los árboles y las estructuras) para reemplazarlo por las zonas afectadas por el incendio.

1. Modelos del terreno

Usaré los modelos del genial IGN, descargando los productos MDS5 y MDT5 de la zona.

http://centrodedescargas.cnig.es/CentroDescargas/index.jsp

2. Ortofotos

Para la imagen satélite, finalmente me decanto por Landsat ya que contaba con una imagen despejada durante los últimos días del incendio.

Usaré las imágenes tomadas el día 17 de febrero de 2023 (antes del incendio) y del 6 de abril de 2023 (ya en sus últimos días).

https://search.earthdata.nasa.gov

Procesar imágenes satélite

Usaremos el proceso i.group de GRASS en QGIS para agrupar las distintas bandas capturadas por el satélite en un único ráster RGB, tal y como vimos en este post:

https://theroamingworkshop.cloud/b/1725/procesar-imagenes-satelite-de-landsat-o-sentinel-2-en-qgis/

Tendremos que hacerlo para cada una de las regiones descargadas, cuatro en mi caso, que luego volveremos a unir usando el proceso Construir ráster virtual.

1. Imagen de color verdadero (TCI)

Combinamos las bandas 4, 3, 2.

2. Imagen de falso color

Combinamos las bandas 5, 4, 3.

3. Ajuste de la tonalidad

Para obtener un mejor resultado, puedes regular los valores mínimos y máximos que se consideran en cada banda que compone la imagen. Estos valores se encuentran en el Histograma de las propiedades de la capa.

Aquí te dejo los valores que yo he usado para obtener el resultado de arriba:

BandaTCI minTCI maxFC minFC max
1 Rojo-1001500-504000
2 Verde01500-1002000
3 Azul-10120001200

Extensión del incendio

Como ves, la imagen en falso color nos muestra claramente la extensión del incendio. Con ella, vamos a generar un polígono que delimite el alcance del incendio.

Primero consultaremos los valores de la banda 1 (rojo) que ofrece mayor contraste para la zona del incendio. Más o menos están en el rango 300-1300.

Usando el proceso Reclasificar por tabla, asignaremos el valor 1 a las celdas dentro del rango, y el valor 0 al resto.

Vectorizamos el resultado con el proceso Poligonizar y, contrastando con la imagen satélite, seleccionamos aquellos polígonos que correspondan con el incendio.

Usaremos la herramienta Disolver para unir todos los polígonos en un elemento, y Suavizar para redondear ligeramente los contornos.

Ahora obtenemos su inverso. Extraemos la extensión de la capa Landsat y, posteriormente, hacemos la Diferencia con el polígono del incendio.

Procesar terreno

1. Combinar los datos del terreno

Lo primero que haremos es combinar los distintos archivos que conforman los modelos en un archivo único (un único fichero MDS y un único fichero MDT).

Usamos el proceso GDAL - Miscelánea ráster - Construir ráster virtual

2. Extraer datos del terreno

Extraemos los datos que nos interesan de cada modelo:

  • Del MDS extraemos la superficie afectada por el incendio, de modo que quitaremos los árboles que hayan en él.
  • Con el MDT hacemos lo inverso: dejamos el terreno (sin árboles) de la zona del incendio, para sustituir los huecos generados en el otro modelo.

Usaremos el proceso Cortar ráster por capa de máscara empleando las capas generadas en el apartado anterior.

Finalmente unimos ambas capas ráster, para que rellenen una a la otra, usando Construir ráster virtual.

Dale vida con Cesium JS

Ya deberíamos tener un modelo de superficies sin árboles en la zona del incendio, pero vamos a intentar verlo de forma interactiva.

Ya mostré un ejemplo parecido, usando un Modelo Digital del Terreno personalizado, así como una imagen satélite reciente, del volcán Tajogaite de La Palma:

https://theroamingworkshop.cloud/b/1319/cesiumjs-el-visor-gratuito-de-mapas-en-3d-para-tu-web/

En este caso volveré a usar Cesium JS para poder interactuar fácilmente con el mapa (sigue el post anterior para ver cómo subir tus ficheros personalizados al visor Cesium JS).

Para esta ocasión he creado una pantalla dividida (usando dos instancias de CesiumJS) para poder comparar el antes y el después del incendio. Aquí tienes una vista previa:

https://theroamingworkshop.cloud/demos/cesiumJSmirror/

Espero que te guste! Aquí tienes el código completo y el enlace a github para que puedas descargarlo. Y recuerda, comparte tus dudas o comentarios en twitter!

🐦 @RoamingWorkshop

<html lang="en">
<head>
<meta charset="utf-8">
<title>Cesium JS mirror v1.0</title>
<script src="https://cesium.com/downloads/cesiumjs/releases/1.96/Build/Cesium/Cesium.js"></script>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.96/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>
<body style="margin:0;width:100vw;height:100vh;display:flex;flex-direction:row;font-family:Arial">
<div style="height:100%;width:50%;" id="cesiumContainer1">
	<span style="display:block;position:absolute;z-Index:1001;top:0;background-color: rgba(0, 0, 0, 0.5);color:darkorange;padding:13px">06/04/2023</span>
</div>
<div style="height:100%;width:50%;background-color:black;" id="cesiumContainer2">
	<span style="display:block;position:absolute;z-Index:1001;top:0;background-color: rgba(0, 0, 0, 0.5);color:darkorange;padding:13px">17/02/2023</span>
	<span style="display:block;position:absolute;z-Index:1001;bottom:10%;right:0;background-color: rgba(0, 0, 0, 0.5);color:white;padding:13px;font-size:14px;user-select:none;">
		<b><u>Cesium JS mirror v1.0</u></b><br>
		· Use the <b>left panel</b> to control the camera<br>
		· <b>Click+drag</b> to move the position<br>
		· <b>Control+drag</b> to rotate camera<br>
		· <b>Scroll</b> to zoom in/out<br>
		<span><a style="color:darkorange" href="https://theroamingworkshop.cloud" target="_blank">© The Roaming Workshop <span id="y"></span></a></span>
    </span>
</div>
<script>
	// INSERT ACCESS TOKEN
    // Your access token can be found at: https://cesium.com/ion/tokens.
    // Replace `your_access_token` with your Cesium ion access token.

    Cesium.Ion.defaultAccessToken = 'your_access_token';

	// Invoke LEFT view
    // Initialize the Cesium Viewer in the HTML element with the `cesiumContainer` ID.

    const viewerL = new Cesium.Viewer('cesiumContainer1', {
		terrainProvider: new Cesium.CesiumTerrainProvider({
			url: Cesium.IonResource.fromAssetId(1640615),//get your asset ID from "My Assets" menu
		}),	  
		baseLayerPicker: false,
		infoBox: false,
    });    

	// Add Landsat imagery
	const layerL = viewerL.imageryLayers.addImageryProvider(
	  new Cesium.IonImageryProvider({ assetId: 1640455 })//get your asset ID from "My Assets" menu
	);
	
	// Hide bottom widgets
	viewerL.timeline.container.style.visibility = "hidden";
	viewerL.animation.container.style.visibility = "hidden";

    // Fly the camera at the given longitude, latitude, and height.
    viewerL.camera.flyTo({
      destination : Cesium.Cartesian3.fromDegrees(-6.7200, 43.175, 6000),
      orientation : {
        heading : Cesium.Math.toRadians(15.0),
        pitch : Cesium.Math.toRadians(-20.0),
      }
    });
    
    // Invoke RIGHT view

    const viewerR = new Cesium.Viewer('cesiumContainer2', {
		terrainProvider: new Cesium.CesiumTerrainProvider({
			url: Cesium.IonResource.fromAssetId(1640502),//get your asset ID from "My Assets" menu
		}),	  
		baseLayerPicker: false,
		infoBox: false,
    });    

	// Add Landsat imagery
	const layerR = viewerR.imageryLayers.addImageryProvider(
	  new Cesium.IonImageryProvider({ assetId: 1640977 })
	);
	
	// Hide bottom widgets
	viewerR.timeline.container.style.visibility = "hidden";
	viewerR.animation.container.style.visibility = "hidden";

    // Fly the camera at the given longitude, latitude, and height.
    viewerR.camera.flyTo({
      destination : Cesium.Cartesian3.fromDegrees(-6.7200, 43.175, 6000),
      orientation : {
        heading : Cesium.Math.toRadians(15.0),
        pitch : Cesium.Math.toRadians(-20.0),
      }
    });
    
    // Invoke camera tracker
    //define a loop
    var camInterval=setInterval(function(){

	},200);
    clearInterval(camInterval);
    
    document.onmousedown=trackCam();
    document.ondragstart=trackCam();
    
    //define loop function (read properties from left camera and copy to right camera)
    function trackCam(){
		camInterval=setInterval(function(){
			viewerR.camera.setView({
				destination: Cesium.Cartesian3.fromElements(
					  viewerL.camera.position.x,
					  viewerL.camera.position.y,
					  viewerL.camera.position.z
					),
				orientation: {
					direction : new Cesium.Cartesian3(
						viewerL.camera.direction.x,
						viewerL.camera.direction.y,
						viewerL.camera.direction.z),
					up : new Cesium.Cartesian3(
						viewerL.camera.up.x,
						viewerL.camera.up.y,
						viewerL.camera.up.z)
				},
			});
		},50);
	};
	//stop loop listeners (release mouse or stop scroll)
	document.onmouseup=function(){
		clearInterval(camInterval);
	};
	document.ondragend=function(){
		clearInterval(camInterval);
	};
	
	//keep the copyright date updated
	var y=new Date(Date.now());
	document.getElementById("y").innerHTML=y.getFullYear();
  </script>
</div>
</html>

Procesa datos LIDAR en QGIS y crea tu propio Modelo Digital del Terreno

Hay veces que un Modelo Digital del Terreno (MDT) se queda corto o no está muy limpio. Si tienes acceso a datos LIDAR, puedes generar tú mismo un modelo del terreno y sacarle más partido obteniendo mayor detalle de las zonas que te interesen. Vamos a ver cómo.

1. Descarga de datos

Nube de puntos

Voy a usar los magníficos datos públicos del Instituto Geográfico Nacional (IGN, España) obtenidos mediante vuelos que utilizan técnicas de medición láser (LIDAR).

  1. Accede a los datos LIDAR del Centro de Descargas del IGN.

http://centrodedescargas.cnig.es/CentroDescargas/index.jsp

  1. Dibuja un polígono de la zona de interés.
  1. Descarga los archivos PNOA-XXXX...XXXX-RGB.LAZ. RGB emplea color verdadero; ICR, infrarrojo. Pero ambos son válidos.

TIP! Descarga todos los ficheros a la vez utilizando el fichero el applet del IGN. Es un fichero .jnlp que requiere JAVA instalado en Windows o IcedTea en Linux (sudo apt-get install icedtea-netx)

2. Procesado de nube de puntos LIDAR en QGIS

Visualización directa

Desde las últimas versiones (p.e.: 3.28 LTR Firenze), QGIS permite visualizar directamente los ficheros de nube de puntos.

En el menú Capa -> Añadir capa... -> Añadir capa de nube de puntos...

Se mostrarán los datos que hemos descargado en color verdadero, que podemos clasificar en las Propiedades de Simbología, eligiendo la representación de Clasificación por tipo de datos:

Vista 3D

Otra función que trae ahora QGIS por defecto es la visualización de la información en 3D.

Vamos a configurar las propiedades 3D de la capa LIDAR para triangular las superficies y obtener un mejor resultado.

Ahora creamos una nueva vista en el menú Ver -> Vistas del Mapa 3D -> Nueva vista del mapa 3D. Con SHIFT+Arrastrar rotaremos la vista en perspectiva.

Complemento de LAStools

Para utilizar la información fácilmente usaremos las herramientas del plugin LAStools, que instalaremos de la siguiente manera:

TIP! En Linux es recomendable tener instalado Wine para trabajar con los ficheros .exe o será necesario compilar los binarios.

  1. Accede a la web de LAStools y navega a la parte inferior:

https://lastools.github.io/

  1. La herramienta completa es de pago, pero puedes acceder a la descarga pública para utilizar las funciones básicas que necesitamos.
  1. Descomprime el fichero .zip en una carpeta sencilla (sin espacios ni caracteres especiales)
  1. Ahora abre QGIS, busca el complemento LAStools e instálalo.
  1. Por último, configura la ruta de instalación de LAStools (si es distinta de su valor por defecto C:/ ). La configuración mostrada abajo sirve para Linux con Wine instalado (en mi caso uso PlayOnLinux).

Extraer tipos de datos LIDAR

Con LAStools podemos extraer información de los distintos tipos de datos que componen la nube de puntos. Por ejemplo, vamos a extraer solo los datos clasificados como Suelo (que se corresponden con el valor 2).

Con el proceso las2las_filter podremos crear una nueva nube de puntos filtrada:

  • Selecciona el fichero a filtrar.
  • En filter, elige la opción keep_class 2 para conservar solo el tipo de datos 2 (suelo)
  • Deja lo demás por defecto, e introducir 0 en los campos que requieren un valor value (de lo contrario devolverá un error).
  • Por último, guarda el fichero con formato .laz en una ubicación conocida para encontrarlo fácilmente.

Al finalizar solo tendrás que cargar el fichero generado para ver la nueva nube de puntos con valores exclusivamente del terreno (edificios y vegetación eliminados).

Conversión de LIDAR a vectorial

Ahora usaremos el proceso las2shp para transformar la nube de puntos a formato vectorial y poder operar normalmente con otras herramientas de GIS:

  • Elige el fichero de nube de puntos filtrado anteriormente.
  • Especifica 1 punto por registro para extraer todos los puntos de la nube.
  • Guarda el fichero con formato .shp en una ubicación conocida para encontrarlo fácilmente.

Y esta será tu nube de puntos filtrada en el formato clásico vectorial.

Como verás, la tabla de atributos no cuenta con ningún campo específico. Yo voy a crear un campo ELEV para guardar las coordenadas Z (o cota) y utilizarlas para generar un Modelo Digital del Terreno a continuación.

3. Creación del Modelo Digital del Terreno

Raster a partir de capa de puntos vectorial

Gracias a la integración de GRASS GIS, disponemos de potentes herramientas de procesado vectorial y ráster. Vamos a usar v.surf.idw para generar una malla regular a partir de la interpolación de los datos de una capa de puntos (en este caso se ponderan los valores obtenidos mediante el inverso de la distancia, pero también hay algoritmos para emplear splines).

  • Seleccionamos la capa vectorial de puntos.
  • Elegimos el número de puntos para emplear en la interpolación (en este caso los puntos son bastante densos así que elijo 50). Cuantos más elijas, más suavizado será el resultado, pero perderás el detalle de la densidad de la información.
  • Dejamos la potencia del inverso de la distancia en 2, para emplear el "inverso de la distancia al cuadrado".
  • Seleccionamos el campo de datos que usará la interpolación (ELEV).
  • Definimos el tamaño de celda de la malla. Elijo 2 para poder comparar el resultado con el producto MDT 2 metros del IGN.

4. Resultado

Vamos a quitar zoom para ver cómo ha quedado todo:

Y ahora veamos un poco más el detalle.

Aplicamos la misma rampa de color al MDT que hemos generado y al producto descargado del IGN para comparar el resultado obtenido. En general es muy bueno, con algunas diferencias en zonas arboladas, siendo más razonable el resultado de nuestro procesado.

¡Y eso es todo! Cualquier duda o comentario lo puedes dejar en Twitter!

🐦 @RoamingWorkshop

Instalar UnityHub en Ubuntu 22

Si has actualizado a Ubuntu 22 y has intentado instalar UnityHub siguiendo los pasos de su web:

https://docs.unity3d.com/hub/manual/InstallHub.html#install-hub-linux

Todo va aparentemente bien, hasta que al ejecutar el programa te aparece esto:

>> unityhub
This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason:
ConnectionLost: Timeout! Cannot connect to Licensing Client within 60000ms
    at Function.fromType (/opt/unityhub/resources/app.asar/node_modules/@licensing/licensing-sdk/lib/core/ipc/licensingIpc.js:51:16)
    ...

Por suerte, surfeando por la web sueles encontrar la solución, y en el propio forum de Unity han dado con una:

https://forum.unity.com/threads/installing-unity-hub-on-ubuntu-22-04.1271816/#post-8136473

Veamos toda la instalación paso a paso:

Instalar UnityHub para Linux

Seguimos los pasos oficiales de su web (primer enlace del post):

  1. Añade el repositorio de Unity a la lista de fuentes de paquetes de Linux:
    sudo sh -c 'echo "deb https://hub.unity3d.com/linux/repos/deb stable main" > /etc/apt/sources.list.d/unityhub.list'
  2. Añade la clave pública para hacerlo confiable:
    wget -qO - https://hub.unity3d.com/linux/keys/public | sudo apt-key add -
  3. Actualiza tus repositorios:
    sudo apt update
  4. Instala UnityHub:
    sudo apt-get install unityhub

Todo debería ir bien, a pesar de un error al abrir una supuesta carpeta «chrome-sandbox». Aun así, ese no es el error, al ejecutar unityhub desde el terminal nos aparece el error de arriba.

Instalar libssl1.1

El problema está en que Ubuntu 22 usa una versión más reciente del paquete libssl, pero podemos descargar la versión utilizada por Ubuntu 20.

  1. Accedemos a la página de paquetes de Ubuntu 20, donde encontramos libssl1.1
    https://packages.ubuntu.com/focal/amd64/libssl1.1/download
  2. Hacemos click derecho -> guardar como… sobre el enlace al fichero que empieza por security.ubuntu.com/ubuntu… (o haz click al enlace de abajo; descargarás el paquete instalador en formato .deb)
    http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2.16_amd64.deb
  3. Hacemos doble-click en el fichero descargado e instalamos el paquete.
  4. Ahora ejecutamos unityhub en el terminal y todo listo!

🐦 @RoamingWorkshop

Three.js: Visor de modelos 3D para tu web

Estaba preparando un artículo donde quería insertar un modelo 3D para ilustrarlo mejor, y pensaba incluso en hacer un visor yo mismo. Pero no tuve que surfear mucho para encontrarme con Three.js.

https://threejs.org/

¡Si es que está ya todo inventado!

Enlazar la librería CDN

Para este ejemplo, haremos un visor "portable" enlazando las librerías al CDN oficial, en lugar de tener que descargarnos los ficheros a nuestro servidor.

De esta forma, el archivo de ejemplo te servirá en cualquier lugar con conexión a internet. Vamos a crear un fichero .html básico como el que nos sugieren en la documentación:

https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>My first three.js app</title>
		<style>
			body { margin: 0; }
		</style>
	</head>
	<body>
        <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>

        <script type="importmap">
          {
            "imports": {
              "three": "https://unpkg.com/[email protected]/build/three.module.js"
            }
          }
        </script>

        <script>
        //App code goes here
        </script>
	</body>
</html>

Crear una escena

Vamos a seguir con el ejemplo y rellenamos el segundo bloque <script> definiendo una escena con un cubo animado en rotación:

<script type="module">
        import * as THREE from 'three';
	const scene = new THREE.Scene();
	const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

	const renderer = new THREE.WebGLRenderer();
	renderer.setSize( window.innerWidth, window.innerHeight );
	document.body.appendChild( renderer.domElement );

	const geometry = new THREE.BoxGeometry( 1, 1, 1 );
	const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
	const cube = new THREE.Mesh( geometry, material );
	scene.add( cube );

	camera.position.z = 5;

	function animate() {
		requestAnimationFrame( animate );

		cube.rotation.x += 0.01;
		cube.rotation.y += 0.01;

		renderer.render( scene, camera );
	};

	animate();
</script>

Todo eso, junto, queda así:

Añade controles de arrastre y un fondo

Ahora tenemos una base para trabajar. Puede añadir más funcionalidad insertando el módulo OrbitControls que maneja la rotación del modelo y de la cámara.

//Importa nuevos módulos al principio del script
import { OrbitControls } from 'https://unpkg.com/[email protected]/examples/jsm/controls/OrbitControls.js';

//luego, añade los controles del cursor después de declarar la cámara y el renderizador
const controls = new OrbitControls( camera, renderer.domElement );

También puedes modificar el fondo fácilmente, pero necesitas hospedar la imagen junto a la aplicación en un servidor, o ejecutarlo localmente, debido al CORS. Yo usaré la imagen de la cabecera del blog, que saqué de Stellarium.

Primero, define una textura. Luego, añádela a la escena:

//añade esto antes de renderizar, mientras defines la escena
//define la textura
const texture = new THREE.TextureLoader().load( "https://theroamingworkshop.cloud/demos/Unity1-north.png" );

//añade la textura a la escena
scene.background=texture;

Código completo:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>My first three.js app</title>
		<style>
			body { margin: 0; }
		</style>
	</head>
	<body>
        <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>

        <script type="importmap">
          {
            "imports": {
              "three": "https://unpkg.com/[email protected]/build/three.module.js"
            }
          }
        </script>
<body style="margin: 0; width:100%;height:300px;">
        <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>

        <script type="importmap">
          {
            "imports": {
              "three": "https://unpkg.com/[email protected]/build/three.module.js"
            }
          }
        </script>

    <script type="module">

    import * as THREE from 'three';
	import { OrbitControls } from 'https://unpkg.com/[email protected]/examples/jsm/controls/OrbitControls.js';

	const scene = new THREE.Scene();
    
	const texture = new THREE.TextureLoader().load( "https://theroamingworkshop.cloud/demos/Unity1-north.png" );
	scene.background=texture;

    const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

	const renderer = new THREE.WebGLRenderer();
	renderer.setSize( window.innerWidth, window.innerHeight );
	document.body.appendChild( renderer.domElement );
    
    const controls = new OrbitControls( camera, renderer.domElement );

	const geometry = new THREE.BoxGeometry( 1, 1, 1 );
	const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
	const cube = new THREE.Mesh( geometry, material );
	scene.add( cube );

	camera.position.z = 5;

	function animate() {
		requestAnimationFrame( animate );

		cube.rotation.x += 0.01;
		cube.rotation.y += 0.01;

		renderer.render( scene, camera );
	};

	animate();
</script>
</body>
</html>

Insertar un modelo 3D

Ahora vamos a sustituir este cubo por un modelo 3D propio, que en el caso de Three.js, debe tener un formato glTF (.GLB o .GLTF), que es el formato más soportado y que renderiza más rápidamente (aunque también hay soporte para .fbx, .stl, .obj y demás).

Yo exportaré a .glb esta carcasa básica de Raspberry Pi 4B que hice hace un tiempo usando Blender:

Ahora, para insertar el modelo sustituimos el bloque <script> anterior basándonos en el ejemplo "webgl_loader_gltf" que se ve al inicio del post:

<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders/GLTFLoader.js';
import { OrbitControls } from 'https://unpkg.com/[email protected]/examples/jsm/controls/OrbitControls.js';

let camera, scene, renderer;

init();
render();

function init() {

	const container = document.createElement( 'div' );
	document.body.appendChild( container );

	camera = new THREE.PerspectiveCamera( 30, window.innerWidth / window.innerHeight, 0.1, 20 );
    camera.position.set( 0.2, 0.2, 0.2 );

	scene = new THREE.Scene();        
    scene.add( new THREE.AmbientLight( 0xffffff, 0.75 ) );

	const dirLight = new THREE.DirectionalLight( 0xffffff, 1 );
	dirLight.position.set( 5, 10, 7.5 );
	dirLight.castShadow = true;
	dirLight.shadow.camera.right = 2;
	dirLight.shadow.camera.left = - 2;
	dirLight.shadow.camera.top	= 2;
	dirLight.shadow.camera.bottom = - 2;
	dirLight.shadow.mapSize.width = 1024;
	dirLight.shadow.mapSize.height = 1024;
	scene.add( dirLight );

    //model
     const loader = new GLTFLoader();
	 loader.load( 'https://theroamingworkshop.cloud/threeJS/models/rPi4case/rPi4_case_v1.glb', function ( gltf ) {
		scene.add( gltf.scene );
		render();
	 } );

	renderer = new THREE.WebGLRenderer( { antialias: true } );
            
	renderer.setPixelRatio( window.devicePixelRatio );
	renderer.setSize( window.innerWidth, window.innerHeight );
	renderer.toneMapping = THREE.ACESFilmicToneMapping;
	renderer.toneMappingExposure = 1;
	renderer.outputEncoding = THREE.sRGBEncoding;
	container.appendChild( renderer.domElement );

	const controls = new OrbitControls( camera, renderer.domElement );
	controls.addEventListener( 'change', render );
    controls.minDistance = 0.001;
	controls.maxDistance = 1;
	controls.target.set( 0.03, 0.01, -0.01 );
	controls.update();
	window.addEventListener( 'resize', onWindowResize );
}
function onWindowResize() {
	camera.aspect = window.innerWidth / window.innerHeight;
	camera.updateProjectionMatrix();
	renderer.setSize( window.innerWidth, window.innerHeight );
	render();
}
function render() {
	renderer.render( scene, camera );
}
</script>

Básicamente se hace lo siguiente:

  • Importar módulos a usar:
    • GLTFLoader cargará nuestro modelo en formato .glb
    • OrbitControls nos permite controlar la vista de la cámara
  • Definir la escena:
    • definir una cámara
    • definir la luz (en este caso hay luz ambiente y direccional, prueba a comentar alguna de ellas y verás la diferencia)
  • Cargar el modelo en la escena
  • Definir los parámetros de renderizado y renderizar.

Y todo ello queda así (clicka y arrastra!):

Espero que te sea útil! Dudas o comentarios al 🐦 Twitter!

🐦 @RoamingWorkshop

BlenderGIS: modelado 3D de información geográfica en Blender

Blender es (para mí), el programa de modelado 3D gratuito por excelencia. Tiene una amplísima comunidad, muchísima documentación, tutoriales y, sobre todo, continuas actualizaciones y mejoras.

Uno de sus plugins más útiles es BlenderGIS, con el que podremos volcar datos geográficos, georreferenciados o no, para poder modelar con ellos.

Vamos a ver un ejemplo de uso para un modelo de elevaciones.

Instalación

Lo primero que debemos hacer es descargar e instalar Blender desde su fuente oficial (su página web) o desde la tienda de aplicaciones de nuestro sistema operativo:

https://www.blender.org/download/

Ahora descargaremos BlenderGIS desde el github de su autor en su formato .zip comprimido:

https://github.com/domlysz/BlenderGIS

A continuación ejecutamos Blender y abrimos los ajustes de Add-ons (Edit > Preferences > Add-ons).

Pulsamos en "Install..." y seleccionamos el fichero .zip de BlenderGIS.

Ahora podremos buscarlo y activarlo.

Verás que ahora aparece el menú "GIS" en la barra superior de Blender.

Descargar información geográfica

En este ejemplo yo voy a usar un Modelo Digital del Terreno en formato ASCII (.asc), ya que es uno de los formatos de trabajo de BlenderGIS y también el formato estándar de descarga.

Si la información la descargas en otro formato como .tiff o .xyz, lo podrás convertir usando algún programa de escritorio como QGIS o ArcGIS.

MDT

En mi caso, usaré el MDT200 del IGN español, un modelo con paso de malla (o tamaño de celdas) de 200 metros, y es que quiero representar una zona bastante amplia que incluye la provincia de Álava.

https://centrodedescargas.cnig.es/CentroDescargas/index.jsp

Ortofoto

Además, podemos usar una ortofoto como textura del terreno. Para ello me ayudaré de QGIS, y cargaré el servicio WMS del PNOA (también del IGN) para recortar la imagen satélite al gusto.

Cargada la ortofoto, la exportaremos como imagen renderizada para la extensión de nuestro MDT, y con tamaño de celda de 20 metros (la ortofoto admite hasta unos 20cm de tamaño de celda, pero el archivo sería ya excesivamente grande, siendo la imagen de 20 metros de 140MB).

TIP! Una forma de optimizar el detalle es generar "teselas", o una cuadrícula de menor tamaño, pero mayor resolución.

Modelado en Blender

Así estaría todo listo para trabajar en Blender.

Usando el menú "GIS", importamos la capa como malla ASCII. Verás que en seguida aparecerá el modelo en pantalla.

TIP! Este modelo está centrado en el origen de coordenadas, pero se podría georeferenciar estableciendo su CRS en las propiedades de "Georeference".

Ahora añadimos la textura satélite:

  1. Creamos un nuevo material.
  1. Creamos una nueva textura y cargamos la imagen satélite.
  1. Ahora nos movemos a la pestaña de UV Editing:
    • Selecciona la capa de terreno en la ventana derecha, entra en Edit Mode, y "Seleccionar todos" los polígonos (Ctrl+A). Deberías verlo naranja como abajo y asegúrate de estar en la vista "Supertior" (pulsa el número 7).
    • Despliega las herramientas "UV" del menú superior y proyecta la capa del terreno con "Project from View (bounds)". Esto hará que se ajuste a la extensión de la imagen.
  1. Elegimos la imagen de la textura para aplicarle la proyección y vemos que se ajustan las celdas del modelo a la imagen (haz un poco de zoom para comprobarlo).
  1. Por último, vamos a la pestaña de Shading y añadimos el elemento "Image Texture" seleccionando la imagen de la textura y conectamos el Vector al UV y el Color al Shader (copiar la imagen).

Ahora, si volvemos a la ventana de Layout, nuestro modelo mostrará la imagen satélite perfectamente ajustada.

Y ya estaría listo, con esto puedes ahora editar y exportar tu modelo, por ejemplo, para imprimirlo en 3D o usarlo en Unity3D.

¿Dudas o comentarios? Pásate por 🐦 Twitter!

🐦 @RoamingWorkshop