Three.js 学习笔记,来自:
- https://gitchat.csdn.net/columnTopic/5b320731bebc3c4bd7e725cb?utm_source=juhe
- https://blog.csdn.net/qq_30100043/article/category/7003591?utm_source=juhe
- https://zhuanlan.zhihu.com/p/95463367
WebGL(Web 图形库)是一种 JavaScript API,用于在任何兼容的 Web 浏览器中呈现交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与 OpenGL ES 2.0 紧密相符合的 API,可以在 HTML5 元素中使用(简介引自 MDN)。
以我的理解,WebGL 给我们提供了一系列的图形接口,能够让我们通过 JavaScript 去使用 GPU 来进行浏览器图形渲染的工具。
Three.js 是一款 webGL 框架,由于其易用性被广泛应用。Three.js 在 WebGL 的 API 接口基础上,又进行的一层封装。它是由居住在西班牙巴塞罗那的程序员 Ricardo Cabbello Miguel 所开发,他更为人知的网名是 Mr.doob。
Three.js 以简单、直观的方式封装了 3D 图形编程中常用的对象。Three.js 在开发中使用了很多图形引擎的高级技巧,极大地提高了性能。另外,由于内置了很多常用对象和极易上手的工具,Three.js 的功能也非常强大。最后,Three.js 还是完全开源的,你可以在 GitHub 上找到它的源代码,并且有很多人贡献代码,帮助 Mr.doob 一起维护这个框架。
WebGL 原生 API 是一种非常低级的接口,而且还需要一些数学和图形学的相关技术。对于没有相关基础的人来说,入门真的很难,Three.js 将入门的门槛降低了一大截,对 WebGL 进行封装,简化我们创建三维动画场景的过程。只要你有一定的 JavaScript 基础,有一定的前端经验,我坚信,用不了多长时间,三维制作会变得很简单。
用最简单的一句话概括:WebGL 和 Three.js 的关系,相当于 JavaScript 和 jQuery 的关系。
Three.js 作为 WebGL 框架中的佼佼者,由于它的易用性和扩展性,使得它能够满足大部分的开发需求,Three.js 的具体功能如下:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title> Hello Three.js!title>
<style>
body {
margin: 0;
}
canvas {
width:100%;
height: 100%;
display: block;
}
style>
head>
<body onload="init()">
<script src="https://cdn.bootcss.com/three.js/92/three.js">script>
<script>
function init () {
const scene = new THREE.Scene(); // 实例化场景
const light = new THREE.SpotLight(0xffffff);
light.position.set(20, 20, 20);
scene.add(light); // 添加点光源
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
1,
200
); // 实例化一个透视相机(视野,显示口的宽高比,近裁剪面,远裁剪面)
camera.position.set(1, 1, 15);
const geometry = new THREE.BoxGeometry(4, 4, 4); // 创建几何体
const material = new THREE.MeshStandardMaterial({ color: 0xffffff });
const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = 0.3;
scene.add(mesh);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight); // 设置宽和高
document.body.appendChild(renderer.domElement); // 添加到 dom
renderer.render(scene, camera);
function animate () {
requestAnimationFrame(animate); // 循环调用函数
mesh.rotation.x += 0.01; // 每帧网格模型的沿 x 轴旋转 0.01 弧度
mesh.rotation.y += 0.01; // 每帧网格模型的沿 y 轴旋转 0.02 弧度
renderer.render(scene, camera); //渲染界面
}
animate();
}
script>
body>
html>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ARDdKTNT-1628039368562)(D:\chenyh\ienyh\resourses\hellothreejs.png)]
使用 Three.js 显示创建的内容,我们必须需要的三大件是:渲染器、相机和场景。
3D 应用的组件结构,图片来自 discoverthreejs.com
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oNIeOsc7-1628039368564)(D:\chenyh\ienyh\resourses\threejs-struct.jpg)]
Three.js 应用架构,图片来自 threejsfundamentals.org
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7a2GF86Q-1628039368565)(D:\chenyh\ienyh\resourses\threejs-struct-2.jpg)]
认识 WebGL 的坐标系统,图片来自 https://gitchat.csdn.net/columnTopic/5b320b4abebc3c4bd7e7283a?utm_source=juhe
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OEt8rzio-1628039368568)(D:\chenyh\ienyh\resourses\threejs01.jpg)]
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
参数 | 值 |
---|---|
antialias | 是否执行抗锯齿。默认为false. |
alpha | canvas 是否包含 alpha (透明度)。默认为 false |
我们都知道 DOM 的结构是树形结构的,Three.js 也遵循这样的理念,将所有可以添加到场景内的结构梳理成了一种树形的结构,方便我们能够更好的理解 Three.js。
在 Three.js 中,为了方便操作,将所有 3D 对象共同的内容抽象成了一个基类,就是 THREE.Object3D
obj instanceof THREE.Object3D // 继承至返回 true 否则返回 false
我们可以通过设置模型的 position
属性来修改模型的当前位置
单独设置每个方向的属性:
mesh.position.x = 3; // 将模型的位置调整到x正轴距离原点为 3 的位置。
mesh.position.y += 5; // 将模型的y轴位置以当前的位置向上移动 5 个单位。
mesh.position.z -= 6;
mesh.position.set()
一次性设置所有方向上的属性
mesh.position.set(3, 5, -6); // 直接将模型的位置设置在 x 轴为 3,y 轴为 5,z 轴为 -6 的位置
mesh
的 position
属性是一个 THREE.Vector3
三维向量对象
mesh.position = new THREE.Vector3(3, 5, -6); // 上面的设置位置也可以通过这样设置。
通过设置模型的 scale 属性来调整大小
单独设置每个方向的缩放:
mesh.scale.x = 2; // 模型沿x轴放大一倍
mesh.scale.y = 0.5; // 模型沿y轴缩小一倍
mesh.scale.z = 1; // 模型沿z轴保持不变
mesh.scale.set()
设置所有大小属性
mesh.scale.set(2, 2, 2); // 每个方向等比放大一倍
mesh.scale.set(0.5, 0.5, 0.5); // 每个方向等比缩小一倍
mesh
的 scale
属性也是一个 THREE.Vector3
三维向量对象
mesh.scale = new THREE.Vector3(2, 2, 2); // 每个方向都放大一倍
设置模型的 rotation 属性进行旋转
注意:旋转的单位是弧度而不是角度
单独设置每个轴的旋转
mesh.rotation.x = Math.PI; //模型沿 x 旋转 180 度
mesh.rotation.y = Math.PI * 2; //模型沿 y 轴旋转 360 度,跟没旋转一样的效果。。。
mesh.rotation.z = - Math.PI / 2; //模型沿 z 轴逆时针旋转 90 度
使用 mesh.rotation.set()
方法重新赋值
mesh.rotation.set(Math.PI, 0, - Math.PI / 2); // 旋转效果和第一种显示的效果相同
正常模型的旋转方式是按照 XYZ 依次旋转的,如果你想先旋转其他轴,可以设置第四个参数(string),有可能的情况为:YZX、ZXY、XZY、YXZ 和 ZYX。
mesh.rotation.set(Math.PI, 0, - Math.PI / 2, "YZX"); // 先沿 y 轴旋转 180 度,再沿 z 轴旋转 0 度,最后沿 x 轴逆时针旋转 90 度
模型的 rotation
属性其实是一个欧拉角对象 THREE.Euler
mesh.rotation = new THREE.Euler(Math.PI, 0, - Math.PI / 2, "YZX");
Geometry 和 BufferGeometry
THREE.Vector3
或者 THREE.Color
这样的对象来存储数据(顶点位置、面、颜色等),这些对象易于阅读和编辑,但效率低于 BufferGeometry 使用的类型化数组。Geometry 和 BufferGeometry 这两种几何体类型可以互转
Geometry
=> BufferGeometry
const geo = new THREE.Geometry();
const bufferGeo = geo.fromBufferGeometry(geometry);
BufferGeometry
=> Geometry
const bufferGeo = new THREE.BufferGeometry();
const geo = bufferGeo.fromGeometry(bufferGeometry);
BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments); // BoxGeometry 构造函数
const geometry = new THREE.BoxGeometry(8, 8, 8); // 创建几何体
const material = new THREE.MeshStandardMaterial({ color: 0xffffff });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
PlaneGeometry(width, height, widthSegments, heightSegments); // PlaneGeometry 构造函数
const planeGeometry = new THREE.PlaneGeometry(15, 1, 32);
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0xf0f0f0, side: THREE.DoubleSide });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
scene.add(plane);
Geometry
和 BufferGeomety
内置了一些常用的方法
center()
此方法为居中方法,可以根据边界框居中几何图形。
computeBoundingBox()
此方法可以计算几何的边界框,方法调用后,会更新 Geometry.boundingBox
属性,我们可以通过 Geometry.boundingBox
属性获取到一个包围几何体的立方体的每个轴向的最大值和最小值。
dispose()
将几何体从内存中删除,这个方法必须记得使用。如果频繁的删除模型,一定要记得将几何体从内存中删除掉。
Material 材质,就是就是物体看起来是什么质地,材质可以看成是材料和质感的结合。
在渲染程序中,它是表面各种可视属性的结合,这些可视属性是指表面的色彩、纹理、光滑度、透明度、反射率、折射率、发光度等。
needsUpdate
如果修改了 Material 内的内容,需要将 needsUpdate
属性设置为 true
,Three.js 会在下一帧里将修改内容同步到 WebGL 的显存内。切记不要在 requestAnimationFrame()
方法内更新,会浪费性能,只需要在更新 Material 属性后设置一次即可。
side
定义当前面的哪个方向会被渲染:
THREE.FrontSide
默认值,只渲染正面;THREE.BackSide
只渲染背面;THREE.DoubleSide
正面和背面都会渲染。transparent
定义了材质是否可以透明,当设置此属性为 true
后,可以通过设置 opacity
来调整透明度,默认为 false
。
对于透明需要材质进行特殊处理,在不透明的物体渲染完成后再渲染透明物体。
opacity
定义材质的透明度,必须将材质的 transparent
设置为 true
才可使透明度管用。取值范围为 0.0
到 1.0
。默认值是 1.0
,也就是默认不透明。
map
配置当前材质的纹理贴图,是一个 THREE.Texture
对象,这是大部分材质都会有的属性,只有个别材质如 LineBasicMaterial
(线材质)等没有这个属性。
THREE.TextureLoader
进行生成纹理对象const texture = new THREE.TextureLoader().load("./assets/texture01.jpg");
material.map = texture
THREE.Texture
对象:该构造函数的参数可以是 img
、canvas
和 video
const texture = new THREE.Texture(canvas);
material.map = texture; // 将纹理赋值给材质
wireframe
是否将模型渲染成线框,默认为 false
。个别材质没有这个属性。
repeat
纹理在整个表面水平方向和垂直方向重复多少次,也会受纹理重复设置的影响,设置方式为:
const texture = new THREE.TextureLoader().load("textures/water.jpg");
texture.wrapS = THREE.RepeatWrapping; // 设置水平方向无限循环
texture.wrapT = THREE.RepeatWrapping; // 设置垂直方向无限循环
texture.repeat.set(4, 4); // 水平方向和垂直方向都重复四次
dispose()
用于将材质从内存中删除,在不需要使用当前材质时使用,但不会将材质的纹理贴图删除,如果需要将纹理贴图也删除,需要调用 material.map.dispose()
。
const material = new THREE.MeshBasicMaterial({color:0x00ffff}); //设置初始的颜色为浅蓝色
material.color.set(0xff00ff); // 将颜色修改为紫色
我们也可以直接赋值一个新的 THREE.Color
对象
material.color = new THREE.Color(0xff00ff);
我们可以通过 new THREE.Color
创建一个颜色对象,Three.js 支持的颜色书写方式比较丰富,如:
// 直接传入十六进制数或者字符串
const color = new THREE.Color(0xff0000);
const color = new THREE.Color("#ff0000");
// RGB 字符串
const color = new THREE.Color("rgb(255, 0, 0)");
const color = new THREE.Color("rgb(100%, 0%, 0%)");
// 支持一百四十多中颜色名称
const color = new THREE.Color('skyblue');
// HSL 字符串
const color = new THREE.Color("hsl(0, 100%, 50%)");
// 支持 RGB 值设置在 0 到 1 之间的方式
const color = new THREE.Color(1, 0, 0);
material.color = color;
MeshBasicMaterial 不会受光的影响,即使有光也不会影响它的效果,有些材质会受到光源的影响。
这里先加上光,测试下面各种材质的效果
// 创建灯光
function initLight() {
const light = new THREE.DirectionalLight(0xffffff); // 添加了一个白色的平行光
light.position.set(20, 50, 50); // 设置光的方向
scene.add(light); // 添加到场景
// 添加一个全局环境光
scene.add(new THREE.AmbientLight(0x222222));
}
这种材质会对光有反应,但是不会出现高光,可以模拟一些粗糙的材质的物体,比如木头或者石头。
这种材质具有高光效果,可以模拟一些光滑的物体的材质效果,比如油漆面,瓷瓦等光滑物体。
这种材质基于物理的渲染(PBR)材质,生成的材质效果更佳,但是相应也占用更多的计算量。这种材质我们可以定义它的粗糙度来确定反光效果,经常用于模拟金属的质感,使金属质感更加真实。
geometry = new THREE.BoxGeometry(2, 2, 2); // 创建几何体
material = new THREE.MeshPhongMaterial({ color: 0x00ffff }); // 创建材质
material.metalness = 0.1; // 设置的值的范围为 0-1,值越小,材质越光滑,高光越明显
material.metalnessMap = 0.1; // 设置的值的范围为 0-1,值越大,越有生锈金属的质感,值越小反光越清晰
mesh = new THREE.Mesh(geometry, material); // 创建网格
scene.add(mesh); // 将网格添加到场景
不同种类的光照,实例化时,可以接受两个传值,分别是
Light(color: Integer, intensity: Float)
注意,并不是直接调用该构造函数(而是使用派生类之一)。
0xffffff
白色)1.0
)。Light 实例的方法:
light.copy(source: Light): Light
从 source 复制 color, intensity 的属性值到当前光源对象中
light.toJSON(meta: Object): Object
以 JSON 格式返回光数据
const light = new THREE.DirectionalLight(0xffffff, 1.0); // 添加了一个白色的平行光
light.color.set(0x000000); // 将光照的颜色修改为黑色
light.intensity = 2.0; // 光照的强度改为默认的两倍
scene.add(light); // 添加光到场景中
环境光是经过多次反射而来的光,环境光源放出的光线被认为来自任何方向,物体无论法向量如何,都将表现为同样的明暗程度。
环境光通常不会单独使用,通过使用多种光源能够实现更真实的光效
const ambientLight = new THREE.AmbientLight(0x222222); // 创建了一个环境全局光
平行光是以特定的方向发射的光。它产生的光都是平行的状态,主要用于模拟太阳光线。
平行光没有衰减,被平行光照亮的整个区域接受到的光强是一样的。
const directionLight = new THREE.DirectionalLight(0xffffff); // 添加了一个白色的平行光
点光源在传播过程中有衰弱,接近点光源光就强一些,远离点光源光就弱一些
实例化点光源支持四个参数:光照颜色、光照强度、照射范围和衰减度
const pointLight = new THREE.PointLight(0xff0000, 1, 100, 2); // 创建一个白色的点光源
类似舞台上的聚光灯效果,光源的光线从一个锥体中射出,在被照射的物体上产生聚光的效果。聚光灯在传播过程是有衰弱的。
const spotLight = new THREE.SpotLight(0xffffff); // 创建一个白色光照
这个光源主要是为了模拟在户外的环境光效果,比如在蓝天绿地的户外,模型下面会显示出来绿色的环境光,而上方则会受到蓝天的影响而颜色偏蓝。
实例化室外光源支持三个参数:天空的颜色,地面的颜色,和光的强度。
const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);
不是所有的光源都能够产生阴影,只有一部分光源可以,例如通过
THREE.PointLight
(点光源)、THREE.SpotLight
(聚光源)和THREE.DirectionalLight
(平行光光源)定义的光源是能够产生阴影的。
renderer.shadowMap.enabled = true;
directionalLight = new THREE.DirectionalLight("#ffffff");
directionalLight.castShadow = true; // 设置平行光投射投影
castShadow
和哪些模型可以接收阴影 receiveShadow
geometry.castShadow = true; // 开启阴影
// 创建平面
const planeGeometry = new THREE.PlaneGeometry(50, 50); // 宽、高
const planeMaterial = new THREE.MeshLambertMaterial({ color: 0xf0f0f0, side: THREE.DoubleSide }); // 注意这里平面的材质一定要可以接受光的影响
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
planeMesh.receiveShadow = true;
scene.add(planeMesh);
我们常用的相机有正交相机 OrthographicCamera
和透视相机 PerspectiveCamera
两种,用于来捕获场景内显示的物体模型。
相机都继承自 THREE.Object3D
对象,所以 position
位置属性、rotation
旋转和 scale
缩放属性,都可以直接对相机对象设置。还可以使用 add()
方法,给相机对象添加子类,移动相机它的子类也会跟随着一块移动。
target
焦点属性和 lookAt()
方法target
和 lookAt()
都是调整相机的朝向,可以设置一个 THREE.Vector3
(三维向量)点的位置
camera.target = new THREE.Vector3(0, 0, 0); // 朝向原点
camera.lookAt(new THREE.Vector3(0, 0, 0));
我们也可以将相机的朝向改为模型网格 mesh 的 position,如果物体的位置发生了变化,相机的焦点方向也会跟随变动:
const mesh = new THREE.Mesh(geometry, material);
camera.target = mesh.position;
camera.lookAt(mesh.position);
getWorldDirection()
可以获取当前位置到 target
位置的世界中的方向。方向也可以使用 THREE.Vector3
对象表示,所以该方法返回一个三维向量。
使用正交相机 OrthographicCamera 渲染出来的场景,所有的物体和模型都按照它固有的尺寸和精度显示,一般使用在工业要求精度或者 2D 平面中,因为它能完整的显示物体应有的尺寸。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EJKKcrG8-1628039368570)(D:\chenyh\ienyh\resourses\OrthographicCamera.jpg)]
只要确定 top、left、right、bottom、near 和 far 六个值,我们就能确定当前相机捕获场景的区域,在这个区域外面的内容不会被渲染
const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
正常情况相机显示的内容需要和窗口显示的内容为同比例才能够显示没有被拉伸变形的效果:
const frustumSize = 1000; // 设置显示相机前方 1000 高的内容
const aspect = window.innerWidth / window.innerHeight; // 计算场景的宽高比
const orthographicCamera = new THREE.OrthographicCamera(
frustumSize * aspect / - 2,
frustumSize * aspect / 2,
frustumSize / 2,
frustumSize / - 2,
1,
2000
); // 根据比例计算出 left,top,right,bottom 的值
正交相机定义后可以动态修改属性,但修改完以后需要调用相机 updateProjectionMatrix()
方法来更新相机显存里面的内容
const frustumSize = 1000; // 设置显示相机前方 1000 高的内容
const aspect = window.innerWidth / window.innerHeight; // 计算场景的宽高比
const orthographicCamera = new THREE.OrthographicCamera(); // 实例化一个空的正交相机
orthographicCamera.left = frustumSize * aspect / - 2; // 设置 left 的值
orthographicCamera.right = frustumSize * aspect / 2; // 设置 right 的值
orthographicCamera.top = frustumSize / 2; // 设置 top 的值
orthographicCamera.bottom = frustumSize / - 2; // 设置 bottom 的值
orthographicCamera.near = 1; // 设置 near 的值
orthographicCamera.far = 2000; // 设置 far 的值
orthographicCamera.updateProjectionMatrix(); // 注意:最后一定要调用该方法进行更新
由于浏览器的窗口可以随意修改,我们有时候需要监听浏览器窗口的变化,然后获取到最新的宽高比,再重新设置相关属性:
// 写到监听方法里
const aspect = window.innerWidth / window.innerHeight; // 重新获取场景的宽高比
orthographicCamera.left = frustumSize * aspect / - 2; // 设置 left 的值
orthographicCamera.right = frustumSize * aspect / 2; // 设置 right 的值
orthographicCamera.top = frustumSize / 2; // 设置 top 的值
orthographicCamera.bottom = frustumSize / - 2; // 设置 bottom 的值
orthographicCamera.updateProjectionMatrix(); // 注意:一定要调用该方法进行更新
renderer.setSize(window.innerWidth, window.innerHeight); // 显示区域尺寸变了,我们也需要修改渲染器的比例
透视相机是最常用的也是模拟人眼视角的一种相机,它所渲染生成的页面是一种近大远小的效果。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ROq1fhDN-1628039368571)(D:\chenyh\ienyh\resourses\PerspectiveCamera.jpg)]
fov
值(见上图右边部分),这个值是用来确定相机前方的垂直视角,像人眼一样,角度越大,视野越大,我们能够查看的内容就越多。aspect
,这个宽高比最好设置成页面显示区域的宽高比,这样我们查看生成画面才不会出现拉伸变形的效果,这时,我们可以确定前面生成内容的范围是一个四棱锥的区域。near
和最大值 far
,注意,这两个值都是距离相机的距离(确定完数值后,相机会显示的范围就是一个近小远大的四棱柱的范围,我们能够看到的内容都是在这个范围内的)const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); // 创建 PerspectiveCamera 相机(fov,aspect,near,far)
定义后同样可以动态修改属性,注意修改完以后调用相机的 updateProjectionMatrix()
方法来更新
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); // 创建相机(fov,aspect,near,far)
perspectiveCamera.fov = 75; // 修改 fov
perspectiveCamera.aspect = window.innerWidth/window.innerHeight; // 修改 aspect
perspectiveCamera.near = 100; // 修改 near
perspectiveCamera.far = 500; // 修改 far
perspectiveCamera.updateProjectionMatrix(); // 更新
同样的,由于浏览器的窗口可以随意修改,需要监听浏览器窗口的变化,然后获取到最新的宽高比,再重新设置相关属性:
function onWindowResize () {
camera.aspect = window.innerWidth / window.innerHeight; // 重新设置宽高比 aspect
camera.updateProjectionMatrix(); // 更新相机
renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器渲染页面大小
}
window.onresize = onWindowResize;
粒子和精灵的效果是一样的,它们之间的区别是,如果当前场景内的精灵过多的话,就会出现性能问题。粒子的作用就是为解决很多精灵而出现的,我们可以使用粒子去模型数量很多的效果,比如下雨,下雪等,数量很多的时候就适合使用粒子来创建,相应的,提高性能的损失就是失去了对单个精灵的操作,所有的粒子的效果都是一样。总的来说:
粒子 THREE.Points
和精灵 THREE.Sprite
还有网格 THREE.Mesh
都属于 THREE.Object3D
的一个扩展
但是粒子有一些特殊的情况就是 THREE.Points
是它们粒子个体的父元素,它的位置设置也是基于 THREE.Points
位置而定位,而修改 THREE.Points
的 scale
属性只会修改掉粒子个体的位置
下面是通过球体几何体创建的一个最简单的粒子特效(选用其他任何几何体均可,包括自己生成的几何体)
// 初始化粒子
function initPoints () {
const sphereGeometry = new THREE.SphereGeometry(5, 24, 16); // 球
const sphereMaterial = new THREE.PointsMaterial({ color: 0xff00ff });
const spherePoints = new THREE.Points(sphereGeometry, sphereMaterial);
scene.add(spherePoints);
}
另外,使用一个空的几何体,将自己创建的顶点坐标放入,也可以实现一组粒子的创建。如果我们需要单独设置每一个粒子的颜色,可以给 geometry
的 colors
数组添加相应数量的颜色:
for (let i = 0; i < 10000; i++) {
const star = new THREE.Vector3();
// THREE.Math.randFloatSpread 在区间内随机浮动* - 范围 / 2 *到* 范围 / 2 *内随机取值。
star.x = THREE.Math.randFloatSpread(2000);
star.y = THREE.Math.randFloatSpread(2000);
star.z = THREE.Math.randFloatSpread(2000);
starsGeometry.vertices.push(star);
starsGeometry.colors.push(new THREE.Color("rgb(" + Math.random() * 255 + ", " + Math.random() * 255 + ", " + Math.random() * 255 + ")")); // 添加一个随机的颜色
}
通过设置 THREE.PointsMaterial
属性去设置粒子的样式
▫️ color
属性设置颜色:
const sphereMaterial = new THREE.PointsMaterial({ color: 0xff00ff }); // 设置了颜色
▫️ size
属性设置粒子的尺寸:
const pointsMaterial = new THREE.PointsMaterial({ color: 0xff00ff, size: 4 }); // 粒子的尺寸改为原来的四倍
pointsMaterial.size = 4; // 或者直接设置属性
▫️ 设置纹理:
const pointsMaterial = new THREE.PointsMaterial({ color: 0xff00ff, map: texture });
▫ 设置 lights
属性为 true
,让粒子受光照影响(默认粒子是不受光照影响的)
const pointsMaterial = new THREE.PointsMaterial({ color: 0xff00ff, lights: true }); // 开启受光照影响
pointsMaterial.lights = true; // 或者直接设置属性
▫ 设置粒子不受到距离的影响产生近大远小的效果
const pointsMaterial = new THREE.PointsMaterial({ color: 0xff00ff, sizeAttenuation: false }); // 关闭粒子的显示效果受距离影响
pointsMaterial.sizeAttenuation = false; // 或者直接设置属性
从官网下载的代码包里可以发现有很多的相机控制器,文件夹地址为:/examples/js/controls/
,里面的文件插件都是和控制相机和控制模型相关的插件
下面重点介绍三种常用的相机控制器
使用 OrbitControls 控制器我们可以实现旋转、缩放、平移等功能
首先,将插件文件引入到项目中(注意路径修改)
<script src="../js/controls/OrbitControls.js">script>
然后,通过相机和渲染器的 Dom 对象实例化相机
const orbitControl = new THREE.OrbitControls(camera, renderer.domElement);
最后,在每一帧渲染里面更新相机的位置
function render () {
orbitControl.update();
renderer.render(scene, camera);
}
OrbitControls 控制器最大的优势就是有丰富的配置项,供我们修改来实现项目中的需求
属性 | 描述 |
---|---|
enabled | 是否开启当前控制器,默认值是 True,如果设置为 False,将无法通过操作修改相机。 |
target | 控制器的焦点位置,是一个 THREE.Vector3 对象,默认是 (0, 0, 0) |
minDistance | 相机距离焦点的最近距离,默认值是0。 此属性适用于透视相机 PerspectiveCamera。 |
maxDistance | 相机距离焦点的最远距离,默认值是 Infinity(无限远), 此属性适用于透视相机 PerspectiveCamera。 |
minZoom | 相机距离焦点的最近距离,默认值是 0,此属性适用于正交相机 OrthographicCamera。 |
maxZoom | 相机距离焦点的最远距离,默认值是 Infinity(无限远),此属性适用于正交相机 OrthographicCamera。 |
minPolarAngle | 相机位置和焦点与焦点和最上方组成的最小夹角限制,默认值是 0。 |
maxPolarAngle | 相机位置和焦点与焦点和最上方组成的最大夹角限制,默认值是 Math.PI ,也就是 180 度角。 |
minAzimuthAngle | 当前相机沿水平方向顺时针旋转的弧度,默认值是 - Infinity 。 |
maxAzimuthAngle | 当前相机沿水平方向逆时针旋转的弧度,默认值是 Infinity 。 |
enableDamping | 是否开启拖拽惯性移动,即拖拽停止相机会有缓慢停止的距离移动,默认值是 false。 |
dampingFactor | 拖拽惯性移动的阻力,默认值是 0.25。 |
enableZoom | 是否开启缩放操作,默认值是 true。 |
zoomSpeed | 缩放速度,默认值是 1.0。 |
enableRotate | 是否开启相机绕焦点旋转操作,默认值是 true。 |
rotateSpeed | 旋转速度,默认值是 1.0。 |
enablePan | 是否开启相机平移操作,默认值是 true。 |
panSpeed | 平移的速度,默认值是 1.0。 |
screenSpacePanning | 修改相机平移的方向,默认值是 false,即沿 x 轴正负方向和 y 轴正负方向移动。可选值是 true,可以修改为沿 x 轴正负方向和 y 轴正负方向移动。 |
keyPanSpeed | 键盘上下左右键移动相机的速度,默认值是 7.0。 |
autoRotate | 当前相机是否自动旋转,默认值是 false,不自动旋转。 |
autoRotateSpeed | 自动旋转的速度,默认值是 2.0,即渲染满 60 帧的情况下 30 秒旋转 360 度。 |
enableKeys | 是否开启键盘控制先机平移,默认值是 true。 |
update()
OrbitControls 控制器更新相机的方法,需要在每一帧里面调用。
reset()
重置方法,相机回到初始位置。
dispose()
销毁当前实例化的 OrbitControls 控制器。
控制器修改了相机,将会产生一个回调:
orbitControl.addEventListener('change', () => {
console.log("camera changed");
});
TrackballControls 控制器比 OrbitControls 控制器更自由,TrackballControls 控制器能够沿焦点进行球形旋转,没有死角,但比 OrbitControls 控制器少一些相关的功能配置。
注意,透视相机和正交相机使用的不是一个插件,此插件为透视相机使用,如果是正交相机请使用 OrthographicTrackballControls。
使用 TrackballControls 控制器我们可以实现旋转、缩放、平移等功能
首先,将插件文件引入到项目中
<script src="../js/controls/TrackballControls.js">script>
然后,通过相机和渲染器的 Dom 对象实例化相机
const trackballControl = new THREE.TrackballControls(camera, renderer.domElement);
最后,在每一帧渲染里面更新相机的位置
function render () {
trackballControl.update();
renderer.render(scene, camera);
}
属性 | 描述 |
---|---|
enabled | 是否开启当前控制器,默认值是 true,如果设置为 false,将无法通过操作修改相机。 |
rotateSpeed | 控制相机旋转速度,默认值是 3.0。 |
zoomSpeed | 控制相机缩放速度,默认值是 1.2。 |
panSpeed | 控制相机平移速度,默认值是 0.3。 |
noRotate | 关闭相机旋转,默认 false,开启。 |
noZoom | 关闭相机缩放,默认 false,开启。 |
noPan | 关闭相机移动,默认 false 开启。 |
staticMoving | 关闭拖拽惯性移动 默认值 False,开启。 |
dynamicDampingFactor | 拖拽惯性移动阻力,默认值是 0.2。 |
minDistance | 相机距离焦点的最近距离,默认值是 0。 |
maxDistance | 相机距离焦点的最远距离,默认值是 Infinity(无限远)。 |
update()
OrbitControls 控制器更新相机的方法,需要在每一帧里面调用。
reset()
重置方法,相机回到初始位置。
dispose()
销毁当前实例化的 OrbitControls 控制器。
控制器修改了相机,将会产生一个回调:
trackballControl.addEventListener('change', () => {
console.log("camera changed");
});
DeviceOrientationControls 只兼容含有陀螺仪的移动端,可以通过获取设备的陀螺仪状态来控制相机的朝向。
首先,将插件文件引入到项目中(注意路径修改)
<script src="../js/controls/DeviceOrientationControls.js">script>
然后,通过相机对象实例化相机控制器对象
const deviceOrientationControls = new THREE.DeviceOrientationControls(camera);
最后,在每一帧渲染里面更新相机的位置
function render () {
deviceOrientationControls.update();
renderer.render(scene, camera);
}
Enabled
dispose()
deviceOrientationControls.dispose(); // 销毁当前控制器
用于简单模拟 3 个坐标轴的对象:红色代表 X 轴,绿色代表 Y 轴,蓝色代表 Z 轴。
AxesHelper(size: number)
size (可选参数) 表示代表轴的线段长度,默认为 1
const axexHelper = new THREE.AxesHelper(20);
注意可能之前版本是叫 AxisHelper 现在重命名为 AxesHelper
市面上的 3D 模型有上百种,每一种格式都有不同的用途,不同的功能和复杂程度。尽管 Three.js 提供了很多的加载器,但选择正确的格式和工作流程将为以后的工作节省大量时间和成本。
官方推荐我们使用的 3D 模型的格式为 glTF,由于 glTF 专注于传输,因此它的传输和解析的速度都很快。glTF 模型的功能包括网格、材质、纹理、蒙皮、骨骼、变形动画、骨骼动画、灯光以及相机。
如果当前的首选不是 glTF 格式,那么推荐使用 Three.js 定期维护并且流行的格式 FBX、OBJ 或者 COLLADA 格式,Three.js 也有自己独有的 JSON 格式。
这里的 JSON 格式指的是 Three.js 可以将其转换为场景 3D 对象的 JSON 格式模型。这种格式内部一般必有的四项为:
所有的模型网格,几何体和材质都有一个固定的 UUID 标识符,在 JSON 格式中均通过 UUID 引用。
Three.js 给我们提供了一个 toJSON()
的方法:用于将 THREE.Object3D
对象都可以转成 JSON 字符串保存成为文件(我们不能直接将对象转成 JSON,因为 JSON 无法保存函数,所以需要使用 Three.js 提供的 toJSON()
方法)
const meshJSONObj = mesh.toJSON(); // 将一个模型网格转成 JSON 对象
const sceneJSONObj = scene.toJSON(); // 将整个场景的内容转换成为 JSON 对象
const sceneJSONStr = JSON.stringify(sceneJSONObj); // 将 JSON 对象转换成 JSON 字符串
使用 Three.js 内置的对象 THREE.ObjectLoader
加载模型。
THREE.ObjectLoader
实例的 parse()
方法):const sceneJSONObj = scene.toJSON(); // 将整个场景的内容转换成为 JSON 对象
const loader = new THREE.ObjectLoader(); // 实例化 ObjectLoader 对象
const scene = loader.parse(sceneJSONObj); // 将 json 对象再转换成 3D 对象
THREE.ObjectLoader
实例的 load()
方法)const loader = new THREE.ObjectLoader(); // 实例化 ObjectLoader 对象
// 加载模型,并在回调中将生成的模型对象添加到场景中
loader.load("./scene.json", group => {
scene.add(group);
});
glTF 格式的 3D 格式文件是官方推荐的使用格式,这种格式的文件我们可以在 Sketchfab 官网下载,这是一个国外比较知名的模型网站。
加载 glTF 模型的流程:
首先,将 GLTFLoader 加载器插件引入到页面,插件在官方包的 /examples/js/loaders/
文件夹中,一些文件的导入插件都在这个文件夹内(根据具体路径修改导入的路径)
<script src="../js/loaders/GLTFLoader.js">script>
然后创建一个加载器
const GLTFLoader = new THREE.GLTFLoader();
使用加载器加载模型,并调节一下模型大小在场景内展示
GLTFLoader.load('./scene.gltf', gltf => {
gltf.scene.scale.set(1, 1, 1);
scene.add(gltf.scene);
});
FBX 最大的用途是,在诸如 Max、Maya、Softimage 等软件间进行模型、材质、动作和摄影机信息的互导,这样就可以发挥 Max 和 Maya 等软件的优势。可以说 FBX 是最好的互导方案。
加载 FBX 模型的流程:
首先我们需要导入 FBXLoader 插件,并且还需要额外增加一个解析二进制文件的插件 inflate.min.js
,不导入该文件的话,除了一些字符串存储的 FBX 格式,别的格式都会报错:
<script src="../js/loaders/inflate.min.js">script>
<script src="../js/loaders/FBXLoader.js">script>
创建 FBX 加载器
const FBXLoader = new THREE.FBXLoader();
修改模型大小,并设置每个模型网格可以投射阴影
FBXLoader.load('./demo.fbx', fbx => {
fbx.scale.set(.1,.1,.1);
fbx.traverse(item => {
if(item instanceof THREE.Mesh){
item.castShadow = true;
item.receiveShadow = true;
}
});
scene.add(fbx);
});
OBJ 文件是 3D 模型文件格式。由 Alias|Wavefront 公司为 3D 建模和动画软件 Advanced Visualizer 开发的一种标准,适合用于 3D 软件模型之间的互导,也可以通过 Maya 读写。
OBJ 文件是一种文本文件,可以直接用写字板打开进行查看和编辑修改,但不包含动画、材质特性、贴图路径、动力学、粒子等信息。
OBJ 文件的导出通常会和 MTL 格式一同导出,MTL 作为 OBJ 文件的附属文件,有着 OBJ 文件需要的贴图材质,所以,我们通常使用时,将它们两个文件*(MTL & OBJ)*一同导入。
导入 OBJ 模型的流程:
OBJLoader 插件和 MTLLoader 插件引入页面
<script src="../js/loaders/OBJLoader.js">script>
<script src="../js/loaders/MTLLoader.js">script>
创建一个 MTL 加载器
const MTLLoader = new THREE.MTLLoader(); // 创建 MTL 加载器
MTLLoader.setPath('../js/models/obj/'); // 设置文件路径
如有需要,还可以设置纹理文件夹地址
MTLLoader.setTexturePath('../js/models/obj/'); // 设置纹理文件路径
加载 MTL 文件,并在文件加载成功后,创建 OBJLoader 并设置对象应用当前的材质:
// 加载 mtl 文件
MTLLoader.load('./test.mtl', material => {
const objLoader = new THREE.OBJLoader(); // 创建 OBJ 加载器
objLoader.setMaterials(material); // 设置当前加载的纹理
objLoader.setPath('../js/models/obj/');
objLoader.load('female02.obj', object => {
// 添加阴影
object.traverse(item => {
if (item instanceof THREE.Mesh) {
item.castShadow = true;
item.receiveShadow = true;
}
});
object.scale.set(3, 3, 3); // 设置大小缩放
scene.add(object);
})
});
COLLADA 是一个开放的标准,最初用于 3D 软件数据交换,由 SCEA 发起,现在则被许多著名厂家(如 Autodesk、XSI 等)支持。COLLADA 不仅仅可以用于建模工具之间的数据交换,也可以作为场景描述语言用于小规模的实时渲染。
COLLADA DOM 拥有丰富的内容用于表现场景中的各种元素,从多边形几何体到摄像机无所不包。我们可以通过 COLLADA DOM 库来进行场景文件的读取与处理操作。
引入 ColladaLoader 插件
<script src="../js/loaders/ColladaLoader.js">script>
实例化 ColladaLoader 对象
const colladaLoader = new THREE.ColladaLoader();
修改模型大小,并设置每个模型网格可以投射阴影
colladaLoader.load('../js/models/collada/elf.dae', collada => {
// 添加阴影
collada.scene.traverse(item => {
if (item instanceof THREE.Mesh) {
item.castShadow = true;
item.receiveShadow = true;
}
});
collada.scene.scale.set(5, 5, 5); // 设置大小缩放
scene.add(collada.scene);
});
一般情况下都是将自身导入,比如 FBX,OBJ,JSON 等,还有一种,会在里面生成一个可导入 scene 属性,如 glTF 和 COLLADA 文件。如果导入哪部分你无法确定,你可以把模型对象打印到控制台查看,然后尝试往场景内导入。
这种情况可能由多种情况造成的,一般主要有下面两种情况:
动画一般可以分为两种:一种是变形动画,另一种是骨骼动画。
变形动画,通过修改当前模型的顶点位置来实现。
比如,一个动画需要变动十次才可以实现,那么我们需要为当前模型的每一个顶点定义每一次所在的位置,Three.js 通过这一次次的修改实现了动画的整个流程。
首先,创建模型的几何体,并为几何体 morphTargets
赋值两个变形目标。morphTargets
是一个数组,我们可以为其增加多个变形目标。在给 morphTargets
添加变形目标时,需要为其定义一个名称和相关的顶点,这个顶点数据必须和默认的模型的顶点数据保持一致,设置完后,我们需要调用 geometry 的 computeMorphNormals()
进行更新,代码如下:
const cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
// 创建两个影响立方体的变形目标
const cubeTarget1 = new THREE.BoxGeometry(2, 10, 2);
const cubeTarget2 = new THREE.BoxGeometry(8, 2, 8);
// 将两个geometry的顶点放入到立方体的morphTargets里面
cubeGeometry.morphTargets[0] = {name: 'target1', vertices: cubeTarget1.vertices};
cubeGeometry.morphTargets[1] = {name: 'target2', vertices: cubeTarget2.vertices};
cubeGeometry.computeMorphNormals();
实现骨骼动画,我们需要生成一个与模型相关的骨架。骨架中的骨骼与骨骼之间存在关联,模型的每一个要动的顶点需要设置影响它的骨骼以及骨骼对顶点的影响度。
和变形动画相比,骨骼动画更复杂一些,但又有更多的灵活性。使用变形动画,我们需要把所有的每一次的变动都存在一个顶点数组中,而骨骼动画,只需要设置骨骼的相关信息,就可以实现更多的动画。
变形动画主要用于精度要求高的动画,比如人物的面部表情。其优点是动画的展现效果很到位,缺点就是扩展性不强,只能执行设置好的相关动画。
骨骼动画主要用于精度要求相对低一些,但需要丰富多样的动画的场合,就比如人物的走动,攻击防御等动画,我们可以通过一套骨骼,修改相应骨骼的位置信息直接实现相应的效果。它没有变形动画的精度高,但可以实现多种多样的效果。
**总结:**我们可以根据项目的需求来设置不同的动画,就比如一个人物模型,说话我们使用变形动画去实现,而肢体动作使用骨骼动画去实现。
浏览器是一个 2D 视口,而 Three.js 展示的是 3D 场景。场景交互时,需要在二维平面中控制三维场景的模型,那如何将 2D 视口的 x 和 y 坐标转换成 Three.js 场景中的 3D 坐标呢?
好在 Three.js 已经有了解决相关问题的方案,那就是 THREE.Raycaster
射线,用于鼠标拾取(计算出鼠标移过的三维空间中的对象)等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-52TdwZk7-1628039368573)(D:\chenyh\ienyh\resourses\Raycaster.jpg)]
THREE.Raycaster
构造函数包含了四个参数:
origin
:光线投射的原点矢量;direction
:光线投射的方向矢量,应该是被归一化的;near
:投射近点,用来限定返回比 near
要远的结果。near
不能为负数,缺省为 0
;far
:投射远点,用来限定返回比 far
要近的结果。far
不能比 near
小,缺省为无穷大。const raycaster = new THREE.Raycaster(origin, direction, near, far);
THREE.Raycaster
的属性可以在实例化对象后有修改需求时再修改。除了上面提到的 origin
、direction
、near
、far
四个属性外,我们还有可能用到另一个属性:
linePrecision
:射线和线相交的精度,浮点数类型。set()
可以重新设置射线的原点和方向,从而更新射线位置。
raycaster.set(origin,direction)
参数 origin 用来设置射线新的原点矢量位置,direction 用来设置基于原点位置的射线的方向矢量。
setFromCamera()
使用当前相机和界面的 2D 坐标设置射线的位置和方向。
raycaster.setFromCamera(coords, camera);
参数 coords 表示鼠标的二维坐标,在归一化的设备坐标(NDC)中,也就是 X 和 Y 分量,应该介于 -1 和 1 之间。camera 表示射线起点处的相机,即把射线起点设置在该相机位置处。
点击事件大多通过鼠标触发,我们用鼠标点击显示区域的位置和当前场景使用的相机对象调用此对象,Three.js 会为我们计算出当前射线的位置。
intersectObject()
& intersectObjects()
两个方法用来检查射线和物体之间的所有交叉点数据。
如果检测射线和一个对象是否相交,推荐使用 intersectObject()
,如果判断的是这个对象的子对象,那推荐使用 intersectObjects()
,将 3D 对象的 children
属性传入。
两个方法的返回值均为交叉点对象数组,如果射线与场景内的模型没有相交,将返回一个空数组,否则,将返回一个按从近到远顺序排列的对象数组
参数 object,用来检测和射线相交的物体。如果 recursive 设置为 true,还会向下继续检查所有后代,否则只检查该对象本身,缺省值为 false。optionalTarget 为可选参数,用于设置放置结果的数组,如果缺省,则将会实例化一个新数组,并将获取到的数据放入其中。
intersectObject()
和 intersectObjects()
的区别在于第一个参数。intersectObject 的第一个参数为 3D 对象,而 intersectObjects 需要传入一个由 3D 对象组成的数组。
如果射线与场景内的模型没有相交,将返回一个空数组,否则,将返回一个按从近到远顺序排列的对象数组,数组中每个对象的内容为:
[{ distance, point, face, faceIndex, indices, object }, ...]
const threeDom = document.querySelector("#three");
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.shadowMap.enabled = true;
renderer.setSize(threeDom.offsetWidth, threeDom.offsetHeight, false);
threeDom.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 4000);
camera.position.set(10, 40, 50);
const boxGroup = new THREE.Group();
const geometry = new THREE.BoxGeometry(30, 3, 30);
for (let i = 0; i < 20; i++) {
const material = new THREE.MeshBasicMaterial({ color: 0x373d41});
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, i * 3, 0);
cube.index = i; // 设置 cube 额外属性记录 index
cube.isFloat = false; // 设置 cube 额外属性记录 是否浮动 isFloat
boxGroup.add(cube);
}
scene.add(boxGroup);
let beforeX, beforeY, afterX, afterY;
threeDom.addEventListener('mousedown', e => {
[ beforeX, beforeY ] = [ e.offsetX, e.offsetY ];
});
threeDom.addEventListener('mouseup', e => {
[ afterX, afterY ] = [ e.offsetX, e.offsetY ];
});
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
threeDom.addEventListener("click", (event) => {
if (beforeX === afterX && beforeY === afterY) {
mouse.x = (event.offsetX / threeDom.offsetWidth) * 2 - 1;
mouse.y = - (event.offsetY / threeDom.offsetHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera); // 更新鼠标和射线位置
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0 && Reflect.has(intersects[0].object, "index")) {
boxGroup.children[this.currentHeightLight].material.color.set(0x373d41);
this.currentHeightLight = this.changeHighLight(intersects[0].object); // 设置高亮方法
const boxes = boxGroup.children;
for (let i = 0; i < boxes.length; i++) {
if (i >= this.currentHeightLight + 1 && !boxes[i].isFloat) {
boxes[i].isFloat = true;
boxes[i].position.y += 3;
}
if (i < this.currentHeightLight + 1 && boxes[i].isFloat) {
boxes[i].isFloat = false;
boxes[i].position.y -= 3;
}
}
}
}
});
// 添加 background
const plane = new THREE.PlaneGeometry(1000, 1000);
const planeMaterial = new THREE.MeshPhongMaterial({ color: 0x000066, side: THREE.DoubleSide });
const planeMesh = new THREE.Mesh(plane, planeMaterial);
planeMesh.rotation.x = Math.PI * 1 / 2;
planeMesh.position.set(0, -8, -8);
planeMesh.receiveShadow = true;
scene.add(planeMesh);
// 添加灯光
scene.add(new THREE.AmbientLight(0x222244, 1.5));
const light = new THREE.DirectionalLight({ color: 0x222244 });
light.castShadow = true;
light.position.set(8, 8, 10);
scene.add(light);
// 引入相机控制器
const controls = new OrbitControls(camera, renderer.domElement);
scene.add(new THREE.AxesHelper(80));
const stats = new ThreeStats.Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
stats.domElement.style.left = '0px';
threeDom.appendChild(stats.domElement);
// 设置一个动画函数
const animate = function () {
// 一秒钟调用 60 次,也就是以每秒 60 帧的频率来绘制场景。
requestAnimationFrame(animate);
// cube.rotation.y += 0.01;
stats.update();
controls.update();
renderer.render(scene, camera);
};
animate();
window.addEventListener("resize", () => {
camera.aspect = threeDom.offsetWidth / threeDom.offsetHeight;
camera.updateProjectionMatrix();
renderer.setSize(threeDom.offsetWidth, threeDom.offsetHeight)
});