本案例用到的技能:
1.加载模型
2.相机移动
3.鼠标选中模型
4.在场景中加入新的物体
5.物体沿路径移动
5.第一视角巡视
7.canvas画布自适应-页面窗口改变时模型不变形
首先新建react项目,引入Three,新建一个加载模型的class方法类,用于处理模型相关的操作。
主要思想是将图形组件与前端页面分离,降低图形组件与前端业务逻辑的耦合度。在图形组件中将方法暴露出来,前端页面来调用即可。
图形组件的class方法
初始化及加载模型:
constructor(canvas, modelPath = "./models/") {
this.camera = new PerspectiveCamera(
45,
canvas.width / canvas.height,
0.1,
1000
);
// this.camera.position.set(0, 10, 15);
this.camera.position.set(
2.4595794148094408,
13.499328043352282,
0.8119053487039843
);
this.camera.lookAt(0, 0, 0);
this.normalCamera = this.camera;
this.renderer = new WebGLRenderer({ canvas });
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.modelPath = modelPath;
this.maps.set(
"cabinet-hover.jpg",
new TextureLoader().load(`${modelPath}cabinet-hover.jpg`)
);
this.creatBox();
this.addRoutes();
this.addLight();
}
loadGLTF(modelName = "") {
gltfLoader.load(this.modelPath + modelName, ({ scene: { children } }) => {
children.forEach((obj) => {
const { color, map, name } = obj.material;
this.changeMat(obj, map, color);
if (name.includes("cabinet")) {
this.cabinets.push(obj);
}
});
this.scene.add(...children);
});
}
// 修改Mesh 对象的材质
changeMat(obj, map, color) {
if (map) {
obj.material = new MeshBasicMaterial({
map: this.crtTexture(map.name),
});
} else {
obj.material = new MeshBasicMaterial({ color });
}
if (obj.name === "cabinet-010") {
obj.cameraPosition = {
x: 1.3604732850786612,
y: 2.6105915306590584,
z: 6.199682831211108,
};
obj.controlsPosition = {
x: 2.415939275692724,
y: -0.5427880982932958,
z: 0.38954958313860316,
};
}
if (obj.name === "cabinet-020") {
obj.cameraPosition = {
x: 0.6089158377757249,
y: 2.3543090823589896,
z: 3.302157733332054,
};
obj.controlsPosition = {
x: 2.725367213430147,
y: 0.38903559697724144,
z: -0.07168470117887349,
};
}
if (obj.name === "cabinet-030") {
obj.cameraPosition = {
x: -0.4005966957451239,
y: 2.6068066780517345,
z: 1.0418153228082048,
};
obj.controlsPosition = {
x: 2.064112146693374,
y: 1.4519662140510958,
z: -1.0561189115306107,
};
}
}
crtTexture(imgName) {
let curTexture = this.maps.get(imgName);
if (!curTexture) {
curTexture = new TextureLoader().load(this.modelPath + imgName);
curTexture.flipY = false;
curTexture.wrapS = 1000;
curTexture.wrapT = 1000;
this.maps.set(imgName, curTexture);
}
return curTexture;
}
渲染场景
// 渲染
render() {
this.renderer.render(this.scene, this.camera);
}
// 连续渲染
animate(time) {
requestAnimationFrame((time) => {
this.animate(time);
});
if (this.resizeRendererToDisplaySize(this.renderer)) {
const { clientWidth, clientHeight } = this.renderer.domElement;
this.camera.aspect = clientWidth / clientHeight; // camera.aspect是 相机视口的宽高比
this.camera.updateProjectionMatrix(); // 更新透视投影矩阵。
}
this.renderer.render(this.scene, this.camera);
}
canvas画布自适应窗口变化的方法:
// 将渲染尺寸设置为其显示的尺寸,返回画布像素尺寸是否等于其显示(css)尺寸的布尔值
resizeRendererToDisplaySize(renderer) {
// width 元素宽度 clientWidth 可见内容的宽度
const { width, height, clientWidth, clientHeight } = renderer.domElement;
const needResize = width !== clientWidth || height !== clientHeight;
// true 不等 false 相等
if (needResize) {
// 是重置渲染尺寸的方法 // setSize() 方法中的bool 参数很重要,会用于判断是否设置canvas 画布的css 尺寸。
// 设置了canvas画布的宽高
this.renderer.setSize(clientWidth, clientHeight, false);
}
return needResize;
}
鼠标选中模型的方法
// 选择机柜
// x,y为传入的鼠标点击的坐标 moveOrClick参数判断鼠标是点击还是覆盖物体
selectCabinet(x, y, moveOrClick) {
const { cabinets, renderer, camera, maps, curCabinet } = this;
const { width, height } = renderer.domElement;
// 鼠标的canvas坐标转裁剪坐标
pointer.set((x / width) * 2 - 1, -(y / height) * 2 + 1);
// 基于鼠标点和相机设置射线投射器 // 通过摄像机和鼠标位置更新射线
raycaster.setFromCamera(pointer, camera);
// 选择机柜 // 计算物体和射线的焦点
const intersect = raycaster.intersectObjects(cabinets)[0];
let intersectObj = intersect ? intersect.object : null;
// 若之前已有机柜被选择,且不等于当前所选择的机柜,取消已选机柜的高亮
if (curCabinet && curCabinet !== intersectObj) {
const material = curCabinet.material;
material.setValues({
map: maps.get("cabinet.jpg"),
});
}
/*
若当前所选对象不为空:
触发鼠标在机柜上移动的事件。
若当前所选对象不等于上一次所选对象:
更新curCabinet。
将模型高亮。
触发鼠标划入机柜事件。
否则:
置空curCabinet。
触发鼠标划出机柜事件。
*/
if (intersectObj) {
// console.log("intersectObj", intersectObj);
if (moveOrClick === "click") {
this.onClickCabinet(intersectObj);
}
this.onMouseMoveCabinet(x, y);
if (intersectObj !== curCabinet) {
this.curCabinet = intersectObj;
const material = intersectObj.material;
material.setValues({
map: maps.get("cabinet-hover.jpg"),
});
this.onMouseOverCabinet(intersectObj);
}
} else if (curCabinet) {
this.curCabinet = null;
this.onMouseOutCabinet();
}
}
// 鼠标点击时相机移动,即改变相机位置和中心点位置,以显示点击物体的最佳视角
// 点击某个机柜
onClickCabinet = (intersectObj) => {
if (intersectObj.cameraPosition) {
this.camera.position.set(
intersectObj.cameraPosition.x,
intersectObj.cameraPosition.y,
intersectObj.cameraPosition.z
);
this.controls.target.set(
intersectObj.controlsPosition.x,
intersectObj.controlsPosition.y,
intersectObj.controlsPosition.z
);
this.controls.update();
}
};
在场景中加入一个新的立方体,并在立方体上加入相机,后面物体沿轨迹移动时调用物体身上的相机,实现以物体的视角展示模型
// 创建一个立方体,并且创建了立方体的相机,放到坐标系中
creatBox() {
window.camera = this.camera;
window.controls = this.controls;
// 坐标辅助线
let axis = new THREE.AxesHelper(10);
this.scene.add(axis);
const geometry = new BoxGeometry(0.5, 0.5, 0.5); //几何对象
// 一种用于具有镜面高光的光泽表面的材质
const material = new MeshPhongMaterial({ color: 0x00ff00 }); // 材质
const cube = new Mesh(geometry, material);
cube.position.set(8, 0.25, -0.45);
this.scene.add(cube);
this.cube = cube;
const boxCamera = new THREE.PerspectiveCamera(75, 2, 0.1, 1000);
boxCamera.position.y = 1;
boxCamera.position.z = 0.5;
boxCamera.position.x = 1;
boxCamera.rotation.y = Math.PI * 0.5;
cube.add(boxCamera);
this.boxCamera = boxCamera;
}
在场景中加入路径,物体沿着此路径移动
// 在模型中建立路径
addRoutes = () => {
const curve = new THREE.SplineCurve([
new THREE.Vector2(8, -0.5),
new THREE.Vector2(-7, -0.5),
new THREE.Vector2(-7, -3),
new THREE.Vector2(8, -3),
new THREE.Vector2(8, -0.5),
]);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x888888 });
const splineObject = new THREE.Line(geometry, material);
splineObject.rotation.x = Math.PI * 0.5;
splineObject.position.y = 0.05;
this.scene.add(splineObject);
this.curveRef.current = curve;
};
// 立方体沿路径移动和停止的方法,调用时执行
cubeMovePosition = new THREE.Vector2();
stopMove = null;
// 盒子沿路径移动
moveBox(time) {
if (time) {
const curve = this.curveRef.current;
const tankTime = (time / 600) * 0.05;
this.cubeMovePosition = curve.getPointAt(tankTime % 1);
this.cube.position.set(
this.cubeMovePosition.x,
0,
this.cubeMovePosition.y
);
}
this.stopMove = requestAnimationFrame((time) => {
this.moveBox(time);
});
}
// 停止移动
stopMoveBox() {
cancelAnimationFrame(this.stopMove);
}
// 调用立方体上面的相机,展示第一视角
// 启用巡视相机位
xunshiCamera(isStart) {
if (isStart) {
this.camera = this.boxCamera;
} else {
this.camera = this.normalCamera;
}
}
前端页面调用
页面中展示模型的canvas、信息div,操作按钮
<div
className="App"
onMouseMove={this.mouseMove}
onClick={this.clickCabinet}
>
<canvas id="canvas" ref={(ele) => (canvas = ele)}></canvas>
<div id="plane" style={{ left, top, display }}>
<p>机柜名称:{name}</p>
<p>机柜温度:{temperature}°</p>
<p>
使用情况:{count}/{capacity}
</p>
</div>
<div className="xunshi" onClick={this.startPatrol.bind(this)}>
{patrolState}巡视
</div>
</div>
实例化模型加载方法
componentDidMount() {
if (!canvas) {
return;
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
room = new MachineRoom(canvas);
room.modelPath = "./models/";
room.loadGLTF("machineRoom.gltf");
room.animate();
room.onMouseOverCabinet = (cabinet) => {
//显示信息面板
this.setState({
planeDisplay: "block",
});
};
room.onMouseMoveCabinet = (left, top) => {
//移动信息面板
this.setState({
planePos: { left, top },
});
};
room.onMouseOutCabinet = () => {
//显示信息面板
this.setState({
planeDisplay: "none",
});
};
}
// 鼠标移动和点击时将鼠标的位置传过去,在图形组件中转裁剪坐标,以判断是否选中物体
mouseMove({ clientX, clientY }) {
room.selectCabinet(clientX, clientY);
}
clickCabinet({ clientX, clientY }) {
room.selectCabinet(clientX, clientY, "click");
}
// 点击巡视按钮时调用立方体上的相机,并使立方体沿轨迹移动
// 点击巡视
startPatrol() {
const nowState = this.state.patrolState;
this.setState({ patrolState: nowState === "开始" ? "暂停" : "开始" });
const open = nowState !== "暂停";
room.xunshiCamera(open);
if (open) {
room.moveBox();
} else {
room.stopMoveBox();
}
}