重要概念(场景、相机、渲染器)
如下图所示,我们最终看到浏览器上生成的内容是通过虚拟场景和虚拟相机被渲染器渲染后的结果,下面首先介绍这三个概念,将贯穿所有简单复杂的threejs项目。
场景 Scene
虚拟的3D场景,用来表示模拟生活中的真实三维场景,或者说三维世界
const scene = new THREE.Scene() //创建场景//添加元素如模型、灯光等scene.add(元素)
相机 Camera
透视投影相机,如果渲染远小近大–透视投影相机,不需要远小近大–正投影相机,常用透视投影相机
正投影相机:OrthographicCamera
透视投影相机:PerspectiveCamera PerspectiveCamera( fov, aspect, near, far )
- fov:相机视锥体竖直方向视野角度 ,默认50
- aspect:相机视锥体水平方向和竖直方向长度比,一般设置为Canvas画布宽高比width / height ,默认1
- near:相机视锥体近裁截面相对相机距离,默认0.1
- far:相机视锥体远裁截面相对相机距离,far-near构成了视锥体高度方向,默认2000
具体使用:
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); // 设置相机位置camera.position.set(15, 20, 50); //控制相机的拍照目标,具体说相机镜头对准哪个物体或说哪个坐标camera.lookAt(0, 0, 0)//0,0,0是坐标圆点,(x,y,z)
注意: 相机实际形成一个视锥体,在这个范围内的物体才会被渲染出来,之外的物体不会被渲染到canvas上
在我们开发的时候,如果将far设置的比较小,在放大物体的时候,可以看到部分模型消失,只渲染了一部分,那么就是模型超出视锥体了,所以不被渲染。
渲染器 WebGLRenderer
有了相机和物体,则需要完成拍照,渲染器其实可以理解为拍照。
renderer=new THREE.WebGLRenderer();
进行创建 .domElement
可以获取到对应的元素。
renderer.render(场景, 相机);
:执行渲染的操作,类似我们按下相机快门的操作。
//创建一个渲染器,一个canvas场景
const renderer = new THREE.WebGLRenderer();renderer.setPixelRatio(window.devicePixelRatio); //设置像素比,如果你遇到你的canvas画布输出模糊问题一定要设置renderer.setSize(window.innerWidth, window.innerHeight); //设置canvas的大小
//锯齿属性
//renderer.antialias = true//插入html中,这里即添加到id为container的标签下
const container = document.getElementById('container')
container.appendChild(renderer.domElement);//最重要的!!!!渲染
renderer.render(scene, camera); //执行渲染操作
画布自适应
window.addEventListener("resize", this.resize);resize() {// console.log("画面变化了");// 更新摄像头let camera = this.cameracamera.aspect = window.innerWidth / window.innerHeight;// 更新摄像机的投影矩阵,如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵camera.updateProjectionMatrix();let renderer = this.renderer// 更新渲染器renderer.setSize(window.innerWidth, window.innerHeight);// 设置渲染器的像素比renderer.setPixelRatio(window.devicePixelRatio);},
光源和材质
首先介绍几个相关的概念:
- 外观-材质
Material
:想定义物体的外观效果,比如颜色,就需要通过材质Material相关的API实现。
- 物体-网格模型
Mesh
:在threejs中可以通过网格模型Mesh (opens new window)表示一个虚拟的物体,比如一个箱子、一个鼠标。 - 模型位置
position
const mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
//设置网格模型在三维空间中的位置坐标,默认是坐标原点
mesh.position.set(0,10,0);
光源
光源Light
:影响物体的明暗效果
不同材质对光照的影响不同,如图(材质不止下图,可到官网查看)
光源也有很多,具体的API推荐看官网(太多啦!)
材质
基础介绍,具体内容看官网
几何体
Three.js提供了各种各样的几何体API,用来表示三维物体的几何形状。
因为太多了就不在此赘述,可以到官网去查看对应的api和属性
引入外部三维模型
引入glb\gltf模型
我们在实际工作中,总是需要另外的引入模型,这里介绍引入模型的方法
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; //加载gltf模型的加载器
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' //解压用,不一定需要使用它const dracoLoader = new DRACOLoader()//设置解压工具位置dracoLoader.setDecoderPath('resources/draco/') //从node_modules\three\examples\jsm\libs\draco 中复制到public下resources/draco/ 注意要从自己的项目node_modules中复制,版本不对可能会报错dracoLoader.setDecoderConfig({ type: "js" });const loader = new GLTFLoader()loader.setDRACOLoader(dracoLoader)
//在vue中默认路径是public下的loader.load("model/city-v1.glb", function (gltf) { //const model = gltf.scene //拿到模型model.name = 'city' model.position.set(0, 0, 0) //设置模型的位置model.scale.set(0.7, 0.7, 0.7)scene.add(model)//添加到场景中})
绘制天空的效果
const loaderbox = new THREE.CubeTextureLoader() //加载CubeTexture的一个类。 内部使用ImageLoader来加载文件let path = 'night/', format = ".jpg"const cubeTexture = loaderbox.load([ //一定是六张图!!!path + 'posx' + format,path + 'negx' + format,path + 'posy' + format,path + 'negy' + format,path + 'posz' + format,path + 'negz' + format])scene.background = cubeTexture
更改引入模型的材质
//traverse threejs中深度遍历的方法,会遍历所有的元素model.traverse(child => {child.material = new THREE.MeshPhongMaterial({ //只能修改mesh的外观color: new THREE.Color('#123ca8'),transparent: true,opacity: 0.5,emissiveMap: Mesh.material.map,})})//或者,如果没有跟上一级有关系的,推荐用第一种model.children.forEach(item => {if (item.name !== selectTagName) {item.children.forEach(mesh => {mesh.material = new THREE.MeshPhongMaterial({color: new THREE.Color('#123ca8'),transparent: true,opacity: 0.5,emissiveMap: mesh.material.map,})})} })
给模型添加点击事件
document.addEventListener('click', this.handleClick, false);handleClick(e){// 射线交叉计算拾取模型const raycaster = new THREE.Raycaster()const mouse = new THREE.Vector2()let tag = this.tagBoxmouse.x = (e.offsetX / this.renderer.domElement.clientWidth) * 2 - 1mouse.y = -(e.offsetY / this.renderer.domElement.clientHeight) * 2 + 1raycaster.setFromCamera(mouse, this.camera); //一定要写,不然获取的就是空// 获取点击到的模型的数组,从近到远排列const intersects = raycaster.intersectObjects(this.scene.children, true); if (intersects.length > 0) {//intersects为点击的模型数组,可以在这里写后续的逻辑}
}
添加文字标签
可以有多种方式,这里介绍css2drender渲染
在animate中记得要写labelRenderer.render(scene, camera)
import {CSS2DObject, CSS2DRenderer
} from "three/examples/jsm/renderers/CSS2DRenderer";// 创建一个CSS2渲染器CSS2DRenderervar labelRenderer = new CSS2DRenderer();labelRenderer.setSize(window.innerWidth, window.innerHeight);labelRenderer.domElement.style.position = 'absolute';// 相对标签原位置位置偏移大小labelRenderer.domElement.style.top = '0px';labelRenderer.domElement.style.left = '0px';// //设置.pointerEvents=none,以免模型标签HTML元素遮挡鼠标选择场景模型labelRenderer.domElement.style.pointerEvents = 'none';document.body.appendChild(labelRenderer.domElement);function animate() {// 通过相机 场景 将结果渲染出来let delta = clock.getDelta();controls.update(delta);requestAnimationFrame(animate);renderer.render(scene, camera);labelRenderer.render(scene, camera); //渲染HTML标签对象}//如果有写resize记得在resize中也要写上//labelRenderer.setSize(window.innerWidth, window.innerHeight)animate()// 创建标签function tag(name) {var div = document.createElement('div');div.innerHTML = name;div.classList.add('lable-text');//div元素包装为CSS2模型对象CSS2DObjectvar label = new CSS2DObject(div);div.style.pointerEvents = 'none';//避免HTML标签遮挡三维场景的鼠标事件gsap.to(label.position, {y: 2,repeat: -1,duration: 2,yoyo: true,ease: "Bounce.inOut",})return label;//返回CSS2模型标签}//在想要创建标签的地方var label = tag('办公楼');//把粮仓名称obj.name作为标签var pos = new THREE.Vector3();model.getWorldPosition(pos);//获取obj世界坐标label.position.copy(pos);//位置设置为posmodel.add(label); //添加到某模型中
ThreeJs内置工具
AxesHelper
辅助坐标系,THREE.AxesHelper()的参数表示坐标系坐标轴线段尺寸大小,你可以根据需要改变尺寸。
默认y轴向上,x向右,z轴正对我们
红色:x轴
绿色:y轴
蓝色:z轴
const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);
DirectionalLightHelper
辅助查看DirectionalLight
光源的位置
const dirHelper = new THREE.DirectionalLightHelper(dirLight, 5); //辅助查看光源在哪里scene.add(dirHelper);
如下:
CameraHelper
投影相机,用于模拟相机视锥体的辅助对象.它使用 LineSegments 来模拟相机视锥体.
//方向光,常常用来表现太阳光照的效果。(颜色,强度)const dirLight = new THREE.DirectionalLight("rgb(253,253,253)", 10);dirLight.position.set(200, 200, 10);dirLight.castShadow = true;const cam = dirLight.shadow.camera;const cameraHelper = new THREE.CameraHelper(cam);scene.add(cameraHelper);cameraHelper.visible = true;scene.add(dirLight);
遇到问题描述:
在设置阴影时,只显示部分模型的阴影?
答:因为剩余模型没有在光源的视锥体内,所以没显示阴影,我们可以更改视锥体的大小位置来保证所有元素都显示阴影
//不一定都设置,根据需要设置dirLight.shadow.mapSize.width = 1024; // defaultdirLight.shadow.mapSize.height = 1024; // defaultdirLight.shadow.camera.near = 0.05; // defaultdirLight.shadow.camera.far = 400; // defaultdirLight.shadow.camera.top = 50dirLight.shadow.camera.right = 50dirLight.shadow.camera.left = -50dirLight.shadow.camera.bottom = -50
OrbitControls
平时开发调试代码,或者展示模型的时候,可以通过相机控件OrbitControls实现旋转缩放预览效果。
- 旋转:拖动鼠标左键
- 缩放:滚动鼠标中键
- 平移:拖动鼠标右键
通过鼠标滚轮、左右拖拽可以放大缩小、旋转查看模型,实际上是更改相机的位置
可以根据实际的需要设置,可以放大倍数,旋转的限制等。
因为在过程中一直更改模型,所以调用animate
或者监听change
保证更新界面
// #引入扩展库OrbitCoimport { OrbitControls } from "three/examples/jsm/controls/OrbitControls";let controls = new OrbitControls(camera, renderer.domElement); //创建控件对象controls.target.set(0, -1, 0); //相机控件.target属性在OrbitControls.js内部表示相机目标观察点,默认0,0,0controls.enableZoom = true;controls.update()controls.maxPolarAngle = Math.PI / 2 // 最大轨道高度为Πcontrols.minPolarAngle = 0 // 最大轨道高度为Πcontrols.autoRotate = true; //是否开启自动旋转const clock = new THREE.Clock(); //初始化时钟function animate() {// 通过相机 场景 将结果渲染出来let delta = clock.getDelta();controls.update(delta);requestAnimationFrame(animate);renderer.render(scene, camera);}animate()//如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
controls.addEventListener('change', function () {renderer.render(scene, camera); //执行渲染操作
});//监听鼠标、键盘事件
stats
监听性能,计算three.js的渲染帧率(FPS),所谓渲染帧率(FPS)
//引入
import Stats from 'three/addons/libs/stats.module.js';//创建stats对象
const stats = new Stats();
//stats.domElement:web页面上输出计算结果,一个div元素,
document.body.appendChild(stats.domElement);
// 渲染函数
function render() {//requestAnimationFrame循环调用的函数中调用方法update(),来刷新时间stats.update();renderer.render(scene, camera); //执行渲染操作requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
}
render();
gui.js
就是一个前端js库,调试用,可以在gui面板更改值,具体的使用的时候可以再深入的了解,这里只是简单的介绍
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';const gui = new GUI();
//改变交互界面style属性
gui.domElement.style.right = '0px';
gui.domElement.style.width = '300px';// 通过GUI改变mesh.position对象的xyz属性
gui.add(ambient, 'intensity', 0, 2.0);//更改模型位置
gui.add(mesh.position, 'x', 0, 180);
gui.add(mesh.position, 'y', 0, 180);
gui.add(mesh.position, 'z', 0, 180);
工具
模型查看
地址:gltf模型查看
可以查看图层、选中部分模型拖拽和模型一些属性值
在线Threejs地址
地址:国内地址
加载比官网快很多,不过是第三方培训机构的,不知道维护到什么时候,暂且用着。
gsap
地址:gsap官网
网上也有很多用tween的,看个人喜欢
动画库
例如:
gsap.to(this.door.scale, {x: this.door.scale.x * 8, //scale拿到门的缩放值duration: 5,ease: "power1.inOut",onComplete: () => {}});
连续的动画
const t1 = gsap.timeline();t1.to(that.door.scale, {x: that.door.scale.x / 8, //scale拿到门的缩放值duration: 5,ease: "power1.inOut",onComplete: () => {that.carMove = 'z'}});t1.to(model.position, {x: 8,y: 0,z: 24,duration: 5,ease: "linear",onComplete: () => {that.carMove = 'x'that.closeDoor()model.rotateY(Math.PI / 2) //旋转90度},})// 设置循环次数 -1为无限循环t1.repeat(-1);// 开始播放动画t1.play();