资源
0
0.4 GitHub 上的 three.js - 魔法出现的地方
mrdoob/three.js: JavaScript 3D Library. (github.com) 中:
build/three.module.js
是运行所需的文件。
examples/
代码示例。
src/
源代码。
0.5 如何在你的项目中引入 three.js
Node.js 直接导入
在项目文件夹中,初始化 npm:
安装 three.js:
1 npm install --save three
导入:
1 import {XXX , XXX , XXX } from 'three/build/three.module.js' ;
直接从 mrdoob/three.js: JavaScript 3D Library. (github.com) 里下载 build/three.module.js
要引入的话,直接:
1 <script type ="module" src ="three.module.js" > </script >
1-入门:真正的乐趣从这里开始!
1.1 Three.js 应用的结构
index.html main.css main.js vendor/ assets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <link href ="./styles/main.css" rel ="stylesheet" type ="text/css" > <body > <div id ="scene-container" > </div > </body > <script type ="module" src ="./src/main.js" > </script > </html >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 body { margin : 0 ; overflow : hidden; text-align : center; font-size : 12px ; font-family : Sans-Serif; color : #444 ; }h1 { position : absolute; width : 100% ; z-index : 1 ; }#scene-container { position : absolute; width : 100% ; height : 100% ; background-color : skyblue; }
npm 方式导入:
1 2 3 4 5 6 7 8 import {Camera ,Group ,Scene , } from 'three' ;import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' ;import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' ;
cdn 方式导入:
1 2 3 import { Camera , Group , Scene } from "https://cdn.skypack.dev/three@0.132.2" ;import { OrbitControls } from "https://cdn.skypack.dev/three@0.132.2/examples/jsm/controls/OrbitControls.js?module" ;import { GLTFLoader } from "https://cdn.skypack.dev/three@0.132.2/examples/jsm/loaders/GLTFLoader.js?module" ;
直接从 mrdoob/three.js: JavaScript 3D Library. (github.com) 里下载 build/three.module.js
,放在vendor/three/build/
下,本地导入:
1 2 3 4 5 6 7 import { XXX , XXX } from '../vendor/three/build/three.module.js' ; ...自由操作...
使用的任何非 HTML、CSS 或 JavaScript 的东西都在这里 :纹理、3D 模型、字体、声音等等。
1.2 你的第一个 three.js 场景:你好,立方体!
编写 main.js
:
1 2 3 4 5 6 7 8 9 10 import { Scene , PerspectiveCamera , WebGLRenderer , BoxGeometry , MeshBasicMaterial , Mesh , Color } from '../vendor/three/build/three.module.js' ;
1 const container = document .querySelector ('#scene-container' );
1 const scene = new Scene ();
1 scene.background = new Color ('skyblue' );
定义一个透视相机:
fov
:视野范围
aspect
:摄影机宽高比(容器的宽高比)
near
:近裁剪面的距离。只有在这个距离之后的物体才会被摄像机捕捉到,太近的物体会被忽略。这个值应该设置为正数,并且尽可能小以避免裁剪近处的重要细节,但过小的值可能会导致深度缓冲问题。
far
:远裁剪面的距离。任何在这个距离之外的物体都不会被摄像机捕捉到。这个值决定了摄像机可以看到多远的场景,设置得过大可能会影响渲染性能和深度缓冲的精度。
定义好后,放在场景的 ( 0 , 0 , 1 0 ) (0, 0, 10) ( 0 , 0 , 1 0 ) 处。
1 2 3 4 5 6 7 const fov = 35 ; const aspect = container.clientWidth / container.clientHeight ;const near = 0.1 ; const far = 100 ; const camera = new PerspectiveCamera (fov, aspect, near, far); camera.position .set (0 , 0 , 10 );
定义一个立方体网格,尺寸设为 ( 2 , 2 , 2 ) (2, 2, 2) ( 2 , 2 , 2 ) :
1 2 const geometry = new BoxGeometry (2 , 2 , 2 );
1 const material = new MeshBasicMaterial ({ color : 0x44aa88 });
1 const cube = new Mesh (geometry, material);
1 2 3 4 5 6 7 const renderer = new WebGLRenderer (); renderer.setSize (container.clientWidth , container.clientHeight ); renderer.setPixelRatio (window .devicePixelRatio ); container.appendChild (renderer.domElement ); renderer.render (scene, camera);
演示
我想,这段代码在 Blender 里的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import bpyimport numpy as np bpy.ops.object .select_all(action='SELECT' ) bpy.ops.object .delete(use_global=False ) bpy.context.scene.render.engine = 'CYCLES' world = bpy.data.worlds['World' ] world.use_nodes = True nodes = world.node_tree.nodes nodes.clear() bg_node = nodes.new(type ='ShaderNodeBackground' ) bg_node.inputs[0 ].default_value = (135 /255 , 206 /235 , 235 /255 , 1 ) world_output_node = nodes.new(type ='ShaderNodeOutputWorld' ) links = world.node_tree.links link = links.new(bg_node.outputs[0 ], world_output_node.inputs[0 ]) bpy.ops.mesh.primitive_cube_add(size=2 , location=(0 , 0 , 0 )) cube = bpy.context.object mat = bpy.data.materials.new(name="CubeMaterial" ) mat.diffuse_color = (68 /255 , 170 /255 , 136 /255 , 1 ) if not cube.data.materials: cube.data.materials.append(mat)else : cube.data.materials[0 ] = mat bpy.ops.object .camera_add(location=(0 , 0 , 10 )) camera = bpy.context.object camera.data.angle = 35 * (np.pi / 180 ) camera.data.clip_start = 0.1 camera.data.clip_end = 100 bpy.context.scene.render.resolution_x = 1848 bpy.context.scene.render.resolution_y = 1206 bpy.context.scene.render.resolution_percentage = 100
1.3 介绍世界应用程序
这章主要是将之前的代码模块化。
main.js world.js renderer.js scene.js camera.js cube.js resizer.js
获取 container
,导入 World.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { World } from './World/world.js' ;function main ( ) { const container = document .querySelector ('#scene-container' ); const world = new World (container); world.render (); }main ();
把之前定义摄像机、创建立方体、设置场景环境、渲染器、窗口适应之类的全扔进去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { createCamera } from './components/camera.js' ;import { createCube } from './components/cube.js' ;import { createScene } from './components/scene.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/resizer.js' ;let camera;let renderer;let scene;class World { constructor (container ) { camera = createCamera (); scene = createScene (); renderer = createRenderer (); container.append (renderer.domElement ); const cube = createCube (); scene.add (cube); const resizer = new Resizer (container, camera, renderer); } render ( ) { renderer.render (scene, camera); } }export { World };
渲染器系统:
1 2 3 4 5 6 7 8 9 import { WebGLRenderer } from 'three' ;function createRenderer ( ) { const renderer = new WebGLRenderer (); return renderer; }export { createRenderer };
场景组件:
1 2 3 4 5 6 7 8 9 10 11 import { Color , Scene } from 'three' ;function createScene ( ) { const scene = new Scene (); scene.background = new Color ('skyblue' ); return scene; }export { createScene };
相机组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { PerspectiveCamera } from 'three' ;function createCamera ( ) { const camera = new PerspectiveCamera ( 35 , 1 , 0.1 , 100 , ); camera.position .set (0 , 0 , 10 ); return camera; }export { createCamera };
立方体组件,它包括创建 几何体、材质和 网格。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { BoxBufferGeometry , Mesh , MeshBasicMaterial } from 'three' ;function createCube ( ) { const geometry = new BoxBufferGeometry (2 , 2 , 2 ); const material = new MeshBasicMaterial (); const cube = new Mesh (geometry, material); return cube; }export { createCube };
使得场景可以占据整个窗口的大小:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Resizer { constructor (container, camera, renderer ) { camera.aspect = container.clientWidth / container.clientHeight ; camera.updateProjectionMatrix (); renderer.setSize (container.clientWidth , container.clientHeight ); renderer.setPixelRatio (window .devicePixelRatio ); } }
1.4 基于物理的渲染和照明
three.js 也是使用**基于物理的渲染 (PBR)**的。
根据之前模块化后的程序,继续修改:
renderer.js light.js World.js
告诉渲染器启动基于物理的渲染。
1 2 3 4 5 6 7 8 9 function createRenderer ( ) { const renderer = new WebGLRenderer ({ antialias : true }); renderer.physicallyCorrectLights = true ; return renderer; }export { createRenderer };
定义一个灯光:
光强设为 8 8 8
位置在 ( 1 0 , 1 0 , 1 0 ) (10, 10, 10) ( 1 0 , 1 0 , 1 0 ) ,照向原点
1 2 3 4 5 6 7 8 9 10 11 12 13 import { DirectionalLight } from 'three' ;function createLights ( ) { const light = new DirectionalLight ('white' , 8 ); light.position .set (10 , 10 , 10 ); return light; }export { createLights };
scene.add()
里给场景添加光源!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import { createCamera } from './components/camera.js' ;import { createCube } from './components/cube.js' ;import { createScene } from './components/scene.js' ;import { createLights } from './components/lights.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/resizer.js' ;let camera;let renderer;let scene;class World { constructor (container ) { camera = createCamera (); scene = createScene (); renderer = createRenderer (); container.append (renderer.domElement ); const cube = createCube (); const light = createLights (); scene.add (cube, light); const resizer = new Resizer (container, camera, renderer); } render ( ) { renderer.render (scene, camera); } }export { World };
演示
1.5 变换、坐标系和场景图
emmm 跟 Unity 里差不多吧。
cube.js
中可以修改 cube 的 position
、rotation
和 scale
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import { BoxBufferGeometry , MathUtils , Mesh , MeshStandardMaterial , } from 'three' ;function createCube ( ) { const geometry = new BoxBufferGeometry (2 , 2 , 2 ); const material = new MeshStandardMaterial ({ color : 'purple' }); const cube = new Mesh (geometry, material); cube.position .x = -0.5 ; cube.position .y = -0.1 ; cube.position .z = 1 ; cube.scale .x = 1.25 ; cube.scale .y = 0.25 ; cube.scale .z = 0.5 ; cube.rotation .x = MathUtils .degToRad (-60 ); cube.rotation .y = MathUtils .degToRad (-45 ); cube.rotation .z = MathUtils .degToRad (60 ); return cube; }export { createCube };
还可以把对象放在其他对象中,作其子对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 scene.add (mesh); scene.children ; scene.add (light); scene.children ; scene.children [0 ]; scene.children [1 ];
1.6 使我们的场景具有响应性(以及处理 Jaggies)
抗锯齿
renderer.js
中启用抗锯齿:
1 const renderer = new WebGLRenderer ({ antialias : true });
无缝处理浏览器窗口大小变化
修改 resizer.js
:
将 setSize
封装成一个函数,当用户改变窗口大小(触发 window.addEventListener('resize', () => {});
)时,重新 setSize()
定义一个 onResize()
函数,便于引用的时候重写它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const setSize = (container, camera, renderer ) => { camera.aspect = container.clientWidth / container.clientHeight ; camera.updateProjectionMatrix (); renderer.setSize (container.clientWidth , container.clientHeight ); renderer.setPixelRatio (window .devicePixelRatio ); };class Resizer { constructor (container, camera, renderer ) { setSize (container, camera, renderer); window .addEventListener ('resize' , () => { setSize (container, camera, renderer); this .onResize (); }); } onResize ( ) { } }export { Resizer };
修改 World.js
:
重写 onResize()
的逻辑,窗口变换时,重新渲染画面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import { createCamera } from './components/camera.js' ;import { createCube } from './components/cube.js' ;import { createScene } from './components/scene.js' ;import { createLights } from './components/lights.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/resizer.js' ;let camera;let renderer;let scene;class World { constructor (container ) { camera = createCamera (); scene = createScene (); renderer = createRenderer (); container.append (renderer.domElement ); const cube = createCube (); const light = createLights (); scene.add (cube, light); const resizer = new Resizer (container, camera, renderer); resizer.onResize = () => { this .render (); }; } render ( ) { renderer.render (scene, camera); } }export { World };
演示
1.7 动画循环
systems/Loops.js World.js main.js cube.js
创建一个 Loops.js
用于循环:
constructor()
构造函数:
camera
摄像机。
scene
场景。
renderer
渲染器。
updatables
要在循环中更新的对象的列表。
start()
使用 this.renderer.setAnimationLoop(() => {});
开启循环。
this.tick();
开启计时器。
this.renderer.render(this.scene, this.camera);
循环中不断渲染帧。
stop()
清空循环:this.renderer.setAnimationLoop(null);
tick()
:
const delta = clock.getDelta();
用于衡量渲染渲染一帧花了多少时间(单位:秒)。
随后调用 this.updatables
里物体的 tick()
(类似 Unity 里的 Update()
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import { Clock } from "three" ;const clock = new Clock ();class Loop { constructor (camera, scene, renderer ) { this .camera = camera; this .scene = scene; this .renderer = renderer; this .updatables = []; } start ( ) { this .renderer .setAnimationLoop (() => { this .tick (); this .renderer .render (this .scene , this .camera ); }); } stop ( ) { this .renderer .setAnimationLoop (null ); } tick ( ) { const delta = clock.getDelta (); for (const object of this .updatables ) { object.tick (delta); } } }export { Loop };
import { Loop } from './systems/Loop.js';
导入相应 JS。
let loop;
和 loop = new Loop(camera, scene, renderer);
将循环创建为模块作用域变量 ,如 camera
、renderer
和 scene
一样,因为我们不希望从 World
类外部访问它。
loop.updatables.push(cube);
将 cube
压入 loop.updatables
中,使其可以循环。
start()
和 stop()
分别就是 loop
中的 start()
和 stop()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import { createCamera } from './components/camera.js' ;import { createCube } from './components/cube.js' ;import { createLights } from './components/lights.js' ;import { createScene } from './components/scene.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/Resizer.js' ;import { Loop } from './systems/Loop.js' ;let camera;let renderer;let scene;let loop;class World { constructor (container ) { camera = createCamera (); renderer = createRenderer (); scene = createScene (); loop = new Loop (camera, scene, renderer); container.append (renderer.domElement ); const cube = createCube (); const light = createLights (); loop.updatables .push (cube); scene.add (cube, light); const resizer = new Resizer (container, camera, renderer); } render ( ) { renderer.render (scene, camera); } start ( ) { loop.start (); } stop ( ) { loop.stop (); } }export { World };
渲染的入口改为 world.start();
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { World } from './World/World.js' ;function main ( ) { const container = document .querySelector ('#scene-container' ); const world = new World (container); world.start (); }main ();
定义 cube.tick
:每秒旋转 3 0 ∘ 30^\circ 3 0 ∘ 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import { BoxGeometry , Mesh , MeshStandardMaterial , MathUtils } from 'three' ;function createCube ( ) { const geometry = new BoxGeometry (2 , 2 , 2 ); const material = new MeshStandardMaterial ({ color : "purple" }); const cube = new Mesh (geometry, material); cube.rotation .set (-0.5 , -0.1 , 0.8 ); const radiansPerSecond = MathUtils .degToRad (30 ); cube.tick = (delta ) => { cube.rotation .z += radiansPerSecond * delta; cube.rotation .x += radiansPerSecond * delta; cube.rotation .y += radiansPerSecond * delta; }; return cube; }export { createCube };
演示
1.8 纹理映射
给立方体贴图:
修改 components/cube.js
:
定义 createMaterial()
,将控制物体材质的代码移到另一个函数中。
const textureLoader = new TextureLoader();
定义一个加载纹理贴图的类。
const texture = textureLoader.load('./assets/textures/uv-test-bw.png',);
加载贴图,使其转换成能材质类读取的形式。
const material = new MeshStandardMaterial({map: texture,});
定义材质。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import { BoxGeometry , Mesh , MeshStandardMaterial , MathUtils , TextureLoader } from 'three' ;function createMaterial ( ) { const textureLoader = new TextureLoader (); const texture = textureLoader.load ( './assets/textures/uv-test-bw.png' , ); const material = new MeshStandardMaterial ({ map : texture, }); return material; }function createCube ( ) { const geometry = new BoxGeometry (2 , 2 , 2 ); const material = createMaterial (); const cube = new Mesh (geometry, material); cube.rotation .set (-0.5 , -0.1 , 0.8 ); const radiansPerSecond = MathUtils .degToRad (30 ); cube.tick = (delta ) => { cube.rotation .z += radiansPerSecond * delta; cube.rotation .x += radiansPerSecond * delta; cube.rotation .y += radiansPerSecond * delta; }; return cube; }export { createCube };
演示
1.9 使用相机控制插件扩展 three.js
本节将使用新插件:three/examples/jsm/controls/OrbitControls.js
!
OrbitControls 是 Three.js 库中的一个辅助控制器,允许用户通过鼠标操作来旋转、缩放和平移场景中的相机,从而实现对场景的交互式查看。
如果是本地加载的,还要修改一下 OrbitControls.js
里的导入逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 import { EventDispatcher , MOUSE , Quaternion , Spherical , TOUCH , Vector2 , Vector3 , Plane , Ray , MathUtils } from '../../../build/three.module.js' ;
controls.js World.js
const controls = new OrbitControls(camera, canvas);
:通过传入相机(camera
)和 canvas 元素(canvas
),创建了一个新的 OrbitControls
实例。
controls.enableDamping = true;
:启用阻尼效果(惯性),使相机移动更加平滑自然。阻尼效果需要在动画循环中不断更新控制器状态。
controls.tick = () => controls.update();
:在控制器上添加了一个名为tick
的方法,这个方法简单地调用了controls.update()
。这是因为当启用阻尼或自动旋转时,需要在每一帧更新控制器的状态以获得平滑的动画效果。在主动画循环中调用此tick
方法可以达到这个目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' ;function createControls (camera, canvas ) { const controls = new OrbitControls (camera, canvas); controls.enableDamping = true ; controls.tick = () => controls.update (); return controls; }export { createControls };
import { createControls } from './systems/controls.js';
导入 controls.js
。
const controls = createControls(camera, renderer.domElement);
将参数传给 createControls()
以激活辅助控制器。
loop.updatables.push(controls);
使得场景能够更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import { createCamera } from './components/camera.js' ;import { createCube } from './components/cube.js' ;import { createLights } from './components/lights.js' ;import { createScene } from './components/scene.js' ;import { createControls } from './systems/controls.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/Resizer.js' ;import { Loop } from './systems/Loop.js' ;let camera;let renderer;let scene;let loop;class World { constructor (container ) { camera = createCamera (); renderer = createRenderer (); scene = createScene (); loop = new Loop (camera, scene, renderer); container.append (renderer.domElement ); const controls = createControls (camera, renderer.domElement ); const cube = createCube (); const light = createLights (); loop.updatables .push (controls); scene.add (cube, light); const resizer = new Resizer (container, camera, renderer); } render ( ) { renderer.render (scene, camera); } start ( ) { loop.start (); } stop ( ) { loop.stop (); } }export { World };
演示
按需渲染
如果只使用相机控制插件,而不使用任何动画,那么使用按需渲染可以节约资源。
World.js main.js
controls.addEventListener('change', () => {renderer.render(scene, camera);});
设置事件监听,当用户控制相机时,重新渲染画面(而不是循环渲染画面)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import { createCamera } from './components/camera.js' ;import { createCube } from './components/cube.js' ;import { createLights } from './components/lights.js' ;import { createScene } from './components/scene.js' ;import { createControls } from './systems/controls.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/Resizer.js' ;let camera;let renderer;let scene;class World { constructor (container ) { camera = createCamera (); renderer = createRenderer (); scene = createScene (); container.append (renderer.domElement ); const controls = createControls (camera, renderer.domElement ); controls.addEventListener ('change' , () => { renderer.render (scene, camera); }); const cube = createCube (); const light = createLights (); scene.add (cube, light); const resizer = new Resizer (container, camera, renderer); } render ( ) { renderer.render (scene, camera); } }export { World };
取消动画循环:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { World } from './World/World.js' ;function main ( ) { const container = document .querySelector ('#scene-container' ); const world = new World (container); world.render (); }main ();
演示
OrbitControls 配置
可以通过设置参数以配置 OrbitControls 的功能:使用相机控制插件扩展 three.js | Discover three.js (discoverthreejs.com)
1.10 环境光:来自各个方向的光照
之前的立方体背面完全是黑色,这是因为缺少环境光。
light.js World.js
AmbientLight
是在 three.js 中伪造间接照明的最廉价的方法。这种类型的光会从各个方向向场景中的每个对象添加恒定数量的光照。
const ambientLight = new AmbientLight('white', 2);
来自 HemisphereLight
的光在场景顶部的天空颜色和场景底部的地面颜色之间渐变。与AmbientLight
一样,此灯不尝试物理精度。相反,HemisphereLight
是在观察到在您发现人类的许多情况下创建的,最亮的光来自场景的顶部,而来自地面的光通常不太亮。
const ambientLight = new HemisphereLight('white', 'darkslategrey', 5, );
创建一个 HemisphereLight
作为环境光
天空颜色:white
地面颜色:darkslategrey
强度:5
return { ambientLight, mainLight };
返回场景光与主光源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { AmbientLight , DirectionalLight , HemisphereLight } from 'three' ;function createLights ( ) { const ambientLight = new HemisphereLight ( 'white' , 'darkslategrey' , 5 , ); const mainLight = new DirectionalLight ('white' , 5 ); mainLight.position .set (10 , 10 , 10 ); return { ambientLight, mainLight }; }export { createLights };
const { ambientLight, mainLight } = createLights();
:获取函数返回的光源对象。(是否可以修改成列表以减少耦合性?)
scene.add(ambientLight, mainLight, cube);
:将这些光源和立方体对象加入场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import { createCamera } from './components/camera.js' ;import { createCube } from './components/cube.js' ;import { createLights } from './components/lights.js' ;import { createScene } from './components/scene.js' ;import { createControls } from './systems/controls.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/Resizer.js' ;import { Loop } from './systems/Loop.js' ;let camera;let renderer;let scene;let loop;class World { constructor (container ) { camera = createCamera (); renderer = createRenderer (); scene = createScene (); loop = new Loop (camera, scene, renderer); container.append (renderer.domElement ); const controls = createControls (camera, renderer.domElement ); const cube = createCube (); const { ambientLight, mainLight } = createLights (); loop.updatables .push (controls); scene.add (ambientLight, mainLight, cube); const resizer = new Resizer (container, camera, renderer); } render ( ) { renderer.render (scene, camera); } start ( ) { loop.start (); } stop ( ) { loop.stop (); } }export { World };
演示
1.11 组织你的场景
meshGroup.js World.js
源教程使用的是 SphereBufferGeometry
在我这里似乎已经过期,应该改成 SphereGeometry
。(用于创建球体形状的几何体)
const group = new Group();
使用 Group
类创建一个新的组合对象 group
。这个组合将用来容纳所有的球体,但它自身是不可见的。
const geometry = new SphereGeometry(0.25, 16, 16);
使用 Group
类创建一个新的组合对象 group
。这个组合将用来容纳所有的球体,但它自身是不可见的。(就是个空节点)
group.add(protoSphere);
将这个 const protoSphere = new Mesh(geometry, material);
作为 group
的子对象。
for (let i = 0; i < 1; i += 0.05) {}
:创建更多球体。
group.scale.multiplyScalar(2);
:将整个组合的大小放大两倍。
const radiansPerSecond = MathUtils.degToRad(30);
尝试将整个 group
每秒旋转 3 0 ∘ 30^\circ 3 0 ∘ 。
group.tick = (delta) => {group.rotation.z -= delta * radiansPerSecond;};
:应用动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import { SphereGeometry , Group , MathUtils , Mesh , MeshStandardMaterial , } from 'three' ;function createMeshGroup ( ) { const group = new Group (); const geometry = new SphereGeometry (0.25 , 16 , 16 ); const material = new MeshStandardMaterial ({ color : 'indigo' , }); const protoSphere = new Mesh (geometry, material); group.add (protoSphere); for (let i = 0 ; i < 1 ; i += 0.05 ) { const sphere = protoSphere.clone (); sphere.position .x = Math .cos (2 * Math .PI * i); sphere.position .y = Math .sin (2 * Math .PI * i); sphere.scale .multiplyScalar (0.01 + i); group.add (sphere); } group.scale .multiplyScalar (2 ); const radiansPerSecond = MathUtils .degToRad (30 ); group.tick = (delta ) => { group.rotation .z -= delta * radiansPerSecond; }; return group; }export { createMeshGroup };
把之前的 cube
换成了 meshGroup
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import { createCamera } from './components/camera.js' ;import { createLights } from './components/lights.js' ;import { createMeshGroup } from './components/meshGroup.js' ;import { createScene } from './components/scene.js' ;import { createControls } from './systems/controls.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/Resizer.js' ;import { Loop } from './systems/Loop.js' ;let camera;let renderer;let scene;let loop;class World { constructor (container ) { camera = createCamera (); renderer = createRenderer (); scene = createScene (); loop = new Loop (camera, scene, renderer); container.append (renderer.domElement ); const controls = createControls (camera, renderer.domElement ); const { ambientLight, mainLight } = createLights (); const meshGroup = createMeshGroup (); loop.updatables .push (controls, meshGroup); scene.add (ambientLight, mainLight, meshGroup); const resizer = new Resizer (container, camera, renderer); } render ( ) { renderer.render (scene, camera); } start ( ) { loop.start (); } stop ( ) { loop.stop (); } }export { World };
演示
1.12 使用内置几何体获得创意
文件组织结构如下:
components/Train/
:创建一个火车类
Train.js
:组装火车,并设置轮子移动动画。
geometries.js
:定义构成火车的组件。
materials.js
:定义火车的材质。
meshes.js
:设置火车的组建的 transform。
components/helpers.js
:在场景中显示坐标系。
World.js helpers.js
import {createAxesHelper, createGridHelper,} from './components/helpers.js';
和 import { Train } from './components/Train/Train.js';
:导入相关库。
const train = new Train();
、loop.updatables.push(controls, train);
、scene.add(ambientLight, mainLight, train);
:定义火车类,加载动画并放置在场景中。
scene.add(createAxesHelper(), createGridHelper());
:在场景中显示坐标轴。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import { createCamera } from './components/camera.js' ;import { createAxesHelper, createGridHelper, } from './components/helpers.js' ;import { createLights } from './components/lights.js' ;import { createScene } from './components/scene.js' ;import { Train } from './components/Train/Train.js' ;import { createControls } from './systems/controls.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/Resizer.js' ;import { Loop } from './systems/Loop.js' ;let camera;let renderer;let scene;let loop;class World { constructor (container ) { camera = createCamera (); renderer = createRenderer (); scene = createScene (); loop = new Loop (camera, scene, renderer); container.append (renderer.domElement ); const controls = createControls (camera, renderer.domElement ); const { ambientLight, mainLight } = createLights (); const train = new Train (); loop.updatables .push (controls, train); scene.add (ambientLight, mainLight, train); const resizer = new Resizer (container, camera, renderer); scene.add (createAxesHelper (), createGridHelper ()); } render ( ) { renderer.render (scene, camera); } start ( ) { loop.start (); } stop ( ) { loop.stop (); } }export { World };
function createAxesHelper() {}
:创建坐标轴。
const helper = new AxesHelper(3);
:显示的坐标轴长度为 3 3 3 。
helper.position.set(-3.5, 0, -3.5);
:将坐标轴放置在 ( − 3 . 5 , 0 , − 3 . 5 ) (-3.5, 0, -3.5) ( − 3 . 5 , 0 , − 3 . 5 ) 的位置。
function createGridHelper() {}
:创建网格。
const helper = new GridHelper(6);
:在 x O z xOz x O z 平面创建 6 × 6 6 \times 6 6 × 6 的网格,大小为 1 1 1 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { AxesHelper , GridHelper } from 'three' ;function createAxesHelper ( ) { const helper = new AxesHelper (3 ); helper.position .set (-3.5 , 0 , -3.5 ); return helper; }function createGridHelper ( ) { const helper = new GridHelper (6 ); return helper; }export { createAxesHelper, createGridHelper };
Train.js geometries.js materials.js meshes.js
从 ./meshes.js
导入 createMeshes
,将这些 meshes
放在自己节点下。
tick(delta) {}
让车轮以每秒 2 4 ∘ 24^\circ 2 4 ∘ 的速度旋转。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import { Group , MathUtils } from 'three' ;import { createMeshes } from './meshes.js' ;const wheelSpeed = MathUtils .degToRad (24 );class Train extends Group { constructor ( ) { super (); this .meshes = createMeshes (); this .add ( this .meshes .nose , this .meshes .cabin , this .meshes .chimney , this .meshes .smallWheelRear , this .meshes .smallWheelCenter , this .meshes .smallWheelFront , this .meshes .bigWheel , ); } tick (delta ) { this .meshes .bigWheel .rotation .y += wheelSpeed * delta; this .meshes .smallWheelRear .rotation .y += wheelSpeed * delta; this .meshes .smallWheelCenter .rotation .y += wheelSpeed * delta; this .meshes .smallWheelFront .rotation .y += wheelSpeed * delta; } }export { Train };
原教程使用的是 BoxGeometry
和 CylinderBufferGeometry
,这似乎已弃用,应分别改为 BoxGeometry,
和 CylinderGeometry
。
const cabin = new BoxGeometry(2, 2.25, 1.5);
:创建一个立方体几何体,参数2
, 2.25
, 1.5
分别对应立方体的宽度、高度和深度。
const wheel = new CylinderGeometry(0.4, 0.4, 1.75, 16);
:创建一个圆柱形几何体。
第一个和第二个参数0.75
, 0.75
分别是圆柱的顶部半径和底部半径。
第三个参数3
是圆柱的高度。
第四个参数12
定义了圆柱周围的面数,更高的值会使圆柱看起来更加圆滑。
const chimney = new CylinderGeometry(0.3, 0.1, 0.5);
同理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { BoxGeometry , CylinderGeometry } from 'three' ;function createGeometries ( ) { const cabin = new BoxGeometry (2 , 2.25 , 1.5 ); const nose = new CylinderGeometry (0.75 , 0.75 , 3 , 12 ); const wheel = new CylinderGeometry (0.4 , 0.4 , 1.75 , 16 ); const chimney = new CylinderGeometry (0.3 , 0.1 , 0.5 ); return { cabin, nose, wheel, chimney, }; }export { createGeometries };
new MeshStandardMaterial({});
创建一个标准材质。
flatShading: true,
:开启平滑着色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { MeshStandardMaterial } from 'three' ;function createMaterials ( ) { const body = new MeshStandardMaterial ({ color : 'firebrick' , flatShading : true , }); const detail = new MeshStandardMaterial ({ color : 'darkslategray' , flatShading : true , }); return { body, detail }; }export { createMaterials };
function createMeshes() {}
:根据 import { createGeometries } from './geometries.js';
和 import { createMaterials } from './materials.js';
,调整各组件的 transform,组装成小火车。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import { Mesh } from 'three' ;import { createGeometries } from './geometries.js' ;import { createMaterials } from './materials.js' ;function createMeshes ( ) { const geometries = createGeometries (); const materials = createMaterials (); const cabin = new Mesh (geometries.cabin , materials.body ); cabin.position .set (1.5 , 1.4 , 0 ); const chimney = new Mesh (geometries.chimney , materials.detail ); chimney.position .set (-2 , 1.9 , 0 ); const nose = new Mesh (geometries.nose , materials.body ); nose.position .set (-1 , 1 , 0 ); nose.rotation .z = Math .PI / 2 ; const smallWheelRear = new Mesh (geometries.wheel , materials.detail ); smallWheelRear.position .y = 0.5 ; smallWheelRear.rotation .x = Math .PI / 2 ; const smallWheelCenter = smallWheelRear.clone (); smallWheelCenter.position .x = -1 ; const smallWheelFront = smallWheelRear.clone (); smallWheelFront.position .x = -2 ; const bigWheel = smallWheelRear.clone (); bigWheel.position .set (1.5 , 0.9 , 0 ); bigWheel.scale .set (2 , 1.25 , 2 ); return { nose, cabin, chimney, smallWheelRear, smallWheelCenter, smallWheelFront, bigWheel, }; }export { createMeshes };
演示
1.13 以 glTF 格式加载 3D 模型
在过去三十年左右的时间里,人们在创建标准 3D 资源交换格式 方面进行了许多尝试。直到最近,FBX 、OBJ (Wavefront) 和 DAE (Collada) 格式仍然是其中最受欢迎的格式,尽管它们都存在阻碍其广泛采用的问题。比如 OBJ 不支持动画,FBX 是属于 Autodesk 的封闭格式,Collada 规范过于复杂,导致大文件难以加载。
然而,最近,一个名为 glTF 的新成员已成为在网络上交换 3D 资源的事实上的标准格式。glTF (GL 传输格式 ),有时被称为 3D 中的 JPEG ,由 Kronos Group 创建,他们负责 WebGL、OpenGL 和一大堆其他图形 API。glTF 最初于 2017 年发布,现在是在网络和许多其他领域交换 3D 资源的最佳格式。在本书中,我们将始终使用 glTF,如果可能,您也应该这样做 。它专为在网络上共享模型而设计,因此文件大小尽可能小,并且您的模型将快速加载。
但是,由于 glTF 相对较新,您最喜欢的应用程序可能还没有导出器。在这种情况下,您可以在使用模型之前将它们转换为 glTF,或者使用其他加载器,例如 FBXLoaderor
或者 OBJLoader
。所有 three.js 加载器的工作方式相同,因此如果您确实需要使用另一个加载器,本章中的所有内容仍然适用,只有细微差别。
原文极力推荐 .glb
格式,好吧,依他。
可以看到 three.js 支持好多种格式的文件,我们使用 GLTFLoader.js
,它还需要依赖项 '../utils/BufferGeometryUtils.js'
。
如果是本地加载,还应修改其相对地址:
1 import {XXX } from '../../../build/three.module.js' ;
文件组织结构如下:
assets/models
:存放着 3D 模型(.glb
格式)。
src/World/components/birds/
:
birds.js
:导入模型,创建鸟对象。
setupModel.js
:处理加载的数据。
main.js World.js setupModel.js birds.js
await world.init();
:等待模型加载完毕,才开始渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { World } from './World/World.js' ;async function main ( ) { const container = document .querySelector ('#scene-container' ); const world = new World (container); await world.init (); world.start (); }main ().catch ((err ) => { console .error (err); });
async init() {}
:等待模型加载完毕,才开始渲染。
const { parrot, flamingo, stork } = await loadBirds();
异步加载模型。
controls.target.copy(parrot.position);
:将场景控制的目标位置设置为 parrot
模型的位置。这通常用于确保相机能够围绕指定的点进行旋转或焦点对准,从而使用户的视角集中在这个点上。
scene.add(parrot, flamingo, stork);
将三只鸟加入场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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' ;import { createControls } from './systems/controls.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/Resizer.js' ;import { Loop } from './systems/Loop.js' ;let camera;let controls;let renderer;let scene;let loop;class World { constructor (container ) { camera = createCamera (); 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); } async init ( ) { const { parrot, flamingo, stork } = await loadBirds (); controls.target .copy (parrot.position ); scene.add (parrot, flamingo, stork); } render ( ) { renderer.render (scene, camera); } start ( ) { loop.start (); } stop ( ) { loop.stop (); } }export { World };
Blender 打开这个 .glb
模型,可以看到 Mesh_0
节点在 Object_0
下面,我们只需要 Mesh_0
这个节点,把它选取出来:
1 2 3 4 5 6 7 function setupModel (data ) { const model = data.scene .children [0 ]; return model; }export { setupModel };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 };
演示
试试自己的模型:
blender 把自己之前创建的模型导出成 .glb
格式:
代码就照葫芦画瓢了!
演示
1.14 three.js 动画系统
创建动画涉及三个元素:关键帧、KeyframeTrack
和AnimationClip
。
关键帧 KeyframeTrack AnimationClip
动画系统中最底层的概念级别是关键帧 。每个关键帧由三部分信息组成:时间 time 、属性 property 和值 value 。
没有代表单个关键帧的类。相反,关键帧是存储在两个数组中的原始数据 ,时间 和值 ,在 KeyframeTrack
中。
创建一个代表不透明度 的数字关键帧轨迹,包含五个关键帧
1 2 3 4 5 6 import { NumberKeyframeTrack } from "three" ;const times = [0 , 1 , 2 , 3 , 4 ];const values = [0 , 1 , 0 , 1 , 0 ];const opacityKF = new NumberKeyframeTrack (".material.opacity" , times, values);
创建一个表示位置 的矢量关键帧轨迹,包含三个关键帧
1 2 3 4 5 6 import { VectorKeyframeTrack } from "three" ;const times = [0 , 3 , 6 ];const values = [0 , 0 , 0 , 2 , 2 , 2 , 0 , 0 , 0 ];const positionKF = new VectorKeyframeTrack (".position" , times, values);
动画剪辑是附加到单个对象的任意数量的关键帧 的集合,表示剪辑的类是 AnimationClip
。
动画位置和不透明度的剪辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { AnimationClip , NumberKeyframeTrack , VectorKeyframeTrack } from "three" ;const positionKF = new VectorKeyframeTrack ( ".position" , [0 , 3 , 6 ], [0 , 0 , 0 , 2 , 2 , 2 , 0 , 0 , 0 ] );const opacityKF = new NumberKeyframeTrack ( ".material.opacity" , [0 , 1 , 2 , 3 , 4 , 5 , 6 ], [0 , 1 , 0 , 1 , 0 , 1 , 0 ] );const moveBlinkClip = new AnimationClip ("move-n-blink" , -1 , [ positionKF, opacityKF, ]);
World.js setupModel.js
loop.updatables.push(parrot, flamingo, stork);
:将 parrot
、flamingo
和 stork
加入 Loop 中以播放动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 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' ;import { createControls } from './systems/controls.js' ;import { createRenderer } from './systems/renderer.js' ;import { Resizer } from './systems/Resizer.js' ;import { Loop } from './systems/Loop.js' ;let camera;let controls;let renderer;let scene;let loop;class World { constructor (container ) { camera = createCamera (); 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); } async init ( ) { const { parrot, flamingo, stork } = await loadBirds (); controls.target .copy (parrot.position ); loop.updatables .push (parrot, flamingo, stork); scene.add (parrot, flamingo, stork); } render ( ) { renderer.render (scene, camera); } start ( ) { loop.start (); } stop ( ) { loop.stop (); } }export { World };
const clip = data.animations[0];
:获取 data
中的第一个动画(animations[0]
),存储在变量 clip
中。
const mixer = new AnimationMixer(model);
:使用刚刚获取的模型(model
)创建一个AnimationMixer
实例。这个实例将用于管理模型的动画。
const action = mixer.clipAction(clip);
:使用 mixer.clipAction(clip)
创建一个针对特定动画片段(clip
)的动画操作(action
)。
action.play();
:开始播放这个动画。
model.tick = (delta) => mixer.update(delta);
:更新动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { AnimationMixer } from 'three' ;function setupModel (data ) { const model = data.scene .children [0 ]; const clip = data.animations [0 ]; const mixer = new AnimationMixer (model); const action = mixer.clipAction (clip); action.play (); model.tick = (delta ) => mixer.update (delta); return model; }export { setupModel };
演示