{"id":1803,"date":"2022-11-17T22:38:49","date_gmt":"2022-11-17T21:38:49","guid":{"rendered":"https:\/\/theroamingworkshop.cloud\/b\/?p=1803"},"modified":"2023-11-19T15:30:45","modified_gmt":"2023-11-19T14:30:45","slug":"three-js-visor-de-modelos-3d-para-tu-web","status":"publish","type":"post","link":"https:\/\/theroamingworkshop.cloud\/b\/1803\/three-js-visor-de-modelos-3d-para-tu-web\/","title":{"rendered":"Three.js: Visor de modelos 3D para tu web"},"content":{"rendered":"\n<p>Estaba preparando un art\u00edculo donde quer\u00eda 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 <strong>Three.js<\/strong>.<\/p>\n\n\n\n<p><a href=\"https:\/\/threejs.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/threejs.org\/<\/a><\/p>\n\n\n\n<p>\u00a1Si es que est\u00e1 ya todo inventado!<\/p>\n\n\n\n<iframe src=\"https:\/\/theroamingworkshop.cloud\/threeJS\/examples\/webgl_loader_gltf.html\" style=\"width:100%;height:300px;border:none\"><\/iframe>\n\n\n\n<div id=\"menu\" style=\"padding:20px 20px 20px 20px; border-left:2px solid darkgrey;\">\n<p style=\"font-weight:bold;\">Contenido<\/p>\n<\/div>\n<script>\nvar text;\nvar element;\n\nfunction fillmenu(){\n\nfor (let i=0; i<window.document.getElementsByTagName(\"h2\").length; i++){\nelement = window.document.getElementsByTagName(\"h2\")[i];\ntext = \"\u25b9 \"+element.innerHTML;\nvar newelement = document.createElement(\"a\");\nnewelement.innerHTML=text;\nvar postid=window.document.getElementsByTagName(\"article\")[0].id;\nvar url = \"https:\/\/theroamingworkshop.cloud\/b\/?p=\"+postid.substr(5,postid.length)+\"#\"+element.id;\nnewelement.setAttribute(\"href\", url);\nnewelement.appendChild(document.createElement(\"br\"));\nwindow.document.getElementById(\"menu\").appendChild(newelement);\n}\n}\n\nif(window.location.href.includes(\"tag\")){\n\n}else{\nwindow.setTimeout(fillmenu,2000);\n}\n\n<\/script>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"enlazar-CDN\">Enlazar la librer\u00eda CDN<\/h2>\n\n\n\n<p>Para este ejemplo, haremos un visor \"portable\" enlazando las librer\u00edas al CDN oficial, en lugar de tener que descargarnos los ficheros a nuestro servidor.<\/p>\n\n\n\n<p>De esta forma, el archivo de ejemplo te servir\u00e1 en cualquier lugar con conexi\u00f3n a internet. Vamos a crear un fichero .html b\u00e1sico como el que nos sugieren en la documentaci\u00f3n:<\/p>\n\n\n\n<p><a href=\"https:\/\/threejs.org\/docs\/index.html#manual\/en\/introduction\/Creating-a-scene\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/threejs.org\/docs\/index.html#manual\/en\/introduction\/Creating-a-scene<\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n\t&lt;head&gt;\n\t\t&lt;meta charset=\"utf-8\"&gt;\n\t\t&lt;title&gt;My first three.js app&lt;\/title&gt;\n\t\t&lt;style&gt;\n\t\t\tbody { margin: 0; }\n\t\t&lt;\/style&gt;\n\t&lt;\/head&gt;\n\t&lt;body&gt;\n        &lt;script async src=\"https:\/\/unpkg.com\/es-module-shims@1.3.6\/dist\/es-module-shims.js\"&gt;&lt;\/script&gt;\n\n        &lt;script type=\"importmap\"&gt;\n          {\n            \"imports\": {\n              \"three\": \"https:\/\/unpkg.com\/three@0.146.0\/build\/three.module.js\"\n            }\n          }\n        &lt;\/script&gt;\n\n        &lt;script&gt;\n        \/\/App code goes here\n        &lt;\/script&gt;\n\t&lt;\/body&gt;\n&lt;\/html&gt;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"crear-escena\">Crear una escena<\/h2>\n\n\n\n<p>Vamos a seguir con el ejemplo y rellenamos el segundo bloque &lt;script&gt; definiendo una escena con un cubo animado en rotaci\u00f3n:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script type=\"module\"&gt;\n        import * as THREE from 'three';\n\tconst scene = new THREE.Scene();\n\tconst camera = new THREE.PerspectiveCamera( 75, window.innerWidth \/ window.innerHeight, 0.1, 1000 );\n\n\tconst renderer = new THREE.WebGLRenderer();\n\trenderer.setSize( window.innerWidth, window.innerHeight );\n\tdocument.body.appendChild( renderer.domElement );\n\n\tconst geometry = new THREE.BoxGeometry( 1, 1, 1 );\n\tconst material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );\n\tconst cube = new THREE.Mesh( geometry, material );\n\tscene.add( cube );\n\n\tcamera.position.z = 5;\n\n\tfunction animate() {\n\t\trequestAnimationFrame( animate );\n\n\t\tcube.rotation.x += 0.01;\n\t\tcube.rotation.y += 0.01;\n\n\t\trenderer.render( scene, camera );\n\t};\n\n\tanimate();\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<p>Todo eso, junto, queda as\u00ed:<\/p>\n\n\n\n<iframe src=\"https:\/\/theroamingworkshop.cloud\/threeJS\/models\/rPi4case\/cube.html\" style=\"width:100%;height:300px;border:none\"><\/iframe>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"controles-de-arrastre-y-fondo\">A\u00f1ade controles de arrastre y un fondo<\/h2>\n\n\n\n<p>Ahora tenemos una base para trabajar. Puede a\u00f1adir m\u00e1s funcionalidad insertando el m\u00f3dulo <strong>OrbitControls<\/strong> que maneja la rotaci\u00f3n del modelo y de la c\u00e1mara.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/Importa nuevos m\u00f3dulos al principio del script\nimport { OrbitControls } from 'https:\/\/unpkg.com\/three@0.146.0\/examples\/jsm\/controls\/OrbitControls.js';\n\n\/\/luego, a\u00f1ade los controles del cursor despu\u00e9s de declarar la c\u00e1mara y el renderizador\nconst controls = new OrbitControls( camera, renderer.domElement );<\/code><\/pre>\n\n\n\n<p>Tambi\u00e9n puedes <strong>modificar el fondo<\/strong> f\u00e1cilmente, pero necesitas hospedar la imagen junto a la aplicaci\u00f3n en un servidor, o <a rel=\"noreferrer noopener\" href=\"https:\/\/threejs.org\/docs\/index.html#manual\/en\/introduction\/How-to-run-things-locally\" target=\"_blank\">ejecutarlo localmente<\/a>, debido al <strong>CORS<\/strong>. Yo usar\u00e9 la imagen de la cabecera del blog, que saqu\u00e9 de <a href=\"https:\/\/theroamingworkshop.cloud\/b\/?p=1155\" target=\"_blank\" rel=\"noreferrer noopener\">Stellarium<\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/theroamingworkshop.cloud\/demos\/Unity1-north.png\" alt=\"\"\/><\/figure>\n\n\n\n<p>Primero, define una <a rel=\"noreferrer noopener\" href=\"https:\/\/threejs.org\/docs\/index.html#api\/en\/textures\/Texture\" target=\"_blank\">textura<\/a>. Luego, a\u00f1\u00e1dela a la escena:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/a\u00f1ade esto antes de renderizar, mientras defines la escena\n\/\/define la textura\nconst texture = new THREE.TextureLoader().load( \"https:\/\/theroamingworkshop.cloud\/demos\/Unity1-north.png\" );\n\n\/\/a\u00f1ade la textura a la escena\nscene.background=texture;<\/code><\/pre>\n\n\n\n<iframe src=\"https:\/\/theroamingworkshop.cloud\/threeJS\/models\/rPi4case\/cube2.html\" style=\"width:100%;height:300px;border:none\"><\/iframe>\n\n\n\n<p>C\u00f3digo completo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n\t&lt;head&gt;\n\t\t&lt;meta charset=\"utf-8\"&gt;\n\t\t&lt;title&gt;My first three.js app&lt;\/title&gt;\n\t\t&lt;style&gt;\n\t\t\tbody { margin: 0; }\n\t\t&lt;\/style&gt;\n\t&lt;\/head&gt;\n\t&lt;body&gt;\n        &lt;script async src=\"https:\/\/unpkg.com\/es-module-shims@1.3.6\/dist\/es-module-shims.js\"&gt;&lt;\/script&gt;\n\n        &lt;script type=\"importmap\"&gt;\n          {\n            \"imports\": {\n              \"three\": \"https:\/\/unpkg.com\/three@0.146.0\/build\/three.module.js\"\n            }\n          }\n        &lt;\/script&gt;\n&lt;body style=\"margin: 0; width:100%;height:300px;\"&gt;\n        &lt;script async src=\"https:\/\/unpkg.com\/es-module-shims@1.3.6\/dist\/es-module-shims.js\"&gt;&lt;\/script&gt;\n\n        &lt;script type=\"importmap\"&gt;\n          {\n            \"imports\": {\n              \"three\": \"https:\/\/unpkg.com\/three@0.146.0\/build\/three.module.js\"\n            }\n          }\n        &lt;\/script&gt;\n\n    &lt;script type=\"module\"&gt;\n\n    import * as THREE from 'three';\n\timport { OrbitControls } from 'https:\/\/unpkg.com\/three@0.146.0\/examples\/jsm\/controls\/OrbitControls.js';\n\n\tconst scene = new THREE.Scene();\n    \n\tconst texture = new THREE.TextureLoader().load( \"https:\/\/theroamingworkshop.cloud\/demos\/Unity1-north.png\" );\n\tscene.background=texture;\n\n    const camera = new THREE.PerspectiveCamera( 75, window.innerWidth \/ window.innerHeight, 0.1, 1000 );\n\n\tconst renderer = new THREE.WebGLRenderer();\n\trenderer.setSize( window.innerWidth, window.innerHeight );\n\tdocument.body.appendChild( renderer.domElement );\n    \n    const controls = new OrbitControls( camera, renderer.domElement );\n\n\tconst geometry = new THREE.BoxGeometry( 1, 1, 1 );\n\tconst material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );\n\tconst cube = new THREE.Mesh( geometry, material );\n\tscene.add( cube );\n\n\tcamera.position.z = 5;\n\n\tfunction animate() {\n\t\trequestAnimationFrame( animate );\n\n\t\tcube.rotation.x += 0.01;\n\t\tcube.rotation.y += 0.01;\n\n\t\trenderer.render( scene, camera );\n\t};\n\n\tanimate();\n&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"insertar-modelo-3D\">Insertar un modelo 3D<\/h2>\n\n\n\n<p>Ahora vamos a sustituir este cubo por un modelo 3D propio, que en el caso de <strong>Three.js<\/strong>, debe tener un formato <strong>glTF<\/strong> (.GLB o .GLTF), que es el formato m\u00e1s soportado y que renderiza m\u00e1s r\u00e1pidamente (aunque tambi\u00e9n hay soporte para .fbx, .stl, .obj y dem\u00e1s).<\/p>\n\n\n\n<p>Yo exportar\u00e9 a .glb esta carcasa b\u00e1sica de Raspberry Pi 4B que hice hace un tiempo usando <strong><a rel=\"noreferrer noopener\" href=\"https:\/\/www.blender.org\/\" target=\"_blank\">Blender<\/a><\/strong>:<\/p>\n\n\n\n<figure class=\"wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex\">\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"689\" height=\"536\" data-id=\"1804\" src=\"https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three1.png\" alt=\"\" class=\"wp-image-1804\" srcset=\"https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three1.png 689w, https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three1-300x233.png 300w, https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three1-676x526.png 676w\" sizes=\"auto, (max-width: 689px) 100vw, 689px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"778\" height=\"770\" data-id=\"1806\" src=\"https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three2.png\" alt=\"\" class=\"wp-image-1806\" srcset=\"https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three2.png 778w, https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three2-300x297.png 300w, https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three2-768x760.png 768w, https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three2-676x669.png 676w\" sizes=\"auto, (max-width: 778px) 100vw, 778px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"856\" height=\"583\" data-id=\"1805\" src=\"https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three3.png\" alt=\"\" class=\"wp-image-1805\" srcset=\"https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three3.png 856w, https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three3-300x204.png 300w, https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three3-768x523.png 768w, https:\/\/theroamingworkshop.cloud\/b\/wp-content\/uploads\/2022\/11\/three3-676x460.png 676w\" sizes=\"auto, (max-width: 856px) 100vw, 856px\" \/><\/figure>\n<\/figure>\n\n\n\n<p>Ahora, para insertar el modelo sustituimos el bloque &lt;script&gt; anterior bas\u00e1ndonos en el ejemplo \"<strong>webgl_loader_gltf<\/strong>\" que se ve al inicio del post:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script type=\"module\"&gt;\nimport * as THREE from 'three';\nimport { GLTFLoader } from 'https:\/\/unpkg.com\/three@0.146.0\/examples\/jsm\/loaders\/GLTFLoader.js';\nimport { OrbitControls } from 'https:\/\/unpkg.com\/three@0.146.0\/examples\/jsm\/controls\/OrbitControls.js';\n\nlet camera, scene, renderer;\n\ninit();\nrender();\n\nfunction init() {\n\n\tconst container = document.createElement( 'div' );\n\tdocument.body.appendChild( container );\n\n\tcamera = new THREE.PerspectiveCamera( 30, window.innerWidth \/ window.innerHeight, 0.1, 20 );\n    camera.position.set( 0.2, 0.2, 0.2 );\n\n\tscene = new THREE.Scene();        \n    scene.add( new THREE.AmbientLight( 0xffffff, 0.75 ) );\n\n\tconst dirLight = new THREE.DirectionalLight( 0xffffff, 1 );\n\tdirLight.position.set( 5, 10, 7.5 );\n\tdirLight.castShadow = true;\n\tdirLight.shadow.camera.right = 2;\n\tdirLight.shadow.camera.left = - 2;\n\tdirLight.shadow.camera.top\t= 2;\n\tdirLight.shadow.camera.bottom = - 2;\n\tdirLight.shadow.mapSize.width = 1024;\n\tdirLight.shadow.mapSize.height = 1024;\n\tscene.add( dirLight );\n\n    \/\/model\n     const loader = new GLTFLoader();\n\t loader.load( 'https:\/\/theroamingworkshop.cloud\/threeJS\/models\/rPi4case\/rPi4_case_v1.glb', function ( gltf ) {\n\t\tscene.add( gltf.scene );\n\t\trender();\n\t } );\n\n\trenderer = new THREE.WebGLRenderer( { antialias: true } );\n            \n\trenderer.setPixelRatio( window.devicePixelRatio );\n\trenderer.setSize( window.innerWidth, window.innerHeight );\n\trenderer.toneMapping = THREE.ACESFilmicToneMapping;\n\trenderer.toneMappingExposure = 1;\n\trenderer.outputEncoding = THREE.sRGBEncoding;\n\tcontainer.appendChild( renderer.domElement );\n\n\tconst controls = new OrbitControls( camera, renderer.domElement );\n\tcontrols.addEventListener( 'change', render );\n    controls.minDistance = 0.001;\n\tcontrols.maxDistance = 1;\n\tcontrols.target.set( 0.03, 0.01, -0.01 );\n\tcontrols.update();\n\twindow.addEventListener( 'resize', onWindowResize );\n}\nfunction onWindowResize() {\n\tcamera.aspect = window.innerWidth \/ window.innerHeight;\n\tcamera.updateProjectionMatrix();\n\trenderer.setSize( window.innerWidth, window.innerHeight );\n\trender();\n}\nfunction render() {\n\trenderer.render( scene, camera );\n}\n&lt;\/script&gt;<\/code><\/pre>\n\n\n\n<p>B\u00e1sicamente se hace lo siguiente:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Importar m\u00f3dulos a usar:\n<ul class=\"wp-block-list\">\n<li><strong>GLTFLoader<\/strong> cargar\u00e1 nuestro modelo en formato .glb<\/li>\n\n\n\n<li><strong>OrbitControls<\/strong> nos permite controlar la vista de la c\u00e1mara<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Definir la escena:\n<ul class=\"wp-block-list\">\n<li>definir una c\u00e1mara<\/li>\n\n\n\n<li>definir la luz (en este caso hay luz ambiente y direccional, prueba a comentar alguna de ellas y ver\u00e1s la diferencia)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Cargar el modelo en la escena<\/li>\n\n\n\n<li>Definir los par\u00e1metros de renderizado y renderizar. <\/li>\n<\/ul>\n\n\n\n<p>Y todo ello queda as\u00ed (clicka y arrastra!):<\/p>\n\n\n\n<iframe src=\"https:\/\/theroamingworkshop.cloud\/threeJS\/models\/rPi4case\/\" style=\"width:100%;height:300px;border:none\"><\/iframe>\n\n\n\n<p>Espero que te sea \u00fatil! Dudas o comentarios al \ud83d\udc26 Twitter!<\/p>\n\n\n\n<p>\ud83d\udc26 <code><a rel=\"noreferrer noopener\" href=\"https:\/\/twitter.com\/roamingworkshop\" target=\"_blank\">@RoamingWorkshop<\/a><\/code><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Estaba preparando un art\u00edculo donde quer\u00eda 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\/ \u00a1Si es que est\u00e1 ya todo inventado! Enlazar la librer\u00eda CDN Para este ejemplo, haremos un visor \u00abportable\u00bb enlazando las librer\u00edas al [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1804,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[72,52,607],"tags":[262,509,511,513,515,517,266,264,519,521,523,525],"class_list":["post-1803","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-3des","category-webes","category-demos","tag-3d","tag-3d-model-viewer-es","tag-model-es","tag-modelo-es","tag-modelos-es","tag-modelos-3d-es","tag-viewer","tag-visor","tag-visor-3d-es","tag-visor-de-modelos-3d-es","tag-web-es","tag-web-viewer-es","post-preview"],"_links":{"self":[{"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/posts\/1803","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/comments?post=1803"}],"version-history":[{"count":9,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/posts\/1803\/revisions"}],"predecessor-version":[{"id":1828,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/posts\/1803\/revisions\/1828"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/media\/1804"}],"wp:attachment":[{"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/media?parent=1803"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/categories?post=1803"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/tags?post=1803"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}