Three.js零基础入门笔记,打造炫酷立体世界
文档获取
由于Three.js官网文档访问体验太卡,可以前往GitHub下载dev分支压缩包,将文档运行在本地。压缩包很大,下载时可能需要科学上网。
在GitHub仓库下载完成后我发现只有100多MB,而教程里有200多MB,感觉不太对劲。解压后发现下载的文件夹比GitHub仓库中缺少很多文件。比如根目录的/icon.png
,还有/examples
、/examples/sounds
等文件夹中很多文件没有下载下来。我以为是网络原因,又下载了一次还是不全。
求助了一下群友,推荐我可以去国内Gitee镜像仓库下载。去试了一下,压缩包有大约300MB,文件是全的。要知道,Gitee的镜像仓库是为了提升国内下载速度,每日同步一次GitHub的项目,理论上说一模一样啊,但为啥GitHub下载的不完整……
启动后打开页面,点击docs/
就可以查看文档了(语言可切换成中文);
另外,点击editor/
是一个可视化编辑器,点击examples/
可以查看官方的一些案例。
下方内容中以http://localhost
开头的超链接均为本地运行的文档,在线版可到官网查看
环境搭建/初始化项目
视频教程中使用的打包工具是Parcel,本文我使用的是webpack来打包。
视频里手动创建的项目,本文使用Vue-Cli可视化面板创建的Vue2.x
项目。
- 【详情】打开Vue-Cli可视化面板,创建新项目
vue_threejs_demo1
,包管理器npm
,初始化init project
。
- 【预设】选择
Manual
手动配置项目
- 【功能】选择
Bable
、Router
、Vuex
、CSS Pre-processors
、Linter / Formatter
、Use config files
- 【配置】版本可以选Vue
2.x
;Pick a linter / formatter config
选择标准的配置文件ESLint + Standard config
;Css预处理语言选择Less
- 创建项目,不用保存预设。耐心等待它下载npm包。
创建格式化文件.prettierrc.js
:
1 2 3 4 5 6 7 8 9
| module.exports = { semi: false, singleQuote: true, bracketSpacing: true, useTabs: false, tabWidth: 2, trailingComma: 'none', printWidth: 100, }
|
在.eslintrc.js
代码审查rules中添加一行代码:
1 2 3 4 5
| rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'space-before-function-paren': 0 }
|
创建全局css文件/src/assets/css/global.css
:
1 2 3 4 5 6 7 8 9
| html, body, #app { height: 100%; margin: 0; padding: 0; background-color: skyblue; }
|
在main.js
中引入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store'
import './assets/css/global.css'
Vue.config.productionTip = false
new Vue({ router, store, render: h => h(App) }).$mount('#app')
|
Demo测试
在App.vue
引入Three.js
1
| import * as THREE from 'three'
|
复制一下代码查看是否正常运行:
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 59 60 61 62 63 64 65 66 67
| <template> <div> <div id="container"></div> </div> </template>
<script> import * as THREE from 'three'
export default { name: 'ThreeTest', data() { return { camera: null, scene: null, renderer: null, mesh: null } }, methods: { init() { const container = document.getElementById('container')
this.camera = new THREE.PerspectiveCamera( 70, container.clientWidth / container.clientHeight, 0.01, 1000 ) this.camera.position.z = 0.6
this.scene = new THREE.Scene()
const geometry = new THREE.CylinderBufferGeometry(0.2, 0.2, 0.2) const material = new THREE.MeshNormalMaterial() this.mesh = new THREE.Mesh(geometry, material) this.scene.add(this.mesh)
this.renderer = new THREE.WebGLRenderer({ antialias: true }) this.renderer.setSize(container.clientWidth, container.clientHeight) container.appendChild(this.renderer.domElement) }, animate() { requestAnimationFrame(this.animate) this.mesh.rotation.x += 0.01 this.mesh.rotation.y += 0.02 this.renderer.render(this.scene, this.camera) } }, mounted() { this.init() this.animate() } } </script>
<style lang="less" scoped> #container { height: 400px; } </style>
|
搭建成功,开始正式学习。
01 渲染第一个场景和物体
了解Three.js最基本的内容
1、创建场景对象
场景(Scene):http://localhost:8080/docs/#api/zh/scenes/Scene
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export default { data() { return { scene: null } }, methods: { init() { this.scene = new THREE.Scene() } }, mounted() { this.init() } }
|
2、创建摄像机对象
以透视相机为例:http://localhost:8080/docs/?q=CA#api/zh/cameras/PerspectiveCamera
PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
fov
— 摄像机视锥体垂直视野角度
aspect
— 摄像机视锥体长宽比
near
— 摄像机视锥体近端面
far
— 摄像机视锥体远端面
这些参数一起定义了摄像机的viewing frustum(视锥体)。
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
| import * as THREE from 'three'
export default { data() { return { scene: null, camera: null } }, methods: { init() { this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) this.camera.position.set(0, 0, 10) this.scene.add(this.camera) } }, mounted() { this.init() } }
|
3、创建物体对象
(1) 添加几何体
以立方缓冲几何体为例:http://localhost:8080/docs/?q=box#api/zh/geometries/BoxGeometry
1 2 3 4 5 6 7 8
| data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null } },
|
1 2 3
|
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1)
|
(2) 添加材质
以基础网格材质为例:http://localhost:8080/docs/?q=material#api/zh/materials/MeshBasicMaterial
1 2 3 4 5 6 7 8
| data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null } },
|
1 2 3 4 5
|
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1)
this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 })
|
(3) 根据几何体和材质创建对象
网格(Mesh):http://localhost:8080/docs/?q=mesh#api/zh/objects/Mesh
1 2 3 4 5 6 7 8 9
| data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null } },
|
1 2 3 4 5 6 7 8
|
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1)
this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 })
this.cube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial) this.scene.add(this.cube)
|
4、初始化渲染器
WebGLRenderer:http://localhost:8080/docs/?q=renderer#api/zh/renderers/WebGLRenderer
1 2 3 4 5 6 7 8 9 10
| data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null } },
|
1 2 3 4 5 6 7
| this.renderer = new THREE.WebGLRenderer()
this.renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(this.renderer.domElement)
|
5、渲染
1 2 3
|
this.renderer.render(this.scene, this.camera)
|
- 弹幕中有个老哥提出疑问,我们在第2步已经使用过
this.scene.add(this.camera)
将摄像机添加到了场景中,那渲染这一步this.renderer.render(this.scene, this.camera)
还添加一次摄像机干啥?摄像机不是已经在场景中了吗?
- 有人回答,第2步创建摄像机后,不添加到场景中,最后效果也是一样的。测试了一下,确实。
完整vue代码:
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 59 60 61 62 63 64
| <template> <div></div> </template>
<script> import * as THREE from 'three'
export default { data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null } }, methods: { init() { this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) this.camera.position.set(0, 0, 10) this.scene.add(this.camera)
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1) this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }) this.cube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial) this.scene.add(this.cube)
this.renderer = new THREE.WebGLRenderer() this.renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(this.renderer.domElement)
this.renderer.render(this.scene, this.camera) } }, mounted() { this.init() } } </script>
<style lang="less" scoped> </style>
|
02 使用控制器查看3D物体
以轨道控制器为例:http://localhost:8080/docs/?q=cont#examples/zh/controls/OrbitControls
1、导入控制器
1 2
| import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
2、创建控制器
1 2 3 4 5 6 7 8 9 10 11
| data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null, controls: null } },
|
OrbitControls( object : Camera, domElement : HTMLDOMElement )
object
: (必须)将要被控制的相机。该相机不允许是其他任何对象的子级,除非该对象是场景自身。
domElement
: 用于事件监听的HTML元素。
1 2
| this.controls = new OrbitControls(this.camera, this.renderer.domElement)
|
3、请求动画帧(重绘渲染)
之前的渲染语句this.renderer.render(this.scene, this.camera)
只能渲染一次,若想让物体动态,需要不断渲染。
requestAnimationFrame
是浏览器用于定时循环操作的一个接口,类似于setTimeout
,主要用途是按帧对网页进行重绘。设置这个API的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。requestAnimationFrame的优势,在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。不过有一点需要注意,requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。requestAnimationFrame使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用。
示例:requestID = window.requestAnimationFrame(callback);
新增函数,用于重绘关键帧:
1 2 3 4 5 6 7 8 9 10 11 12 13
| methods: { render() { this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) }, mounted() { this.init() this.render() } }
|
现在页面上的正方体就可以使用鼠标左键拖动旋转,或者滚轮放大缩小了。
4、添加坐标轴辅助器
用于简单模拟3个坐标轴的对象.红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
AxesHelper:http://localhost:8080/docs/?q=help#api/zh/helpers/AxesHelper
1 2 3 4 5 6 7 8 9 10 11 12
| data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null, controls: null, axesHelper: null } },
|
AxesHelper( size : Number )
- size 表示代表轴的线段长度. 默认为 1
1 2 3
| this.axesHelper = new THREE.AxesHelper(5) this.scene.add(this.axesHelper)
|
完整Vue代码:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| <template> <div></div> </template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
export default { data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null, controls: null, axesHelper: null } }, methods: { init() { this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) this.camera.position.set(0, 0, 10) this.scene.add(this.camera)
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1) this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }) this.cube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial) this.scene.add(this.cube)
this.renderer = new THREE.WebGLRenderer() this.renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.axesHelper = new THREE.AxesHelper(5) this.scene.add(this.axesHelper) }, render() { this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) } }, mounted() { this.init() this.render() } } </script>
<style lang="less" scoped> </style>
|
03 设置物体移动
三维向量(Vector3):http://localhost:8080/docs/?q=vec#api/zh/math/Vector3
在创建物体
的代码后面,且在使用渲染器开始渲染
的代码之前,设置物体的位置。可以对位置坐标进行修改,也能单独对某一个轴的参数进行修改。
1 2 3
|
this.cube.position.x = 3
|
或者直接在重绘渲染函数render()
中添加代码,实现动画:
1 2 3 4 5 6 7 8 9 10 11 12 13
| render() { this.cube.position.x += 0.01 if (this.cube.position.x > 5) { this.cube.position.x = 0 }
this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) }
|
完整Vue代码:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| <template> <div></div> </template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
export default { data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null, controls: null, axesHelper: null } }, methods: { init() { this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) this.camera.position.set(0, 0, 10) this.scene.add(this.camera)
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1) this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }) this.cube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial) this.scene.add(this.cube)
this.cube.position.x = 3
this.renderer = new THREE.WebGLRenderer() this.renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.axesHelper = new THREE.AxesHelper(5) this.scene.add(this.axesHelper) }, render() { this.cube.position.x += 0.01 if (this.cube.position.x > 5) { this.cube.position.x = 0 }
this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) } }, mounted() { this.init() this.render() } } </script>
<style lang="less" scoped> </style>
|
04 物体的缩放与旋转
三维向量(Vector3):http://localhost:8080/docs/?q=vec#api/zh/math/Vector3
同样,我们需要在创建物体
的代码后面,且在使用渲染器开始渲染
的代码之前,设置物体的缩放与旋转。
缩放代码三个参数分别表示xyz轴的放大倍数,可单独设置。http://localhost:8080/docs/?q=Ob#api/zh/core/Object3D.scale
1 2 3
| this.cube.scale.set(3, 2, 1)
|
旋转代码的三个数字参数分别表示xyz轴的旋转角度,以弧度来表示。http://localhost:8080/docs/?q=Ob#api/zh/core/Object3D.rotation
1 2 3
| this.cube.rotation.set(Math.PI / 4, 0, 0)
|
同样,直接在重绘渲染函数render()
中添加代码,可以实现动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| render() { this.cube.position.x += 0.01 if (this.cube.position.x > 5) { this.cube.position.x = 0 }
this.cube.rotation.x += 0.01
this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) }
|
完整Vue代码:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| <template> <div></div> </template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
export default { data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null, controls: null, axesHelper: null } }, methods: { init() { this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) this.camera.position.set(0, 0, 10) this.scene.add(this.camera)
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1) this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }) this.cube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial) this.scene.add(this.cube)
this.cube.scale.set(3, 2, 1)
this.cube.rotation.set(Math.PI / 4, 0, 0)
this.renderer = new THREE.WebGLRenderer() this.renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.axesHelper = new THREE.AxesHelper(5) this.scene.add(this.axesHelper) }, render() { this.cube.position.x += 0.01 if (this.cube.position.x > 5) { this.cube.position.x = 0 }
this.cube.rotation.x += 0.01
this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) } }, mounted() { this.init() this.render() } } </script>
<style lang="less" scoped> </style>
|
05 应用requestAnimationFrame
由于机器性能问题,requestAnimationFrame(this.render)
调用的重绘渲染函数在执行时,默认传入的时间参数在运行时其实是不均匀的,也就是说不同帧之间不均匀,可以打印查看:
1 2 3
| render(time) { console.log('默认传入的毫秒数' + time) }
|
为了解决这个问题可以如下进行优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| render(time) {
const t = time / 1000 this.cube.position.x = t * 1 if (this.cube.position.x > 5) { this.cube.position.x = 0 }
this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) },
|
时间在大于5秒后,位置会回到0,如果想往复运动,可以取余数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| render(time) {
const t = (time / 1000) % 5 this.cube.position.x = t * 1
this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) },
|
完整Vue代码:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| <template> <div></div> </template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
export default { data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null, controls: null, axesHelper: null } }, methods: { init() { this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) this.camera.position.set(0, 0, 10) this.scene.add(this.camera)
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1) this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }) this.cube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial) this.scene.add(this.cube)
this.cube.rotation.set(Math.PI / 4, 0, 0)
this.renderer = new THREE.WebGLRenderer() this.renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.axesHelper = new THREE.AxesHelper(5) this.scene.add(this.axesHelper) }, render(time) {
const t = (time / 1000) % 5 this.cube.position.x = t * 1
this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) } }, mounted() { this.init() this.render() } } </script>
<style lang="less" scoped> </style>
|
现在物体的运动就是随时间均匀变化,而不是随帧数变化的了。
06 通过Clock跟踪时间处理动画
由于requestAnimationFrame
只有一个时间参数,如果有多个内容想要多个计时的话,只用这一个来计时会很累。
这就需要我们的Three.js提供的Clock
来跟踪时间对象了:http://localhost:8080/docs/?q=clock#api/zh/core/Clock
1 2
| this.clock = new THREE.Clock()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| render() {
const deltaTime = this.clock.getDelta() console.log('获取两次时间的间隔:', deltaTime)
this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) },
|
07 Gsap动画库基本使用与原理
上面我们通过手动计算速度和时间来控制物体的移动和旋转,如何更加简便,不用自己去做这些麻烦步骤呢?
这里我们就可以使用Gsap(补间动画)来帮助我们开发。它是专门用来做动画的,比如用在css动画、svg动画、React项目、Vue项目,还可以做数字动画。
使用npm命令安装,或者在可视化面板中安装运行依赖,搜索gsap
1 2
| import gsap from 'gsap'
|
完整Vue代码:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| <template> <div></div> </template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import gsap from 'gsap'
export default { data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null, controls: null, axesHelper: null, clock: null } }, methods: { init() { this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) this.camera.position.set(0, 0, 10) this.scene.add(this.camera)
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1) this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }) this.cube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial) this.scene.add(this.cube)
this.cube.rotation.set(Math.PI / 4, 0, 0)
this.renderer = new THREE.WebGLRenderer() this.renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.axesHelper = new THREE.AxesHelper(5) this.scene.add(this.axesHelper)
this.clock = new THREE.Clock()
const animate1 = gsap.to(this.cube.position, { x: 5, duration: 5, ease: 'power1.inout', repeat: 2, yoyo: true, delay: 2, onStart: () => { console.log('动画开始') }, onComplete: () => { console.log('动画完成') } }) gsap.to(this.cube.rotation, { x: 2 * Math.PI, duration: 5, ease: 'power1.inout' })
window.addEventListener('dblclick', () => { if (animate1.isActive()) { animate1.pause() } else { animate1.resume() } }) }, render() { this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) } }, mounted() { this.init() this.render() } } </script>
<style lang="less" scoped> </style>
|
08 控制器阻尼与画面尺寸自适应
1、控制器阻尼(惯性)
轨道控制器启用阻尼(惯性):http://localhost:8080/docs/?q=orbi#examples/zh/controls/OrbitControls.enableDamping
.enableDamping : Boolean
- 将其设置为true以启用阻尼(惯性),这将给控制器带来重量感。默认值为
false
。
- 请注意,如果该值被启用,你将必须在你的动画循环里调用
.update()
。
代码添加到创建控制器之后:
1 2 3 4
| this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
|
在动画循环里调用.update()
:
1 2 3 4 5 6 7 8 9
| render() { this.controls.update() this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) }
|
2、渲染画面尺寸自适应
监听屏幕画面大小变化,修改渲染器宽高和相机的比例,更新渲染画面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| mounted() { this.init() this.render()
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight) this.renderer.setPixelRatio(window.deviceRixelRatio) }) }
|
完整Vue代码:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
| <template> <div></div> </template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import gsap from 'gsap'
export default { data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null, controls: null, axesHelper: null, clock: null } }, methods: { init() { this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) this.camera.position.set(0, 0, 10) this.scene.add(this.camera)
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1) this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }) this.cube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial) this.scene.add(this.cube)
this.cube.rotation.set(Math.PI / 4, 0, 0)
this.renderer = new THREE.WebGLRenderer() this.renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement) this.controls.enableDamping = true
this.axesHelper = new THREE.AxesHelper(5) this.scene.add(this.axesHelper)
this.clock = new THREE.Clock()
const animate1 = gsap.to(this.cube.position, { x: 5, duration: 5, ease: 'power1.inout', repeat: 2, yoyo: true, delay: 2, onStart: () => { console.log('动画开始') }, onComplete: () => { console.log('动画完成') } }) gsap.to(this.cube.rotation, { x: 2 * Math.PI, duration: 5, ease: 'power1.inout' })
window.addEventListener('dblclick', () => { if (animate1.isActive()) { animate1.pause() } else { animate1.resume() } }) }, render() { this.controls.update() this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) } }, mounted() { this.init() this.render()
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight) this.renderer.setPixelRatio(window.deviceRixelRatio) }) } } </script>
<style lang="less" scoped> </style>
|
09 调用js接口控制画布全屏和退出全屏
键盘的F11一般可以控制浏览器的全屏,或者我们通过JS操作。
双击进入全屏
1 2 3 4 5
| window.addEventListener('dblclick', () => { this.renderer.domElement.requestFullscreen() })
|
双击控制屏幕进入全屏,退出全屏
1 2 3 4 5 6 7 8 9 10 11 12
| window.addEventListener('dblclick', () => { const fullScreenElement = document.fullscreenElement if (!fullScreenElement) { this.renderer.domElement.requestFullscreen() } else { document.exitFullscreen() } })
|
完整Vue代码:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
| <template> <div></div> </template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
export default { data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null, controls: null, axesHelper: null, clock: null } }, methods: { init() { this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) this.camera.position.set(0, 0, 10) this.scene.add(this.camera)
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1) this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }) this.cube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial) this.scene.add(this.cube)
this.cube.rotation.set(Math.PI / 4, 0, 0)
this.renderer = new THREE.WebGLRenderer() this.renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement) this.controls.enableDamping = true
this.axesHelper = new THREE.AxesHelper(5) this.scene.add(this.axesHelper)
this.clock = new THREE.Clock()
window.addEventListener('dblclick', () => { const fullScreenElement = document.fullscreenElement if (!fullScreenElement) { this.renderer.domElement.requestFullscreen() } else { document.exitFullscreen() } }) }, render() { this.controls.update() this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) } }, mounted() { this.init() this.render()
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight) this.renderer.setPixelRatio(window.deviceRixelRatio) }) } } </script>
<style lang="less" scoped> </style>
|
10 应用图形用户界面更改变量
如果想方便地在页面调整参数来控制画面,有没有简便的方法?
这就要用到一个轻量级的 JavaScript 控制器库dat.GUI
。它使我们可以轻松地即时操作变量和触发函数。我们可以通过设定好的控制器去快捷的修改设定的变量。
在可视化面板安装运行依赖,搜索dat.gui
并安装。
1 2
| import * as dat from 'dat.gui'
|
1、改变物体位置x轴的值
- 在创建完物体并放置在场景中后,初始化(创建)GUI,并让其可以改变物体x轴坐标:
1 2 3 4 5 6 7 8
| this.scene.add(this.cube)
console.log(this.cube)
const gui = new dat.GUI()
gui.add(this.cube.position, 'x').min(0).max(5)
|
此时页面上就出现了一个可拖动的进度条,与物体x轴坐标绑定,.min().max()
控制着数值的变化范围。
我们可以通过.step()
让数值增减的步长更加精细:
1 2 3 4 5 6 7 8
| this.scene.add(this.cube)
console.log(this.cube)
const gui = new dat.GUI()
gui.add(this.cube.position, 'x').min(0).max(5).step(0.01)
|
也可以通过.name()
对此项参数名称自定义:
1 2 3 4 5 6 7 8
| this.scene.add(this.cube)
console.log(this.cube)
const gui = new dat.GUI()
gui.add(this.cube.position, 'x').min(0).max(5).step(0.01).name('移动x轴')
|
还可以通过.onChange
函数在参数修改时执行语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| this.scene.add(this.cube)
console.log(this.cube)
const gui = new dat.GUI()
gui .add(this.cube.position, 'x') .min(0) .max(5) .step(0.01) .name('移动x轴') .onChange((value) => { console.log('值被修改', value) })
|
或者通过.onFinishChange
函数在停止修改时执行语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| this.scene.add(this.cube)
console.log(this.cube)
const gui = new dat.GUI()
gui .add(this.cube.position, 'x') .min(0) .max(5) .step(0.01) .name('移动x轴') .onChange((value) => { console.log('值被修改', value) }) .onFinishChange((value) => { console.log('完全停下来', value) })
|
2、改变物体的颜色属性
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
| this.scene.add(this.cube)
console.log(this.cube)
const gui = new dat.GUI()
gui .add(this.cube.position, 'x') .min(0) .max(5) .step(0.01) .name('移动x轴') .onChange((value) => { console.log('值被修改', value) }) .onFinishChange((value) => { console.log('完全停下来', value) })
const params = { color: '#ffff00' } gui.addColor(params, 'color').onChange((value) => { this.cube.material.color.set(value) })
|
3、改变物体的显示属性(可见性)
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
| this.scene.add(this.cube)
console.log(this.cube)
const gui = new dat.GUI()
gui .add(this.cube.position, 'x') .min(0) .max(5) .step(0.01) .name('移动x轴') .onChange((value) => { console.log('值被修改', value) }) .onFinishChange((value) => { console.log('完全停下来', value) })
const params = { color: '#ffff00' } gui.addColor(params, 'color').onChange((value) => { this.cube.material.color.set(value) })
gui.add(this.cube, 'visible').name('是否显示')
|
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 33 34 35
| this.scene.add(this.cube)
console.log(this.cube)
const gui = new dat.GUI()
gui .add(this.cube.position, 'x') .min(0) .max(5) .step(0.01) .name('移动x轴') .onChange((value) => { console.log('值被修改', value) }) .onFinishChange((value) => { console.log('完全停下来', value) })
const params = { color: '#ffff00', fn: () => { gsap.to(this.cube.position, { x: 5, duration: 2, yoyo: true, repeat: -1 }) } } gui.addColor(params, 'color').onChange((value) => { this.cube.material.color.set(value) })
gui.add(this.cube, 'visible').name('是否显示')
gui.add(params, 'fn').name('立方体运动')
|
5、设置可展开的文件夹
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
| this.scene.add(this.cube)
console.log(this.cube)
const gui = new dat.GUI()
gui .add(this.cube.position, 'x') .min(0) .max(5) .step(0.01) .name('移动x轴') .onChange((value) => { console.log('值被修改', value) }) .onFinishChange((value) => { console.log('完全停下来', value) })
const params = { color: '#ffff00', fn: () => { gsap.to(this.cube.position, { x: 5, duration: 2, yoyo: true, repeat: -1 }) } } gui.addColor(params, 'color').onChange((value) => { this.cube.material.color.set(value) })
gui.add(this.cube, 'visible').name('是否显示')
gui.add(params, 'fn').name('立方体运动')
const folder = gui.addFolder('设置立方体')
folder.add(cube.material, 'wireframe')
|
文件夹可添加多个属性:
1 2 3 4 5 6
| const folder = gui.addFolder('设置立方体')
folder.add(this.cube.material, 'wireframe')
folder.add(params, 'fn').name('立方体运动')
|
完整Vue代码:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
| <template> <div></div> </template>
<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import * as dat from 'dat.gui'
import gsap from 'gsap'
export default { data() { return { scene: null, camera: null, cubeGeometry: null, cubeMaterial: null, cube: null, renderer: null, controls: null, axesHelper: null, clock: null } }, methods: { init() { this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) this.camera.position.set(0, 0, 10) this.scene.add(this.camera)
this.cubeGeometry = new THREE.BoxGeometry(1, 1, 1) this.cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }) this.cube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial)
this.cube.rotation.set(Math.PI / 4, 0, 0)
this.scene.add(this.cube)
console.log(this.cube) const gui = new dat.GUI() gui .add(this.cube.position, 'x') .min(0) .max(5) .step(0.01) .name('移动x轴') .onChange((value) => { console.log('值被修改', value) }) .onFinishChange((value) => { console.log('完全停下来', value) }) const params = { color: '#ffff00', fn: () => { gsap.to(this.cube.position, { x: 5, duration: 2, yoyo: true, repeat: -1 }) } } gui.addColor(params, 'color').onChange((value) => { this.cube.material.color.set(value) }) gui.add(this.cube, 'visible').name('是否显示')
const folder = gui.addFolder('设置立方体') folder.add(this.cube.material, 'wireframe') folder.add(params, 'fn').name('立方体运动')
this.renderer = new THREE.WebGLRenderer() this.renderer.setSize(window.innerWidth, window.innerHeight) document.body.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement) this.controls.enableDamping = true
this.axesHelper = new THREE.AxesHelper(5) this.scene.add(this.axesHelper)
this.clock = new THREE.Clock()
window.addEventListener('dblclick', () => { const fullScreenElement = document.fullscreenElement if (!fullScreenElement) { this.renderer.domElement.requestFullscreen() } else { document.exitFullscreen() } }) }, render() { this.controls.update() this.renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) } }, mounted() { this.init() this.render()
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight) this.renderer.setPixelRatio(window.deviceRixelRatio) }) } } </script>
<style lang="less" scoped> </style>
|
【参考内容】: