123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706 |
- <!DOCTYPE html><html lang="en"><head>
- <meta charset="utf-8">
- <title>Loading a .GLTF File</title>
- <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
- <meta name="twitter:card" content="summary_large_image">
- <meta name="twitter:site" content="@threejs">
- <meta name="twitter:title" content="Three.js – Loading a .GLTF File">
- <meta property="og:image" content="https://threejs.org/files/share.png">
- <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
- <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
- <link rel="stylesheet" href="../resources/lesson.css">
- <link rel="stylesheet" href="../resources/lang.css">
- <script type="importmap">
- {
- "imports": {
- "three": "../../build/three.module.js"
- }
- }
- </script>
- </head>
- <body>
- <div class="container">
- <div class="lesson-title">
- <h1>Loading a .GLTF File</h1>
- </div>
- <div class="lesson">
- <div class="lesson-main">
- <p>In a previous lesson we <a href="load-obj.html">loaded an .OBJ file</a>. If
- you haven't read it you might want to check it out first.</p>
- <p>As pointed out over there the .OBJ file format is very old and fairly
- simple. It provides no scene graph so everything loaded is one large
- mesh. It was designed mostly as a simple way to pass data between
- 3D editors.</p>
- <p><a href="https://github.com/KhronosGroup/glTF">The gLTF format</a> is actually
- a format designed from the ground up for be used for displaying
- graphics. 3D formats can be divided into 3 or 4 basic types.</p>
- <ul>
- <li><p>3D Editor Formats</p>
- <p>This are formats specific to a single app. .blend (Blender), .max (3d Studio Max),
- .mb and .ma (Maya), etc...</p>
- </li>
- <li><p>Exchange formats</p>
- <p>These are formats like .OBJ, .DAE (Collada), .FBX. They are designed to help exchange
- information between 3D editors. As such they are usually much larger than needed with
- extra info used only inside 3d editors</p>
- </li>
- <li><p>App formats</p>
- <p>These are usually specific to certain apps, usually games.</p>
- </li>
- <li><p>Transmission formats</p>
- <p>gLTF might be the first true transmission format. I suppose VRML might be considered
- one but VRML was actually a pretty poor format.</p>
- <p>gLTF is designed to do some things well that all those other formats don't do</p>
- <ol>
- <li><p>Be small for transmission</p>
- <p>For example this means much of their large data, like vertices, is stored in
- binary. When you download a .gLTF file that data can be uploaded to the GPU
- with zero processing. It's ready as is. This is in contrast to say VRML, .OBJ,
- or .DAE where vertices are stored as text and have to be parsed. Text vertex
- positions can easily be 3x to 5x larger than binary.</p>
- </li>
- <li><p>Be ready to render</p>
- <p>This again is different from other formats except maybe App formats. The data
- in a glTF file is mean to be rendered, not edited. Data that's not important to
- rendering has generally been removed. Polygons have been converted to triangles.
- Materials have known values that are supposed to work everywhere.</p>
- </li>
- </ol>
- </li>
- </ul>
- <p>gLTF was specifically designed so you should be able to download a glTF file and
- display it with a minimum of trouble. Let's cross our fingers that's truly the case
- as none of the other formats have been able to do this.</p>
- <p>I wasn't really sure what I should show. At some level loading and displaying a gLTF file
- is simpler than an .OBJ file. Unlike a .OBJ file materials are directly part of the format.
- That said I thought I should at least load one up and I think going over the issues I ran
- into might provide some good info.</p>
- <p>Searching the net I found <a href="https://sketchfab.com/models/edd1c604e1e045a0a2a552ddd9a293e6">this low-poly city</a>
- by <a href="https://sketchfab.com/antonmoek">antonmoek</a> which seemed like if we're lucky
- might make a good example.</p>
- <div class="threejs_center"><img src="../resources/images/cartoon_lowpoly_small_city_free_pack.jpg"></div>
- <p>Starting with <a href="load-obj.html">an example from the .OBJ article</a> I removed the code
- for loading .OBJ and replaced it with code for loading .GLTF</p>
- <p>The old .OBJ code was</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mtlLoader = new MTLLoader();
- mtlLoader.loadMtl('resources/models/windmill/windmill-fixed.mtl', (mtl) => {
- mtl.preload();
- mtl.materials.Material.side = THREE.DoubleSide;
- objLoader.setMaterials(mtl);
- objLoader.load('resources/models/windmill/windmill.obj', (event) => {
- const root = event.detail.loaderRootNode;
- scene.add(root);
- ...
- });
- });
- </pre>
- <p>The new .GLTF code is</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
- const gltfLoader = new GLTFLoader();
- const url = 'resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf';
- gltfLoader.load(url, (gltf) => {
- const root = gltf.scene;
- scene.add(root);
- ...
- });
- </pre>
- <p>I kept the auto framing code as before</p>
- <p>We also need to include the <a href="/docs/#examples/loaders/GLTFLoader"><code class="notranslate" translate="no">GLTFLoader</code></a> and we can get rid of the <a href="/docs/#examples/loaders/OBJLoader"><code class="notranslate" translate="no">OBJLoader</code></a>.</p>
- <pre class="prettyprint showlinemods notranslate lang-html" translate="no">-import {LoaderSupport} from 'three/addons/loaders/LoaderSupport.js';
- -import {OBJLoader} from 'three/addons/loaders/OBJLoader.js';
- -import {MTLLoader} from 'three/addons/loaders/MTLLoader.js';
- +import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js';
- </pre>
- <p>And running that we get</p>
- <p></p><div translate="no" class="threejs_example_container notranslate">
- <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-gltf.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/load-gltf.html" target="_blank">click here to open in a separate window</a>
- </div>
- <p></p>
- <p>Magic! It just works, textures and all.</p>
- <p>Next I wanted to see if I could animate the cars driving around so
- I needed to check if the scene had the cars as separate entities
- and if they were setup in a way I could use them.</p>
- <p>I wrote some code to dump put the scenegraph to the <a href="debugging-javascript.html">JavaScript
- console</a>.</p>
- <p>Here's the code to print out the scenegraph.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function dumpObject(obj, lines = [], isLast = true, prefix = '') {
- const localPrefix = isLast ? '└─' : '├─';
- lines.push(`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`);
- const newPrefix = prefix + (isLast ? ' ' : '│ ');
- const lastNdx = obj.children.length - 1;
- obj.children.forEach((child, ndx) => {
- const isLast = ndx === lastNdx;
- dumpObject(child, lines, isLast, newPrefix);
- });
- return lines;
- }
- </pre>
- <p>And I just called it right after loading the scene.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gltfLoader = new GLTFLoader();
- gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
- const root = gltf.scene;
- scene.add(root);
- console.log(dumpObject(root).join('\n'));
- </pre>
- <p><a href="../examples/load-gltf-dump-scenegraph.html">Running that</a> I got this listing</p>
- <pre class="prettyprint showlinemods notranslate lang-text" translate="no">OSG_Scene [Scene]
- └─RootNode_(gltf_orientation_matrix) [Object3D]
- └─RootNode_(model_correction_matrix) [Object3D]
- └─4d4100bcb1c640e69699a87140df79d7fbx [Object3D]
- └─RootNode [Object3D]
- │ ...
- ├─Cars [Object3D]
- │ ├─CAR_03_1 [Object3D]
- │ │ └─CAR_03_1_World_ap_0 [Mesh]
- │ ├─CAR_03 [Object3D]
- │ │ └─CAR_03_World_ap_0 [Mesh]
- │ ├─Car_04 [Object3D]
- │ │ └─Car_04_World_ap_0 [Mesh]
- │ ├─CAR_03_2 [Object3D]
- │ │ └─CAR_03_2_World_ap_0 [Mesh]
- │ ├─Car_04_1 [Object3D]
- │ │ └─Car_04_1_World_ap_0 [Mesh]
- │ ├─Car_04_2 [Object3D]
- │ │ └─Car_04_2_World_ap_0 [Mesh]
- │ ├─Car_04_3 [Object3D]
- │ │ └─Car_04_3_World_ap_0 [Mesh]
- │ ├─Car_04_4 [Object3D]
- │ │ └─Car_04_4_World_ap_0 [Mesh]
- │ ├─Car_08_4 [Object3D]
- │ │ └─Car_08_4_World_ap8_0 [Mesh]
- │ ├─Car_08_3 [Object3D]
- │ │ └─Car_08_3_World_ap9_0 [Mesh]
- │ ├─Car_04_1_2 [Object3D]
- │ │ └─Car_04_1_2_World_ap_0 [Mesh]
- │ ├─Car_08_2 [Object3D]
- │ │ └─Car_08_2_World_ap11_0 [Mesh]
- │ ├─CAR_03_1_2 [Object3D]
- │ │ └─CAR_03_1_2_World_ap_0 [Mesh]
- │ ├─CAR_03_2_2 [Object3D]
- │ │ └─CAR_03_2_2_World_ap_0 [Mesh]
- │ ├─Car_04_2_2 [Object3D]
- │ │ └─Car_04_2_2_World_ap_0 [Mesh]
- ...
- </pre>
- <p>From that we can see all the cars happen to be under a parent
- called <code class="notranslate" translate="no">"Cars"</code></p>
- <pre class="prettyprint showlinemods notranslate lang-text" translate="no">* ├─Cars [Object3D]
- │ ├─CAR_03_1 [Object3D]
- │ │ └─CAR_03_1_World_ap_0 [Mesh]
- │ ├─CAR_03 [Object3D]
- │ │ └─CAR_03_World_ap_0 [Mesh]
- │ ├─Car_04 [Object3D]
- │ │ └─Car_04_World_ap_0 [Mesh]
- </pre>
- <p>So as a simple test I thought I would just try rotating
- all the children of the "Cars" node around their Y axis.</p>
- <p>I looked up the "Cars" node after loading the scene
- and saved the result.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+let cars;
- {
- const gltfLoader = new GLTFLoader();
- gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
- const root = gltf.scene;
- scene.add(root);
- + cars = root.getObjectByName('Cars');
- </pre>
- <p>Then in the <code class="notranslate" translate="no">render</code> function we can just set the rotation
- of each child of <code class="notranslate" translate="no">cars</code>.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function render(time) {
- + time *= 0.001; // convert to seconds
- if (resizeRendererToDisplaySize(renderer)) {
- const canvas = renderer.domElement;
- camera.aspect = canvas.clientWidth / canvas.clientHeight;
- camera.updateProjectionMatrix();
- }
- + if (cars) {
- + for (const car of cars.children) {
- + car.rotation.y = time;
- + }
- + }
- renderer.render(scene, camera);
- requestAnimationFrame(render);
- }
- </pre>
- <p>And we get</p>
- <p></p><div translate="no" class="threejs_example_container notranslate">
- <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-gltf-rotate-cars.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/load-gltf-rotate-cars.html" target="_blank">click here to open in a separate window</a>
- </div>
- <p></p>
- <p>Hmmm, it looks like unfortunately this scene wasn't designed to
- animate the cars as their origins are not setup for that purpose.
- The trucks are rotating in the wrong direction.</p>
- <p>This brings up an important point which is if you're going to
- do something in 3D you need to plan ahead and design your assets
- so they have their origins in the correct places, so they are
- the correct scale, etc.</p>
- <p>Since I'm not an artist and I don't know blender that well I
- will hack this example. We'll take each car and parent it to
- another <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>. We will then move those <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a> objects
- to move the cars but separately we can set the car's original
- <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a> to re-orient it so it's about where we really need it.</p>
- <p>Looking back at the scene graph listing it looks like there
- are really only 3 types of cars, "Car_08", "CAR_03", and "Car_04".
- Hopefully each type of car will work with the same adjustments.</p>
- <p>I wrote this code to go through each car, parent it to a new
- <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a>, parent that new <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a> to the scene, and apply
- some per car <em>type</em> settings to fix its orientation, and add
- the new <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a> a <code class="notranslate" translate="no">cars</code> array.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-let cars;
- +const cars = [];
- {
- const gltfLoader = new GLTFLoader();
- gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
- const root = gltf.scene;
- scene.add(root);
- - cars = root.getObjectByName('Cars');
- + const loadedCars = root.getObjectByName('Cars');
- + const fixes = [
- + { prefix: 'Car_08', rot: [Math.PI * .5, 0, Math.PI * .5], },
- + { prefix: 'CAR_03', rot: [0, Math.PI, 0], },
- + { prefix: 'Car_04', rot: [0, Math.PI, 0], },
- + ];
- +
- + root.updateMatrixWorld();
- + for (const car of loadedCars.children.slice()) {
- + const fix = fixes.find(fix => car.name.startsWith(fix.prefix));
- + const obj = new THREE.Object3D();
- + car.getWorldPosition(obj.position);
- + car.position.set(0, 0, 0);
- + car.rotation.set(...fix.rot);
- + obj.add(car);
- + scene.add(obj);
- + cars.push(obj);
- + }
- ...
- </pre>
- <p>This fixes the orientation of the cars. </p>
- <p></p><div translate="no" class="threejs_example_container notranslate">
- <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-gltf-rotate-cars-fixed.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/load-gltf-rotate-cars-fixed.html" target="_blank">click here to open in a separate window</a>
- </div>
- <p></p>
- <p>Now let's drive them around.</p>
- <p>Making even a simple driving system is too much for this post but
- it seems instead we could just make one convoluted path that
- drives down all the roads and then put the cars on the path.
- Here's a picture from Blender about half way through building
- the path.</p>
- <div class="threejs_center"><img src="../resources/images/making-path-for-cars.jpg" style="width: 1094px"></div>
- <p>I needed a way to get the data for that path out of Blender.
- Fortunately I was able to select just my path and export .OBJ checking "write nurbs".</p>
- <div class="threejs_center"><img src="../resources/images/blender-export-obj-write-nurbs.jpg" style="width: 498px"></div>
- <p>Opening the .OBJ file I was able to get a list of points
- which I formatted into this</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const controlPoints = [
- [1.118281, 5.115846, -3.681386],
- [3.948875, 5.115846, -3.641834],
- [3.960072, 5.115846, -0.240352],
- [3.985447, 5.115846, 4.585005],
- [-3.793631, 5.115846, 4.585006],
- [-3.826839, 5.115846, -14.736200],
- [-14.542292, 5.115846, -14.765865],
- [-14.520929, 5.115846, -3.627002],
- [-5.452815, 5.115846, -3.634418],
- [-5.467251, 5.115846, 4.549161],
- [-13.266233, 5.115846, 4.567083],
- [-13.250067, 5.115846, -13.499271],
- [4.081842, 5.115846, -13.435463],
- [4.125436, 5.115846, -5.334928],
- [-14.521364, 5.115846, -5.239871],
- [-14.510466, 5.115846, 5.486727],
- [5.745666, 5.115846, 5.510492],
- [5.787942, 5.115846, -14.728308],
- [-5.423720, 5.115846, -14.761919],
- [-5.373599, 5.115846, -3.704133],
- [1.004861, 5.115846, -3.641834],
- ];
- </pre>
- <p>THREE.js has some curve classes. The <a href="/docs/#api/en/extras/curves/CatmullRomCurve3"><code class="notranslate" translate="no">CatmullRomCurve3</code></a> seemed
- like it might work. The thing about that kind of curve is
- it tries to make a smooth curve going through the points.</p>
- <p>In fact putting those points in directly will generate
- a curve like this</p>
- <div class="threejs_center"><img src="../resources/images/car-curves-before.png" style="width: 400px"></div>
- <p>but we want a sharper corners. It seemed like if we computed
- some extra points we could get what we want. For each pair
- of points we'll compute a point 10% of the way between
- the 2 points and another 90% of the way between the 2 points
- and pass the result to <a href="/docs/#api/en/extras/curves/CatmullRomCurve3"><code class="notranslate" translate="no">CatmullRomCurve3</code></a>.</p>
- <p>This will give us a curve like this</p>
- <div class="threejs_center"><img src="../resources/images/car-curves-after.png" style="width: 400px"></div>
- <p>Here's the code to make the curve </p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let curve;
- let curveObject;
- {
- const controlPoints = [
- [1.118281, 5.115846, -3.681386],
- [3.948875, 5.115846, -3.641834],
- [3.960072, 5.115846, -0.240352],
- [3.985447, 5.115846, 4.585005],
- [-3.793631, 5.115846, 4.585006],
- [-3.826839, 5.115846, -14.736200],
- [-14.542292, 5.115846, -14.765865],
- [-14.520929, 5.115846, -3.627002],
- [-5.452815, 5.115846, -3.634418],
- [-5.467251, 5.115846, 4.549161],
- [-13.266233, 5.115846, 4.567083],
- [-13.250067, 5.115846, -13.499271],
- [4.081842, 5.115846, -13.435463],
- [4.125436, 5.115846, -5.334928],
- [-14.521364, 5.115846, -5.239871],
- [-14.510466, 5.115846, 5.486727],
- [5.745666, 5.115846, 5.510492],
- [5.787942, 5.115846, -14.728308],
- [-5.423720, 5.115846, -14.761919],
- [-5.373599, 5.115846, -3.704133],
- [1.004861, 5.115846, -3.641834],
- ];
- const p0 = new THREE.Vector3();
- const p1 = new THREE.Vector3();
- curve = new THREE.CatmullRomCurve3(
- controlPoints.map((p, ndx) => {
- p0.set(...p);
- p1.set(...controlPoints[(ndx + 1) % controlPoints.length]);
- return [
- (new THREE.Vector3()).copy(p0),
- (new THREE.Vector3()).lerpVectors(p0, p1, 0.1),
- (new THREE.Vector3()).lerpVectors(p0, p1, 0.9),
- ];
- }).flat(),
- true,
- );
- {
- const points = curve.getPoints(250);
- const geometry = new THREE.BufferGeometry().setFromPoints(points);
- const material = new THREE.LineBasicMaterial({color: 0xff0000});
- curveObject = new THREE.Line(geometry, material);
- scene.add(curveObject);
- }
- }
- </pre>
- <p>The first part of that code makes a curve.
- The second part of that code generates 250 points
- from the curve and then creates an object to display
- the lines made by connecting those 250 points.</p>
- <p>Running <a href="../examples/load-gltf-car-path.html">the example</a> I didn't see
- the curve. To make it visible I made it ignore the depth test and
- render last</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no"> curveObject = new THREE.Line(geometry, material);
- + material.depthTest = false;
- + curveObject.renderOrder = 1;
- </pre>
- <p>And that's when I discovered it was way too small.</p>
- <div class="threejs_center"><img src="../resources/images/car-curves-too-small.png" style="width: 498px"></div>
- <p>Checking the hierarchy in Blender I found out that the artist had
- scaled the node all the cars are parented to.</p>
- <div class="threejs_center"><img src="../resources/images/cars-scale-0.01.png" style="width: 342px;"></div>
- <p>Scaling is bad for real time 3D apps. It causes all kinds of
- issues and ends up being no end of frustration when doing
- real time 3D. Artists often don't know this because it's so
- easy to scale an entire scene in a 3D editing program but
- if you decide to make a real time 3D app I suggest you request your
- artists to never scale anything. If they change the scale
- they should find a way to apply that scale to the vertices
- so that when it ends up making it to your app you can ignore
- scale.</p>
- <p>And, not just scale, in this case the cars are rotated and offset
- by their parent, the <code class="notranslate" translate="no">Cars</code> node. This will make it hard at runtime
- to move the cars around in world space. To be clear, in this case
- we want cars to drive around in world space which is why these
- issues are coming up. If something that is meant to be manipulated
- in a local space, like the moon revolving around the earth this
- is less of an issue.</p>
- <p>Going back to the function we wrote above to dump the scene graph,
- let's dump the position, rotation, and scale of each node.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function dumpVec3(v3, precision = 3) {
- + return `${v3.x.toFixed(precision)}, ${v3.y.toFixed(precision)}, ${v3.z.toFixed(precision)}`;
- +}
- function dumpObject(obj, lines, isLast = true, prefix = '') {
- const localPrefix = isLast ? '└─' : '├─';
- lines.push(`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`);
- + const dataPrefix = obj.children.length
- + ? (isLast ? ' │ ' : '│ │ ')
- + : (isLast ? ' ' : '│ ');
- + lines.push(`${prefix}${dataPrefix} pos: ${dumpVec3(obj.position)}`);
- + lines.push(`${prefix}${dataPrefix} rot: ${dumpVec3(obj.rotation)}`);
- + lines.push(`${prefix}${dataPrefix} scl: ${dumpVec3(obj.scale)}`);
- const newPrefix = prefix + (isLast ? ' ' : '│ ');
- const lastNdx = obj.children.length - 1;
- obj.children.forEach((child, ndx) => {
- const isLast = ndx === lastNdx;
- dumpObject(child, lines, isLast, newPrefix);
- });
- return lines;
- }
- </pre>
- <p>And the result from <a href="../examples/load-gltf-dump-scenegraph-extra.html">running it</a></p>
- <pre class="prettyprint showlinemods notranslate lang-text" translate="no">OSG_Scene [Scene]
- │ pos: 0.000, 0.000, 0.000
- │ rot: 0.000, 0.000, 0.000
- │ scl: 1.000, 1.000, 1.000
- └─RootNode_(gltf_orientation_matrix) [Object3D]
- │ pos: 0.000, 0.000, 0.000
- │ rot: -1.571, 0.000, 0.000
- │ scl: 1.000, 1.000, 1.000
- └─RootNode_(model_correction_matrix) [Object3D]
- │ pos: 0.000, 0.000, 0.000
- │ rot: 0.000, 0.000, 0.000
- │ scl: 1.000, 1.000, 1.000
- └─4d4100bcb1c640e69699a87140df79d7fbx [Object3D]
- │ pos: 0.000, 0.000, 0.000
- │ rot: 1.571, 0.000, 0.000
- │ scl: 1.000, 1.000, 1.000
- └─RootNode [Object3D]
- │ pos: 0.000, 0.000, 0.000
- │ rot: 0.000, 0.000, 0.000
- │ scl: 1.000, 1.000, 1.000
- ├─Cars [Object3D]
- * │ │ pos: -369.069, -90.704, -920.159
- * │ │ rot: 0.000, 0.000, 0.000
- * │ │ scl: 1.000, 1.000, 1.000
- │ ├─CAR_03_1 [Object3D]
- │ │ │ pos: 22.131, 14.663, -475.071
- │ │ │ rot: -3.142, 0.732, 3.142
- │ │ │ scl: 1.500, 1.500, 1.500
- │ │ └─CAR_03_1_World_ap_0 [Mesh]
- │ │ pos: 0.000, 0.000, 0.000
- │ │ rot: 0.000, 0.000, 0.000
- │ │ scl: 1.000, 1.000, 1.000
- </pre>
- <p>This shows us that <code class="notranslate" translate="no">Cars</code> in the original scene has had its rotation and scale
- removed and applied to its children. That suggests either whatever exporter was
- used to create the .GLTF file did some special work here or more likely the
- artist exported a different version of the file than the corresponding .blend
- file, which is why things don't match.</p>
- <p>The moral of that is I should have probably downloaded the .blend
- file and exported myself. Before exporting I should have inspected
- all the major nodes and removed any transformations.</p>
- <p>All these nodes at the top</p>
- <pre class="prettyprint showlinemods notranslate lang-text" translate="no">OSG_Scene [Scene]
- │ pos: 0.000, 0.000, 0.000
- │ rot: 0.000, 0.000, 0.000
- │ scl: 1.000, 1.000, 1.000
- └─RootNode_(gltf_orientation_matrix) [Object3D]
- │ pos: 0.000, 0.000, 0.000
- │ rot: -1.571, 0.000, 0.000
- │ scl: 1.000, 1.000, 1.000
- └─RootNode_(model_correction_matrix) [Object3D]
- │ pos: 0.000, 0.000, 0.000
- │ rot: 0.000, 0.000, 0.000
- │ scl: 1.000, 1.000, 1.000
- └─4d4100bcb1c640e69699a87140df79d7fbx [Object3D]
- │ pos: 0.000, 0.000, 0.000
- │ rot: 1.571, 0.000, 0.000
- │ scl: 1.000, 1.000, 1.000
- </pre>
- <p>are also a waste.</p>
- <p>Ideally the scene would consist of a single "root" node with no position,
- rotation, or scale. At runtime I could then pull all the children out of that
- root and parent them to the scene itself. There might be children of the root
- like "Cars" which would help me find all the cars but ideally it would also have
- no translation, rotation, or scale so I could re-parent the cars to the scene
- with the minimal amount of work.</p>
- <p>In any case the quickest though maybe not the best fix is to just
- adjust the object we're using to view the curve.</p>
- <p>Here's what I ended up with.</p>
- <p>First I adjusted the position of the curve and found values
- that seemed to work. I then hid it.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
- const points = curve.getPoints(250);
- const geometry = new THREE.BufferGeometry().setFromPoints(points);
- const material = new THREE.LineBasicMaterial({color: 0xff0000});
- curveObject = new THREE.Line(geometry, material);
- + curveObject.scale.set(100, 100, 100);
- + curveObject.position.y = -621;
- + curveObject.visible = false;
- material.depthTest = false;
- curveObject.renderOrder = 1;
- scene.add(curveObject);
- }
- </pre>
- <p>Then I wrote code to move the cars along the curve. For each car we pick a
- position from 0 to 1 along the curve and compute a point in world space using
- the <code class="notranslate" translate="no">curveObject</code> to transform the point. We then pick another point slightly
- further down the curve. We set the car's orientation using <code class="notranslate" translate="no">lookAt</code> and put the
- car at the mid point between the 2 points.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">// create 2 Vector3s we can use for path calculations
- const carPosition = new THREE.Vector3();
- const carTarget = new THREE.Vector3();
- function render(time) {
- ...
- - for (const car of cars) {
- - car.rotation.y = time;
- - }
- + {
- + const pathTime = time * .01;
- + const targetOffset = 0.01;
- + cars.forEach((car, ndx) => {
- + // a number between 0 and 1 to evenly space the cars
- + const u = pathTime + ndx / cars.length;
- +
- + // get the first point
- + curve.getPointAt(u % 1, carPosition);
- + carPosition.applyMatrix4(curveObject.matrixWorld);
- +
- + // get a second point slightly further down the curve
- + curve.getPointAt((u + targetOffset) % 1, carTarget);
- + carTarget.applyMatrix4(curveObject.matrixWorld);
- +
- + // put the car at the first point (temporarily)
- + car.position.copy(carPosition);
- + // point the car the second point
- + car.lookAt(carTarget);
- +
- + // put the car between the 2 points
- + car.position.lerpVectors(carPosition, carTarget, 0.5);
- + });
- + }
- </pre>
- <p>and when I ran it I found out for each type of car, their height above their origins
- are not consistently set and so I needed to offset each one
- a little.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const loadedCars = root.getObjectByName('Cars');
- const fixes = [
- - { prefix: 'Car_08', rot: [Math.PI * .5, 0, Math.PI * .5], },
- - { prefix: 'CAR_03', rot: [0, Math.PI, 0], },
- - { prefix: 'Car_04', rot: [0, Math.PI, 0], },
- + { prefix: 'Car_08', y: 0, rot: [Math.PI * .5, 0, Math.PI * .5], },
- + { prefix: 'CAR_03', y: 33, rot: [0, Math.PI, 0], },
- + { prefix: 'Car_04', y: 40, rot: [0, Math.PI, 0], },
- ];
- root.updateMatrixWorld();
- for (const car of loadedCars.children.slice()) {
- const fix = fixes.find(fix => car.name.startsWith(fix.prefix));
- const obj = new THREE.Object3D();
- car.getWorldPosition(obj.position);
- - car.position.set(0, 0, 0);
- + car.position.set(0, fix.y, 0);
- car.rotation.set(...fix.rot);
- obj.add(car);
- scene.add(obj);
- cars.push(obj);
- }
- </pre>
- <p>And the result.</p>
- <p></p><div translate="no" class="threejs_example_container notranslate">
- <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-gltf-animated-cars.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/load-gltf-animated-cars.html" target="_blank">click here to open in a separate window</a>
- </div>
- <p></p>
- <p>Not bad for a few minutes work.</p>
- <p>The last thing I wanted to do is turn on shadows.</p>
- <p>To do this I grabbed all the GUI code from the <a href="/docs/#api/en/lights/DirectionalLight"><code class="notranslate" translate="no">DirectionalLight</code></a> shadows
- example in <a href="shadows.html">the article on shadows</a> and pasted it
- into our latest code.</p>
- <p>Then, after loading, we need to turn on shadows on all the objects.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
- const gltfLoader = new GLTFLoader();
- gltfLoader.load('resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
- const root = gltf.scene;
- scene.add(root);
- + root.traverse((obj) => {
- + if (obj.castShadow !== undefined) {
- + obj.castShadow = true;
- + obj.receiveShadow = true;
- + }
- + });
- </pre>
- <p>I then spent nearly 4 hours trying to figure out why the shadow helpers
- were not working. It was because I forgot to enable shadows with</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">renderer.shadowMap.enabled = true;
- </pre>
- <p>😭</p>
- <p>I then adjusted the values until our <code class="notranslate" translate="no">DirectionLight</code>'s shadow camera
- had a frustum that covered the entire scene. These are the settings
- I ended up with.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
- const color = 0xFFFFFF;
- const intensity = 1;
- const light = new THREE.DirectionalLight(color, intensity);
- + light.castShadow = true;
- * light.position.set(-250, 800, -850);
- * light.target.position.set(-550, 40, -450);
- + light.shadow.bias = -0.004;
- + light.shadow.mapSize.width = 2048;
- + light.shadow.mapSize.height = 2048;
- scene.add(light);
- scene.add(light.target);
- + const cam = light.shadow.camera;
- + cam.near = 1;
- + cam.far = 2000;
- + cam.left = -1500;
- + cam.right = 1500;
- + cam.top = 1500;
- + cam.bottom = -1500;
- ...
- </pre>
- <p>and I set the background color to light blue.</p>
- <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
- -scene.background = new THREE.Color('black');
- +scene.background = new THREE.Color('#DEFEFF');
- </pre>
- <p>And ... shadows</p>
- <p></p><div translate="no" class="threejs_example_container notranslate">
- <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/load-gltf-shadows.html"></iframe></div>
- <a class="threejs_center" href="/manual/examples/load-gltf-shadows.html" target="_blank">click here to open in a separate window</a>
- </div>
- <p></p>
- <p>I hope walking through this project was useful and showed some
- good examples of working though some of the issues of loading
- a file with a scenegraph.</p>
- <p>One interesting thing is that comparing the .blend file to the .gltf
- file, the .blend file has several lights but they are not lights
- after being loaded into the scene. A .GLTF file is just a JSON
- file so you can easily look inside. It consists of several
- arrays of things and each item in an array is referenced by index
- else where. While there are extensions in the works they point
- to a problem with almost all 3d formats. <strong>They can never cover every
- case</strong>.</p>
- <p>There is always a need for more data. For example we manually exported
- a path for the cars to follow. Ideally that info could have been in
- the .GLTF file but to do that we'd need to write our own exporter
- and some how mark nodes for how we want them exported or use a
- naming scheme or something along those lines to get data from
- whatever tool we're using to create the data into our app.</p>
- <p>All of that is left as an exercise to the reader.</p>
- </div>
- </div>
- </div>
- <script src="../resources/prettify.js"></script>
- <script src="../resources/lesson.js"></script>
- </body></html>
|