一、简介
three.js 是 JavaScript 编写的 WebGL 第三方库,开发者可以不用关注 WebGL 底层就能制作出一些酷炫的效果
WebGL 是在浏览器中实现三维效果的一套规范,通过 can i use 查询可知,现在的主流浏览器基本支持 webGL:
应用范围:
在小游戏、产品展示、物联网、数字孪生、智慧城市园区、机械、建筑、全景看房、GIS等各个领域基本上都有 three.js 的身影:
小游戏:____________________ (bruno-simon.com)(碰撞检测,相机跟随)
产品展示:WEBGi Jewelry Landing Page Demo - by Neotix (webgi-jewelry.vercel.app) (轨道控制器,渲染贴图、材质)
智慧城市园区:瞰图智慧物流展示平台 (kantu3d.com)
全景看房:WebVR Showroom by Little Workshop (Raycaster 光线投射)
二、在页面中展示第一个模型
- 安装 Three 的 npm 模块 npm install three --save
- 需要一对 canvas 作为容器,渲染的模型会在 canvas 中展示
ThreeJS 基本三要素:
- scene 场景
- camera 相机
- render 渲染器
关于场景:
在真实世界中,天空就是场景,大家都知道天空是蓝白色的,那么开发时,我们试着给场景添加蓝白色的背景颜色
const scene = new THREE.Scene()
scene.background = new THREE.Color('#00DCFF')
在threejs中,场景是右手坐标系,把右手放在原点的位置,使大拇指,食指和中指互成直角
把大拇指指向 x 轴的正方向,食指指向 y 轴的正方向时,中指所指的方向就是z轴的正方向
关于相机
真实世界中,看见物体需要一双眼睛,在ThreeJS里相机相当于你的眼睛
Threejs 提供两种相机,根据实际场景选择不同的相机
正交相机:
无论物体距离相机距离远或者近,在最终渲染的图片中物体的大小都保持不变。用于渲染2D场景或者UI元素是非常有用的
- 图中红色三角锥体是视野的大小
- 红色锥体连着的第一个面是摄像机能看到的最近位置
- 从该面通过白色辅助线延伸过去的面是摄像机能看到的最远的位置
透视相机:
它是3D场景的渲染中使用得最普遍的投影模式,被用来模拟人眼所看到的景象。
关于透视相机的几个参数,new THREE.PerspectiveCamera(fov, width / height, near, far)
- fov(field of view) — 摄像机视锥体垂直视野角度
- aspect(width / height) — 摄像机视锥体长宽比
- near — 摄像机视锥体近端面
- far — 摄像机视锥体远端面
const camera = new THREE.PerspectiveCamera(
50, // 透视摄像机,垂直视野角度
window.innerWidth / window.innerHeight, // 摄像机横纵比
10, // 近端面
6000 // 远端面
)
// 手动调整摄像机位置
camera.position.x = 0
camera.position.z = 600
camera.position.y = 480
关于渲染器
WebGLRenderer 该对象的属性定义了渲染器的行为。也可以完全不传参数。当缺少参数时,它将采用合理的默认值
需要先获取 dom 元素,作为参数创建 renderer 对象,同时渲染这一帧的画面
const canvas = document.querySelector('#three') // 将 canvas 绑定到元素上
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true // 抗锯齿
})
renderer.render(scene, camera) // 渲染器只渲染当前帧
得注意,如果场景引入了模型或者使用了动画,需要循环渲染场景!!
关于几何体
ThreeJS 提供很多不同类型的几何体,我们可以使用这些基本模型搭建场景
BoxGeometry – three.js docs (threejs.org) (需开代理)
导入外部模型
实际开发中,大多数项目,通常是3D美术设计师或建筑、机械等行业工程师提供的由3dmx、blender、substence、Solidworks等软件创建好的三维模型文件
官方对 GLTF 格式的模型支持较好
我们试着在在线 GLTF Editor 建模平台,设计一个模型(200 × 200 × 150) glTF Editor (gltfviewer.com)
同时引入GLTF Loader: import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
const gltfLoader = new GLTFLoader()
gltfLoader.load('../static/models/scene.gltf', (gltf) => {
const model = gltf.scene
scene.add(model)
})
导入模型后,之前的渲染仅仅只是一帧的画面。为了让场景中的物体能动起来,我们需要使用 requestAnimationFrame,所以我们可以写一个 loop 函数
requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,节约性能
/**
* 循环渲染每一帧
*/
function animate() {
renderer.render(scene, camera) // 刷新渲染器
requestAnimationFrame(animate)
}
animate()
关于 gltf 模型的性能:
在 ThreeJS 里 3D 模型如同矢量图形,支持不失真缩放,所有无限放大和缩小模型是不影响加载和展示性能
gltf (图形语言传输格式) 本质上和 json 格式相似,主要存储点、线、面等数据,性能主要是受三角面数量影响 glTF Viewer 2.0 - 3D模型在线查看 | BimAnt
关于 gltf 存储格式:
glTF 的特点:传输和解析高效
json中包含的描述信息,内容详细,比如mesh,纹理,蒙皮和动画,定义了accessor的访问器规则,同时还给出了相机,节点这些场景管理的信息
尝试添加光源
现在模型出来了,但是一片黑色(我们的立方体是红色的),还需要添加一些光源照亮它们
本次使用 HemisphereLight(半球光)和 DirectionaLight(平行光),能更好的创建出更加贴切⾃然的户外光照效果
// 添加个平行光
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(-10, 8, -5); // 光源位置
dirLight.castShadow = true; // 可以产生阴影
dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
scene.add(dirLight);
// 添加个半球光
const hemLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);
hemLight.position.set(0, 100, 0);
scene.add(hemLight);
调整设备物理像素分辨率与CSS像素分辨率的比值
目前的模型较模糊,原因是 ThreeJS 默认的宽高 150×100,而页面中宽高被我们设置成 100%,相当于图片被放大模糊,所以场景也需要同步调整大小以保持比例
不断检查 renderer 的尺寸是否与 canvas 相等,若不等则设置 renderer 的尺寸
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
var width = window.innerWidth;
var height = window.innerHeight;
var canvasPixelWidth = canvas.width / window.devicePixelRatio;
var canvasPixelHeight = canvas.height / window.devicePixelRatio;
const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height // 判断 canvas 的宽高和 renderer 渲染的尺寸是否相等
if (needResize) {
renderer.setSize(width, height, false)
}
return needResize
}
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement
this.camera.aspect = canvas.clientWidth / canvas.clientHeight
this.camera.updateProjectionMatrix()
}
三、场景拓展(全景看房,天空盒贴图)
目前常用于实现全景看房的技术主要分两种,分别纹理贴图和 3D 建模
- 3D 建模实现全景看房 WebVR Showroom by Little Workshop (主要用在展示精细的场景,成本较高,花费时间长)
- 纹理贴图实现全景看房又分为 天空盒贴图 和 全景图片贴图(主要用在展示大概的场景,例如全景地图)
天空盒的原理
我们所处的场景看成是有前后、左右、上下6个面组成的,将我们所看到的这6个面的视觉镜像处理成图片,将其分别以纹理的形式添加到立方体中,这时,我们如果立身于这个立方体中,即可还原当时场景。
const path = './static/img/' // 设置路径
const directions = ['px', 'nx', 'py', 'ny', 'pz', 'nz'] // 贴图名称, 左右、上下、后前
const format = '.jpg' // 图片格式
// 1、创建盒子,并设置盒子的大小
const skyGeometry = new THREE.BoxGeometry( 10000, 10000, 10000 );
// 2、导入图片,创建材质
const skyMaterial = [];
directions.forEach((item) => {
// 加载纹理
const texture = new THREE.TextureLoader().load(path + item + format)
// 传入加载的纹理,创建材质
skyMaterial.push(new THREE.MeshBasicMaterial({ map: texture }))
})
const skyBox = new THREE.Mesh(skyGeometry, skyMaterial) // 传入盒子+材质
skyBox.geometry.scale(1, 1, -1) // 贴图反转
scene.add( skyBox );
全景图片贴图
全景图贴图就是使用一张鱼眼全景图片以纹理的形式添加到球体上
const url = 'https://cdn.huodao.hk/upload_img/20220621/6bd594e62ea5654c03d7b82718443751.png?proportion=1.99'
const geometry = new THREE.SphereGeometry(5000, 32, 32)
const texture = new THREE.TextureLoader().load(url)
const material = new THREE.MeshBasicMaterial({ map: texture })
const sphere = new THREE.Mesh(geometry, material)
sphere.geometry.scale(1, 1, -1)
scene.add(sphere)
缺点就是:对图片的要求很高(清晰度),目前有缺陷(白色线条)
四、相机拓展(轨道控制器,动态跟踪)
Orbit controls(轨道控制器)可以使得相机围绕目标进行轨道运动,实现类似产品 360° 展示的效果
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' // 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.autoRotate = true // 自动旋转
controls.autoRotateSpeed = 5.0 // 自动旋转
controls.maxPolarAngle = 1.65 // 相机旋转最大角度,不会超过地面
controls.enabled = false // 当设置为false时,控制器将不会响应用户的操作
同时轨道控制器需要 -> 控制视角(相机朝向),与动画一样必须每一帧循环渲染
function animate() {
renderer.render(scene, camera) // 刷新渲染器
controls.update(); // 刷新轨道控制器
requestAnimationFrame(animate)
}
动态跟踪 实现思路
1,定义关键固定位置坐标,用固定坐标值生成一组曲线。
2,将曲线分割成多份获取分割后的一组顶点坐标
3,加载物体并设置初始位置,这里用的是个飞机模型
4,通过Threejs的帧动画实时修改物体坐标和物体朝向,达到运动效果
(20条消息) Threejs实现镜头跟随物体移动效果,镜头拐弯并保持运动方向_左本的博客-CSDN博客_threejs 相机跟随
五、动画
ThreeJS 中实现动画有很多种方式,一种是直接在render循环中写动画逻辑,比如每次渲染的时候修改动画对象的一些属性
这种方法比较简单粗暴,缺点就是动画一多,控制起来比较麻烦,当然也可以使用一些成熟的第三方库,比如 Tween 来丰富动画,丰富过渡效果
var render = function () {
requestAnimationFrame(render);
step += 0.01;
cube.position.x = 2 + (1 * (Math.cos(step)));
cube.position.y = 2 + (10 * Math.abs(Math.sin(step)));
cube.rotation.x += 0.03;
cube.rotation.y += 0.03;
cube.rotation.z += 0.03;
renderer.render(scene, camera);
};
另一种是直接利用 GLTF 格式的优势:GLTF 格式中内置了动画信息
使用官方推荐的 blender 可以很简单的为模型添加上丰富的动画
使用时
let mixer
let carAction
const gltfLoader = new GLTFLoader()
gltfLoader.load('./static/models/scene3.gltf', (gltf) => {
const model = gltf.scene
scene.add(model)
/**
* 初始化动画
*/
// 我们需要创建 AnimationMixer,它就像音乐播放器一样,帮助我们播放动画
mixer = new THREE.AnimationMixer(gltf.scene)
// 使用 clipAction 方法将动画信息添加到 mixer 中
// action = mixer.clipAction(gltf.animations[0])
// action.play() // 自动播放
// 多个动画
// gltf.animations.forEach((item) => {
// mixer.clipAction(item).play()
// })
// 同样的,如果想单独控制某个动画,给它
// carAction = mixer.clipAction(gltf.animations[4])
})
同样的,作为帧动画,在必须每一帧循环渲染,用到THREE提供的时钟对象
const clock = new THREE.Clock() // 计帧器
if (mixer) mixer.update(clock.getDelta()) // 加到 animate 中循环更新
控制动画播放或者暂停:
carAction.play() // 动画初始化开始播放
carAction.stop() // 动画销毁
carAction.paused = true // 暂停后只有重新设置 false 才能继续
carAction.paused = false
六、射线检测
在场景中,常常需要对三维场景里的物体进行点击、拖拽等交互,这时候需要判断,鼠标是否点中了三维场景中的物体
通常是通过 raycaster(射线检测)进行检测,判断鼠标确实在这个点上
webGL中获取鼠标交互物体的原理:
通过三维空间中相机视点与鼠标在屏幕上的地位的连线,造成一条直线,捕捉与此直线相交的空间中的物体,即为交互对象物体
在three中,Raycaster为咱们封装了大量的逻辑代码,包含生成相机到鼠标的射线、射线与空间物体的碰撞检测、射线相交物体深度计算、相交物体列表等等
Raycaster类的.intersectObject()办法:检测所有在射线与物体之间,包含或不包含后辈的相交局部。返回后果时,相交局部将按间隔进行排序,最近的位于第一个
function raycasterTest(e) {
e.preventDefault()
const { clientX, clientY } = e
const dom = renderer.domElement
// 拿到canvas画布到屏幕的距离
const domRect = dom.getBoundingClientRect()
// 计算标准设备坐标 - 归一化设备坐标
const x = ((clientX - domRect.left) / dom.clientWidth) * 2 - 1
const y = -((clientY - domRect.top) / dom.clientHeight) * 2 + 1
const vector = new THREE.Vector3(x, y)
// 转世界坐标
const worldVector = vector.unproject(camera)
console.log('世界坐标', worldVector)
// 向量相减,并获取单位向量
const ray = worldVector.sub(camera.position).normalize()
// 射线投射对象, 第一个参数是射线原点 第二个参数是射线方向
const raycaster = new THREE.Raycaster(camera.position, ray)
raycaster.camera = camera
//返回射线选中的对象 //第一个参数是检测的目标对象 第二个参数是目标对象的子元素
const intersects= raycaster.intersectObjects(scene.children)
if (intersects.length > 0) {
console.log("捕获到对象", intersects);
if(intersects[0] && intersects[0].object && intersects[0].object.name === '立方体1') {
carAction.play()
}
} else {
console.log("没捕获到对象");
}
}
window.addEventListener('click', raycasterTest)
项目源码
wind-base: 关于 ThreeJS 一些尝试 (gitee.com)
参考文档
【译】基于 Three.js 实现了交互式 3D 人物 - 知乎 (zhihu.com)
threejs实现3d全景看房 - 掘金 (juejin.cn)
ThreeJs之选中模型中的物体及物体沿轨迹移动 - 简书 (jianshu.com)
(20条消息) Threejs实现镜头跟随物体移动效果,镜头拐弯并保持运动方向_左本的博客-CSDN博客_threejs 相机跟随
glTF简介 - 腾讯云开发者社区-腾讯云 (tencent.com)
glTF - 统一应用程序渲染 3D 内容的传输格式 - 掘金 (juejin.cn)
详解 glTF 格式及数据加载细节 - 掘金 (juejin.cn)
后续分享
- 深入场景、相机、渲染器
- 深入材质,贴图,丰富模型展示效果
- 分享碰撞检测、第一人称移动
- 性能优化,GLTF文件压缩 Draco Compression