{"id":1809,"date":"2022-11-17T22:54:35","date_gmt":"2022-11-17T21:54:35","guid":{"rendered":"https:\/\/theroamingworkshop.cloud\/b\/?p=1809"},"modified":"2023-11-19T15:30:40","modified_gmt":"2023-11-19T14:30:40","slug":"three-js-a-3d-model-viewer-for-your-site","status":"publish","type":"post","link":"https:\/\/theroamingworkshop.cloud\/b\/en\/1809\/three-js-a-3d-model-viewer-for-your-site\/","title":{"rendered":"Three.js: a 3D model viewer for your site"},"content":{"rendered":"\n<p>I was writing a post where I wanted to insert a 3D model to picture it better, and I even thought in doing a viewer myself. But I didn&#8217;t have to browse long to find <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>It&#8217;s all invented!<\/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;\">Content<\/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);\nconsole.log(text);\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=\"cdn-link\">Link CDN library<\/h2>\n\n\n\n<p>For this example, I'll use the official CDN libraries, instead of downloading the files to a server.<\/p>\n\n\n\n<p>Create a basic .html file as suggested in the documentation:<\/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=\"create-scene\">Create a scene<\/h2>\n\n\n\n<p>Let's keep with the example and fill up the second &lt;script&gt; block defining a scene with an animated rotating cube:<\/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>All of this would look like this:<\/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=\"drag-controls-background\">Add drag controls and background<\/h2>\n\n\n\n<p>Now we have a base to work with. We can add some functionality inserting the <strong>OrbitControls<\/strong> module.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/Import new modules at the beginning of the script\nimport { OrbitControls } from 'https:\/\/unpkg.com\/three@0.146.0\/examples\/jsm\/controls\/OrbitControls.js';\n\n\/\/then add the mouse controls after declaring the camera and renderer\nconst controls = new OrbitControls( camera, renderer.domElement );<\/code><\/pre>\n\n\n\n<p>Also, you can <strong>modify the background<\/strong> easily, but you will need to host your images within your app in a server, or <a rel=\"noreferrer noopener\" href=\"https:\/\/threejs.org\/docs\/index.html#manual\/en\/introduction\/How-to-run-things-locally\" target=\"_blank\">run it locally<\/a>, because of <strong>CORS<\/strong>. I will be using the background image of the blog header, which was taken from <a rel=\"noreferrer noopener\" href=\"https:\/\/theroamingworkshop.cloud\/b\/?p=1170&amp;lang=en\" target=\"_blank\">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>First define a <a rel=\"noreferrer noopener\" href=\"https:\/\/threejs.org\/docs\/index.html#api\/en\/textures\/Texture\" target=\"_blank\">texture<\/a>. Then, add it to the scene:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/do this before rendering, while defining the scene\n\/\/define texture\nconst texture = new THREE.TextureLoader().load( \"https:\/\/theroamingworkshop.cloud\/demos\/Unity1-north.png\" );\n\n\/\/add texture to scene\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>Full code:<\/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=\"insert-3d-model\">Insert a 3D model<\/h2>\n\n\n\n<p>Now let's replace the cube for our own 3D model which, in the case of <strong>Three.js<\/strong>, will be a <strong>glTF<\/strong> (.GLB o .GLTF) format, that is most supported and renders faster (.fbx, .stl, .obj and so on are also supported).<\/p>\n\n\n\n<p>I will export a .glb of this basic Raspberry Pi 4B case that I did some time ago using <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>Now, replace the &lt;script&gt; block based on the \"<strong>webgl_loader_gltf<\/strong>\" which was shown at the start of the 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>Basically it does the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Import used modules:\n<ul class=\"wp-block-list\">\n<li><strong>GLTFLoader<\/strong> will load our model in .glb format<\/li>\n\n\n\n<li><strong>OrbitControls<\/strong> for camera rotation and position<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Define the scene:\n<ul class=\"wp-block-list\">\n<li>define a camera<\/li>\n\n\n\n<li>define the light (in this case, ambient and directional; try commenting each of them to see the difference)<\/li>\n\n\n\n<li>define a background<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Load the model in the scene<\/li>\n\n\n\n<li>Define rendering parameter and render.<\/li>\n<\/ul>\n\n\n\n<p>All of it finally looks like this (click and drag!):<\/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>Hope it's useful! Doubts or comments on \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>I was writing a post where I wanted to insert a 3D model to picture it better, and I even thought in doing a viewer myself. But I didn&#8217;t have to browse long to find Three.js. https:\/\/threejs.org\/ It&#8217;s all invented! Link CDN library For this example, I&#8217;ll use the official CDN libraries, instead of downloading [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1804,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[74,609,50],"tags":[276,493,491,505,503,507,310,312,499,501,495,497],"class_list":["post-1809","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-3den","category-demos-en","category-weben","tag-3d-en","tag-3d-model-viewer","tag-model","tag-modelo","tag-modelos","tag-modelos-3d","tag-viewer-en","tag-visor-en","tag-visor-3d","tag-visor-de-modelos-3d","tag-web","tag-web-viewer","post-preview"],"_links":{"self":[{"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/posts\/1809","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=1809"}],"version-history":[{"count":8,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/posts\/1809\/revisions"}],"predecessor-version":[{"id":1827,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/posts\/1809\/revisions\/1827"}],"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=1809"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/categories?post=1809"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/theroamingworkshop.cloud\/b\/wp-json\/wp\/v2\/tags?post=1809"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}