|
|
刚毅的针织衫 · LEX/DBインターネット | ご利用案内· 1 年前 · |
|
|
爱看书的小狗 · 奋进新征程 建功新时代(十) - 求是网· 1 年前 · |
|
|
英姿勃勃的日记本 · InfluxDB 安装部署 | HelloDog· 1 年前 · |
|
|
刚毅的灭火器 · GIT permission denied ...· 1 年前 · |
|
|
冲动的消炎药 · 清华附中嘉兴实验学校公开招聘事业编制教师7名 ...· 2 年前 · |
In the last chapter, we created a simple toy train model using some of the built-in three.js geometries, and it quickly became clear that it would be hard to build anything complex or organic using just these. To create beautiful 3D models, a sophisticated modeling program is required. You can use three.js to build any kind of 3D application, however, building a modeling app from scratch would be a huge amount of work. A much simpler solution is to use an existing program and export your work for use in three.js… or, cheat, and download any of the millions of amazing models and other scene assets that are available for free in many places around the web.
In this chapter, we’ll show you how to load some models that were created in
Blender
, an open-source 3D graphics application that can be used for modeling, scene building, material creation, animation authoring, and more. Once you have created a model in Blender, you can export your work using a 3D format such as glTF, then use the
GLTFLoader
plugin
to bring the model into three.js.
There have been many attempts at creating a standard 3D asset exchange format over the last thirty years or so. FBX , OBJ (Wavefront) and DAE (Collada) formats were the most popular of these until recently, although they all have problems that prevented their widespread adoption. For example, OBJ doesn’t support animation, FBX is a closed format that belongs to Autodesk, and the Collada spec is overly complex, resulting in large files that are difficult to load.
However, recently, a newcomer called glTF has become the de facto standard format for exchanging 3D assets on the web. glTF ( GL Transmission Format ), sometimes referred to as the JPEG of 3D , was created by the Kronos Group , the same people who are in charge of WebGL, OpenGL, and a whole host of other graphics APIs. Originally released in 2017, glTF is now the best format for exchanging 3D assets on the web, and in many other fields. In this book, we’ll always use glTF, and if possible, you should do the same . It’s designed for sharing models on the web, so the file size is as small as possible and your models will load quickly.
However, since glTF is relatively new, your favorite application might not have an exporter yet. In that case, you can convert your models to glTF before using them, or use another loader such as the
FBXLoader
or
OBJLoader
. All three.js loaders work the same way, so if you do need to use another loader, everything from this chapter will still apply, with only minor differences.
Whenever we mention glTF, we mean glTF Version 2 . The original glTF Version 1 never found widespread use and is no longer supported by three.js
glTF files can contain models, animations, geometries, materials, lights, cameras, or even entire scenes. This means you can create an entire scene in an external program then load it into three.js.
glTF files come in standard and binary form. These have different extensions:
Both standard and binary glTF files may contain textures embedded in the file or may reference external textures. Since binary .glb files are considerably smaller, it’s best to use this type. On the other hand, uncompressed .gltf are easily readable in a text editor, so they may be useful for debugging purposes.
There are lots of free glTF models available on the three.js repo , and amongst these are three simple and beautiful models of a parrot , a flamingo , and a stork , created by the talented people at mirada.com . These three models are low poly , meaning they’ll run on even the most low-power of mobile devices, and they are even animated.
You can find these three files in the editor, in the assets/models/ folder. In this chapter, we’ll load Parrot.glb , Flamingo.glb , and Stork.glb and then add the bird-shaped meshes each file contains to our scene. In the next chapter, we’ll show you how to play the flying animation that is included with each bird.
If you’re working locally rather than using the inline code editor, you’ll need to set up a webserver . Otherwise, due to browser security restrictions, you won’t be able to load these files from your hard drive.
Whenever we load a model over the internet, we need to do so in a manner that ensures our app continues to run smoothly while the model is loading, and which can also gracefully handle failure if there is a network error. There are several ways to solve this problem using JavaScript, and there’s an entire chapter of the appendices dedicated to this subject.
In this chapter, we’ll use async functions to load the models, and we’re going to assume that you have at least some familiarity with these. If these are new to you, or you need a refresher, head over to the Asynchronous JavaScript .
GLTFLoader
Plugin
To load glTF files, first, you need to add
the
GLTFLoader
plugin
to your app. This works the same way as adding the
OrbitControls
plugin
. You can find the loader in
examples/jsm/loaders/GLTFLoader.js
on the repo, and we have also included this file in the editor. Go ahead and locate the file now.
Importing and creating an instance of the loader works like this:
GLTFLoader
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
const loader = new GLTFLoader();
You can use one instance of the loader to load any number of glTF files.
.load
and
.loadAsync
Methods
All three.js loaders have two methods for loading files: the old callback-based
.load
method, and the newer Promise based
.loadAsync
method. Again, refer to chapter
A.5
where we cover the difference between these two approaches in detail. Promises allow us to use async functions, which in turn results in much cleaner code, so throughout this book, we will always use
.loadAsync
.
GLTFLoader.loadAsync
const loader = new GLTFLoader();
const loadedData = await loader.loadAsync('path/to/yourModel.glb');
The
await
keyword means “wait here until the model has loaded”. If you have previously dealt with loading models using callbacks or Promises, then
await
will seem almost magical in its simplicity. However, we need to make a few adjustments to our code before we can use it since we can only use
await
inside a function that has been marked as
async
:
await
inside an
async
function
async function loadingSuccess() {
// inside an async function: OK!
await loader.loadAsync('yourModel.glb');
function loadingFail() {
// not inside an async function: ERROR!
await loader.loadAsync('yourModel.glb');
Another issue is that we cannot mark a constructor as async. A common solution to this is to create a separate
.init
method.
async
class Foobazzer {
constructor() {
// constructor cannot be async: ERROR!
await loader.loadAsync('yourModel.glb');
async init() {
// inside an async function: OK!
await loader.loadAsync('yourModel.glb')
This way, the constructor can handle the synchronous setup of the class, as usual, and then the init method will take over for asynchronous setup. We will use this approach, so we need to create a new
World.init
method.
World.init
method to handle asynchronous setup
Go ahead and add an empty
.init
method to World now, and make sure you mark it
async
. Splitting the setup into synchronous and asynchronous stages like this gives us full control over the setup of our app. In the synchronous stage, we will create everything that doesn’t rely on loaded assets, and in the asynchronous stage, we’ll create everything that does.
main
Function as Async
Over in
main.js
, first, we must also mark the main function as async. This is required so that we can call the async
World.init
method.
async
async function main() {
Now we can call both stages of setting up the World app. First, the synchronous constructor, as usual, then the new
.init
method to handle asynchronous tasks.
main.js
: call both synchronous and asynchronous stages of World setup
async function main() {
// Get a reference to the container element
const container = document.querySelector('#scene-container');
// create a new world
const world = new World(container);
// complete async tasks
await world.init();
// start the animation loop
world.start();
Catch Errors
No method of loading files is complete unless we can also handle any errors that occur. Errors can be as simple as a typo in the file name, or something more complex like a network error. Fortunately, with async functions, error handling is also simple. At the bottom of
main.js
, replace this line:
main.js
: calling the main() function
main();
… with:
main.js
: add a catch method to handle errors
main().catch((err) => {
console.error(err);
Now any errors will be logged to the console. In a real app, you might want to do more sophisticated error handling, such as displaying a message to the user to let them know that something went wrong. However, while we are in development mode, the most important thing is that all errors are logged to the console where we can see them.
Create the
birds.js
Module
Now everything is set up and we can go ahead and load our first model. Open (or create) the
components/birds/birds.js
module. Start by importing the
GLTFLoader
, then create an async
loadBirds
function. Inside the function, create an instance of the loader, and finally, export the function at the bottom of the file:
birds/birds.js
: initial structure
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
async function loadBirds() {
const loader = new GLTFLoader();
export { loadBirds };
The structure of this new module should be familiar to you since it’s the same as nearly
every other component we have created so far
. The only difference is the
async
keyword.
Over in World, update the list of imports:
World.js
: import components
import { loadBirds } from './components/birds/birds.js';
import { createCamera } from './components/camera.js';
import { createLights } from './components/lights.js';
import { createScene } from './components/scene.js';
Load the Parrot
Now, we’re ready to load the
Parrot.glb
file using
.loadAsync
. Once you have done so, log the loaded data to the console:
birds.js
: load the Parrot
async function loadBirds() {
const loader = new GLTFLoader();
const parrotData = await loader.loadAsync('/assets/models/Parrot.glb');
console.log('Squaaawk!', parrotData);
Next, call the
loadBirds
in
World.init
:
World.js
: load the birds!
async init() {
await loadBirds();
Data Returned by the
GLTFLoader
We need to take a deeper look at the data we have just loaded before we can add the model to our scene, so for now we’ve simply logged the data to the console. Open up the browser console (press F12). You should see the word
Squaaawk!
followed by an Object containing the loaded data. This Object contains meshes, animations, cameras, and other data from the file:
Data return by the
GLTFLoader
animations: [AnimationClip]
asset: {generator: "Khronos Blender glTF 2.0 I/O", version: "2.0"}
cameras: []
parser: GLTFParser {json: {…}, extensions: {…}, options: {…}, cache: {…}, primitiveCache: {…}, …}
scene: Scene {uuid: "1CF93318-696B-4411-B672-4C12C46DF7E1", name: "Scene", type: "Scene", parent: null, children: Array(0), …}
scenes: [Scene]
userData: {}
**proto**: Object
gltfData.animations
is an array of animation clips. Here, there’s a flying animation. We’ll make use of this
in the next chapter
.
gltfData.assets
contains metadata showing this glTF file was created using the
Blender
exporter.
gltfData.cameras
is an array of cameras. This file doesn’t contain any cameras, so the array is empty.
gltfData.parser
contains technical details about the
GLTFLoader
.
gltfData.scene
is a
Group
containing any meshes from the file.
This is where we’ll find the parrot model.
gltfData.scenes
: The glTF format supports storing multiple scenes in a single file. In practice, this feature is rarely used.
gltfData.userData
may contain additional non-standard data.
__proto__
is a standard property that every JavaScript object has, you can ignore that.
Usually, all you need is
.animations
,
.cameras
, and
.scene
(not
.scenes
!) and you can safely ignore everything else.
Process the Loaded Data
Extracting data from a glTF file usually follows a predictable pattern, especially if the file contains a single animated model, as these three files do. This means we can create a
setupModel
function and then run it on each of the three files. We’ll do this in a separate module. Open or create the
birds/setupModel.js
module, and create the function, following the now-familiar pattern:
birds/setupModel.js
: initial structure
function setupModel(data) {}
export { setupModel };
The idea of this function is that we can pass in the loaded data and get back the bird model, ready to be added to the scene. Next, import this new module into
birds.js
, then pass in the loaded data. Finally, return the results for use within World.
birds.js
: process loaded data
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { setupModel } from './setupModel.js';
async function loadBirds() {
const loader = new GLTFLoader();
const parrotData = await loader.loadAsync('/assets/models/Parrot.glb');
console.log('Squaaawk!', parrotData);
const parrot = setupModel(parrotData);
return { parrot }
Extract the Mesh from the Loaded Data
At this point, we have the unprocessed loaded data within the
setupModel
function. The next step is to extract the model, and then do any processing to prepare it for use. The amount of work we need to do here depends on the model, and what we want to do with it. Here, all we need to do is extract the mesh, but in the next chapter, we’ll have a bit more work to do as we connect the animation clip to the mesh.
Look at the loaded data in the console again, and expand the
gltfData.scene
. This a
Group
, and any meshes that are in the file will be
children of the group
. These can be accessed using the
group.children
array. If you look inside there, you’ll see that
glTF.scene.children
has only one object inside it, so that must be our parrot model.
Using this knowledge, we can finish the
setupModel
function:
setupModel.js
: extract the model from the loaded data
function setupModel(data) {
const model = data.scene.children[0];
return model;
Note A: if you click the toggle to complete the scene in the editor, then view the
gltfData.scene.children
array in the console, it will be empty. This is because, by the time you look at it, the mesh has already been removed and added to the scene.
Note B: you could also just add the
gltf.scene
to your scene since it’s a group. That would add an additional node to your scene graph but everything will still work. However, it’s best practice to keep your scene graph as simple as possible, since every node means additional calculations are required to render the scene.
Add the Mesh to the Scene
Over in World,
loadBirds
now returns the parrot mesh and you can add it to the scene:
World.js
: add the mesh to the scene
async init() {
const { parrot } = await loadBirds();
scene.add(parrot);
Load the Other Two Birds
You can use a single instance of the
GLTFLoader
to load any number of files. When performing multiple asynchronous operations with async functions, you should (in most cases) use
Promise.all
. We go into the reason for this in more detail
in the appendix
, but here’s the short version.
First, here’s the obvious way of loading the other two files:
Load multiple glTF files, the WRONG way
// Don't do this!
const parrotData = await loader.loadAsync('/assets/models/Parrot.glb');
const flamingoData = await loader.loadAsync('/assets/models/Flamingo.glb');
const storkData = await loader.loadAsync('/assets/models/Stork.glb');
const parrot = setupModel(parrotData);
const flamingo = setupModel(flamingoData);
const stork = setupModel(storkData);
There’s a problem with this approach.
As we stated above
,
await
means
wait here until the file has loaded
. This means the app will wait until the parrot has fully loaded,
then
start to load the flamingo, wait until
that
has fully loaded, and
finally
start to load the stork. Using this approach, loading will take nearly three times longer than it should.
Instead, we want all three files to load at the same time, and the simplest way of doing this is to use
Promise.all
.
birds.js
: load the other two file using
Promise.all
const [parrotData, flamingoData, storkData] = await Promise.all([
loader.loadAsync("/assets/models/Parrot.glb"),
loader.loadAsync("/assets/models/Flamingo.glb"),
loader.loadAsync("/assets/models/Stork.glb"),
Then we can process each file’s loaded data using the
setupModel
function. Once we do that, here’s our (nearly complete)
loadModels
function:
birds.js
: load and then process multiple glTF files
async function loadBirds() {
const loader = new GLTFLoader();
const [parrotData, flamingoData, storkData] = await Promise.all([
loader.loadAsync('/assets/models/Parrot.glb'),
loader.loadAsync('/assets/models/Flamingo.glb'),
loader.loadAsync('/assets/models/Stork.glb'),
console.log('Squaaawk!', parrotData);
const parrot = setupModel(parrotData);
const flamingo = setupModel(flamingoData);
const stork = setupModel(storkData);
return {
parrot,
flamingo,
stork,
Over in World, you now have all three models. Add them to your scene:
World.js
: add the second two birds to the scene
async init() {
const { parrot, flamingo, stork } = await loadBirds();
scene.add(parrot, flamingo, stork);
Great! Well…
Just like visiting the zoo!
Move the Birds into Position
It is possible for models loaded from a glTF file to have a position already specified, but that’s not the case here, so all three models start at the point $(0,0,0)$, all jumbled together on top of each other. We’ll adjust the position of each bird to make it look like they are flying in formation:
birds.js
: move the birds into position
const parrot = setupModel(parrotData);
parrot.position.set(0, 0, 2.5);
const flamingo = setupModel(flamingoData);
flamingo.position.set(7.5, 0, -10);
const stork = setupModel(storkData);
stork.position.set(0, -2.5, -10);
Final
birds.js
Module
The
birds.js
module is now complete. Here’s the final code:
birds.js
: final code
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { setupModel } from './setupModel.js';
async function loadBirds() {
const loader = new GLTFLoader();
const [parrotData, flamingoData, storkData] = await Promise.all([
loader.loadAsync('/assets/models/Parrot.glb'),
loader.loadAsync('/assets/models/Flamingo.glb'),
loader.loadAsync('/assets/models/Stork.glb'),
console.log('Squaaawk!', parrotData);
const parrot = setupModel(parrotData);
parrot.position.set(0, 0, 2.5);
const flamingo = setupModel(flamingoData);
flamingo.position.set(7.5, 0, -10);
const stork = setupModel(storkData);
stork.position.set(0, -2.5, -10);
return {
parrot,
flamingo,
stork,
export { loadBirds };
Center the Camera on the Parrot
The very last thing we’ll do is
adjust the
OrbitControls
target
. Currently, this is in its default position, the center of the scene. Now that we have moved the birds into formation, this ends up being somewhere around the tail of the parrot. It would look better if the camera focused on the center of the bird rather than its tail. We can easily set this up by copying the
parrot.position
into
controls.target
. However, to do so, we need to access
controls
within
.init
, so first, let’s convert it to a module-scoped variable.
World.js
: make
controls
a module scoped variable
let camera;
let controls;
let renderer;
let scene;
let loop;
renderer = createRenderer();
scene = createScene();
loop = new Loop(camera, scene, renderer);
container.append(renderer.domElement);
controls = createControls(camera, renderer.domElement);
const { ambientLight, mainLight } = createLights();
loop.updatables.push(controls);
scene.add(ambientLight, mainLight);
const resizer = new Resizer(container, camera, renderer);
Now, the controls are accessible from
.init
and we can move the target to the center of the parrot.
World.js
: target the parrot with the camera
async init() {
const { parrot, flamingo, stork } = await loadBirds();
// move the target to the center of the front bird
controls.target.copy(parrot.position);
scene.add(parrot, flamingo, stork);
Next up, we’ll introduce the three.js animation system and show you how to play the animation clips that were loaded alongside the bird models.
Challenges
Look at that parrot hogging the limelight! Switch around the bird’s positions to give the stork and the flamingo each a turn in leading the flock.
Alternatively, leave the birds in place and try making the
controls.target
focus on one of the other two birds instead of the parrot.
Medium
Add a
<button>
element with the text
Switch Focus
. Whenever you click this button, the camera should focus on the next bird. You can implement this however you like, but, if you want to do it in keeping with our work so far, you should set up the buttons inside
main.js
and then
expand the World class interface
with a method to move focus onto the next bird. You can call this method
World.focusNext
or something similar.
Once you have implemented the button above, you’ll have three camera views, one for each bird. Add a fourth view which is a zoomed-out overview of the scene that allows you to see all three birds. For this fourth view, you may need to adjust the
camera.position
as well as the
controls.target
.
Now, make the camera smoothly animate from one viewpoint to the next. You will have to animate the camera.position and the controls.target at the same time. The best place to do this is within the
controls.tick
method.
0.2:
What Do You Need to Run a three.js App?
0.3:
Welcome to the Community: three.js Around the Web
0.4:
three.js on GitHub - Where the Magic Happens
0.5:
How to Include three.js in Your Projects
0.7:
Using three.js with React, Vue.js, Angular, Svelte, TypeScript...
1:
Getting Started: Here's Where the Real Fun Begins!
1.1:
The Structure of a three.js App
1.2:
Your First three.js Scene: Hello, Cube!
1.3:
Introducing the World App
1.4:
Physically Based Rendering and Lighting
1.5:
Transformations and Coordinate Systems
1.6:
Making Our Scenes Responsive (and also Dealing with Jaggies)
1.7:
The Animation Loop
1.8:
A Brief Introduction to Texture Mapping
1.9:
Extend three.js With a Camera Controls Plugin
1.10:
Ambient Lighting: Illumination from Every Direction
1.11:
Organizing Your Scenes
1.12:
The Built-In Geometries
1.13:
Load 3D Models in glTF Format
1.14:
The three.js Animation System
A:
Appendices
A.1:
HTML and CSS Used in This Book
A.2:
JavaScript Reference
A.3:
The Document Object Model and DOM API
A.4:
JavaScript Modules
A.5:
Asynchronous JavaScript
B.1:
Dealing with Different three.js Versions
B.2:
The Big List of three.js Tips and Tricks!
|
|
刚毅的针织衫 · LEX/DBインターネット | ご利用案内 1 年前 |
|
|
爱看书的小狗 · 奋进新征程 建功新时代(十) - 求是网 1 年前 |
|
|
英姿勃勃的日记本 · InfluxDB 安装部署 | HelloDog 1 年前 |