tech explorers, welcome!

Etiqueta: viewer

Carcasa DFRobot para UNIHIKER

El pequeño y eficiente factor de forma de la UNIHIKER hace que sea muy fácil diseñar una carcasa para ella.

Para my asistente inteligente buscaba un estilo tipo androide, y el logo de DFRobot es perfecto para la UNIHIKER, haciendo un guiño a sus desarrolladores.

Repositorio Github

He publicado un repositorio donde abriré las fuentes (open-source) de todos los archivos de los modelos, pudiendo cualquiera contribuir con los suyos, así que anímate a crear una solicitud de cambios (pull request) y comparte tus diseños!

https://github.com/TheRoam/DFRobot-UNIHIKER-case/

También se incluye una página github donde se pueden previsualizar los modelos:

https://theroam.github.io/DFRobot-UNIHIKER-case/

Unihiker_DFRcase_v1

Mi primer lanzamiento, usado para testear e incluir las funcionalidades básicas para mi asistente.

Archivos

https://github.com/TheRoam/DFRobot-UNIHIKER-case/tree/main/blender

Características

  • Aperturas superiores para mostrar texto en la pantalla táctil.
  • Apertura lateral para conexión de USB-C.
  • Apertura trasera para cableado de sensores externos.
  • Extrusiones traseras para colocación de altavoces de 40mm.
  • Pie de soporte para posicionamiento vertical.

Partes de la carcasa

  1. Pieza inferior: actúa como envoltorio.
  2. Pieza de soporte interior: sostiene la placa UNIHIKER a la pieza inferior usando los tornillos de la placa.
  3. Pieza superior: actúa como tapa encajándose en la pieza inferior.
  4. Pie de soporte: permite posicionamiento vertical.
  5. Antenas: le dan el toque final al logo de DFRobot.

Ensamblaje

  1. Coloca los tornillos en el soporte interior y atorníllalo a la placa UNIHIKER.
  1. Coloca la UNIHIKER dentro de la pieza inferior. Si vas a usar sensores exteriores, saca el cableado por la apertura trasera.
  1. Sujeta la placa a la carcasa con un par de tornillos de 2.5mm desde la parte trasera.
  1. Coloca la pieza superior y encájala con la inferior. Hará click con dos pestañas laterales.
  1. Coloca el pie de soporte y las antenas en su sitio. Puedes pegarlos con pegamento para asegurarte de que no se mueven.

Y ahí tienes tu carcasa montada con un look muy droide!

No olvides compartir tus impresiones en 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>

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