ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

three.js一些细节功能的实现

2026/9/24 7:04:30 拓冰建站 浏览量
three.js一些细节功能的实现

three.js一些细节功能的实现

  • 1.three.js如何实现一键复原的功能
    • 说明
    • 实现
    • 完整demo
  • 2.three.js中,如何用一个新的变量复制另一个模型的坐标?是直接等于号赋值吗?
  • 3. 如何在three.js中随心所欲的添加或删除一个模型?(就是说,添加后我们才能选中他,删除后我们就不能选中他)
  • 4.鼠标左键移动three.js中某个独立的个体

1.three.js如何实现一键复原的功能

说明

一键复原,即恢复模型位置,旋转角度,缩放

实现

  • 第一步,将原来的网格先复制一遍
// 假设 cube是你要复制的网格const newMesh = cube.clone();
  • 第二步,一键还原
cube.position.copy(newMesh.position);
cube.rotation.copy(newMesh.rotation);
cube.scale.copy(newMesh.scale);

完整demo

import { useState, useEffect, Component } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import * as THREE from 'three';
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 引入指针锁定控制器扩展库PointerLockControls.js
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'
// 导入动画库
import gsap from "gsap";
//导入lil.gui
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";// function App() {
//   useEffect(() => {//     const scene = new THREE.Scene();
//     const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
//     //创建渲染器
//     const renderer = new THREE.WebGLRenderer();
//     renderer.setSize(window.innerWidth, window.innerHeight);
//     document.body.appendChild(renderer.domElement);
//     //创建几何体
//     const geometry = new THREE.BoxGeometry(1, 1, 1);
//     //创建材质
//     const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
//     //创建网格=几何体+材质
//     const cube = new THREE.Mesh(geometry, material);
//     //将网格添加到场景
//     scene.add(cube);//     //正对着我们的是z轴
//     camera.position.z = 5;
//     //默认看向原点
//     camera.lookAt(0, 0, 0);//     //创建轨道控制器
//     //const controls = new OrbitControls(camera, renderer.domElement);//     function animate() {
//       //播放下一帧,继续调用animate函数
//       requestAnimationFrame(animate);
//       //旋转
//       cube.rotation.x += 0.01;
//       cube.rotation.y += 0.01;
//       //渲染
//       renderer.render(scene, camera);
//     }
//     //调用函数
//     animate();
//   }, [])//   return (
//     <>
//       <div className='App'></div>//     </>
//   )
// }class App extends Component {render() {return <div></div>}componentDidMount() {// const scene = new THREE.Scene();// const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);// //创建渲染器// const renderer = new THREE.WebGLRenderer();// renderer.setSize(window.innerWidth, window.innerHeight);// document.body.appendChild(renderer.domElement);// //创建几何体// const geometry = new THREE.BoxGeometry(1, 1, 1);// //创建材质// const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });// //创建网格=几何体+材质// const cube = new THREE.Mesh(geometry, material);// //将网格添加到场景// scene.add(cube);// //正对着我们的是z轴// camera.position.z = 5;// //默认看向原点// camera.lookAt(0, 0, 0);// //创建轨道控制器// //const controls = new OrbitControls(camera, renderer.domElement);// function animate() {//   //播放下一帧,继续调用animate函数//   requestAnimationFrame(animate);//   //旋转//   cube.rotation.x += 0.01;//   cube.rotation.y += 0.01;//   //渲染//   renderer.render(scene, camera);// }// //调用函数// animate();// console.log(THREE);// 设置场景、相机const scene = new THREE.Scene();// 设置场景颜色为蓝色scene.background = new THREE.Color(0x0000ff);//透视投影(透视投影会使得远离中心的物体看起来扭曲或拉伸,这是一个不可避免的数学事实)//const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);//解决办法是使用正交投影:const camera = new THREE.OrthographicCamera(window.innerWidth / -2,window.innerWidth / 2,window.innerHeight / 2,window.innerHeight / -2,1, 1000);//设置相机的缩放比例(正交投影会使得物体变得非常小,所以你需要设置一下缩放比例,比如放大一百倍)camera.zoom = 100;// 更新相机的投影矩阵(这句话好像不用加)camera.updateProjectionMatrix();// 设置相机位置//camera.position.set(0, 0, 10);//camera.position.set(0, 15, 70);//camera.position.z = -6;camera.position.y = 6;camera.position.x = -6;scene.add(camera);//渲染器const renderer = new THREE.WebGLRenderer();renderer.setSize(window.innerWidth, window.innerHeight);document.body.appendChild(renderer.domElement);// 监听窗口大小变化事件window.addEventListener('resize', function () {var width = window.innerWidth;var height = window.innerHeight;//1. 更新渲染器大小renderer.setSize(width, height);//2.更新相机的宽高比camera.aspect = width / height;//   设置渲染器的像素比(这个好像可有可无,不过加上确实更好一点,拉伸更自然了)renderer.setPixelRatio(window.devicePixelRatio);// 3.更新相机的投影矩阵camera.updateProjectionMatrix();});/********************************************///1.创建纹理加载器let textureLoader = new THREE.TextureLoader();//2.加载纹理//let texture = textureLoader.load("./picture/52ec610d2c388ddc9c07f02027231f1.png",// let texture = textureLoader.load(//   '/root/myspace/three/react-three-app/vite-project/src/picture/52ec610d2c388ddc9c07f02027231f1.png',let texture = textureLoader.load('./src/picture/2.png',// onLoad回调function (texture) {// in this example we create the material when the texture is loadedconsole.log('Texture loaded successfully.');},// 目前暂不支持 onProgress 的回调undefined,// onError回调function (err) {console.log('An error happened while loading the texture.');console.log('Error details:{{{[', err, ']}}}');});/********************************************/// 创建一个立方体const geometry = new THREE.BoxGeometry();//材质const material = new THREE.MeshBasicMaterial({color: 0x00ff00,map: texture,});//设置材质为线框模式material.wireframe = false;const cube = new THREE.Mesh(geometry, material);//cube.position.x = 2;//等价于cube.position.set(0, 0, 0);// 假设 mesh0 是你要复制的网格const newMesh = cube.clone();scene.add(cube);/**************/// 创建轨道控制器(此时你可以使用右键让模型动起来)const controls = new OrbitControls(camera, renderer.domElement);// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。controls.mouseButtons = {LEFT: THREE.MOUSE.ROTATE, // 左键旋转MIDDLE: THREE.MOUSE.PAN, // 中键平移RIGHT: THREE.MOUSE.DOLLY // 右键缩放};controls.enableDamping = true;//设置阻尼的系数controls.dampingFactor = 0.05;//让其主动的旋转//controls.autoRotate = true;// 添加坐标轴辅助器(相当于一个线段,看成一个物件)const axesHelper = new THREE.AxesHelper(5);//线段的长度为5scene.add(axesHelper);//添加到场景中// 设置时钟const clock = new THREE.Clock();/**************/// 鼠标交互let isDragging = false;let previousMousePosition = {x: 0,y: 0};function onDocumentMouseDown(event) {isDragging = true;}function onDocumentMouseMove(event) {if (isDragging) {var deltaMove = {x: event.clientX - previousMousePosition.x,y: event.clientY - previousMousePosition.y};const deltaRotationQuaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(toRadians(deltaMove.y * 1),toRadians(deltaMove.x * 1),0,'XYZ'));cube.quaternion.multiplyQuaternions(deltaRotationQuaternion, cube.quaternion);}previousMousePosition = {x: event.clientX,y: event.clientY};}function onDocumentMouseUp(event) {isDragging = false;}// 将鼠标事件监听器添加到渲染器的DOM元素// renderer.domElement.addEventListener('mousedown', onDocumentMouseDown, false);// renderer.domElement.addEventListener('mousemove', onDocumentMouseMove, false);// renderer.domElement.addEventListener('mouseup', onDocumentMouseUp, false);// 动画循环渲染function animate() {// 如果没有鼠标交互,立方体会自动旋转if (!isDragging) {// cube.rotation.x += 0.01;// cube.rotation.y += 0.01;}controls.update();renderer.render(scene, camera);requestAnimationFrame(animate);}animate(); // 开始动画循环// 辅助函数:将角度转换为弧度function toRadians(angle) {return angle * (Math.PI / 180);}//lil-guilet eventObj = {Fullscreen: function () {renderer.domElement.requestFullscreen();gui.show();},ExitFullscreen: function () {document.exitFullscreen();gui.show();}};//创建GUIconst gui = new GUI();//改变交互界面style属性gui.domElement.style.right = '0px';gui.domElement.style.width = '300px';gui.close();// 创建一个折叠的文件夹const folder = gui.addFolder('全屏设置');// 添加按钮到折叠文件夹folder.add(eventObj, 'Fullscreen').name('全屏');folder.add(eventObj, 'ExitFullscreen').name('退出全屏');// 关闭折叠面板folder.close();const folder1 = gui.addFolder('立方体位置设置');folder1.add(cube.position, "x", -5, 5).name("立方体x轴位置").onChange((val) => {console.log("cube's x position:", val);});folder1.add(cube.position, "y", -5, 5).name("立方体y轴位置");folder1.add(cube.position, "z", -5, 5).name("立方体z轴位置");//.min(-10).max(10).step(1)folder1.close();const folder2 = gui.addFolder('线框模式设置');folder2.add(material, "wireframe").name("线框模式");folder2.close();const folder3 = gui.addFolder('颜色设置');let colorParams = {cubeColor: "#ff0000",};folder3.addColor(colorParams, "cubeColor").name("颜色变化").onChange((val) => {cube.material.color.set(val);});folder3.close();window.addEventListener('mouseup', function () {console.log('鼠标按下');cube.position.x += 1;cube.rotateY(Math.PI / 3);cube.scale.x = 2; // 在 x 轴方向上缩放为原来的两倍cube.scale.y = 0.5; // 在 y 轴方向上缩放为原来的一半cube.scale.z = 1.5; // 在 z 轴方向上缩放为原来的1.5倍renderer.render(scene, camera);});// 添加HTML按钮const button = document.getElementById('myButton');button.addEventListener('click', function () {console.log('Button Clicked!');// // 将新网格对象添加到场景中// scene.add(newMesh);// // 从场景中删除要替换的网格对象// scene.remove(cube);cube.position.copy(newMesh.position);cube.rotation.copy(newMesh.rotation);cube.scale.copy(newMesh.scale);// 渲染场景renderer.render(scene, camera);});}
}export default App

2.three.js中,如何用一个新的变量复制另一个模型的坐标?是直接等于号赋值吗?

答:不是,需要先建立一个THREE.Vector3对象,然后再调用copy函数!

var startPoint = new THREE.Vector3();
startPoint.copy(axesHelper1.position);
console.log("axesHelper1:", axesHelper1.position);
console.log("startPoint:", startPoint);

3. 如何在three.js中随心所欲的添加或删除一个模型?(就是说,添加后我们才能选中他,删除后我们就不能选中他)

  • 天才第一步,在代码的第一行添加射线选中的元素有哪些,我们用一个空数组表示,你最好写在第一行代码,不然你后面用的时候,你根本找不到到该数组,就会报错,说系统也找不到
var objects = [];
  • 天才第二步
//创建射线
const raycaster = new THREE.Raycaster();
//创建鼠标向量
const mouse = new THREE.Vector2();
//存放射线可射的范围
let intersects;/********************************
* 上面两行放全局变量
* 下面两行放在mousedown鼠标事件中
*********************************///通过摄像机和鼠标为位置,更新射线
raycaster.setFromCamera(mouse, camera);
//计算物体和射线的焦点
intersects = raycaster.intersectObjects(objects);      
  • 天才第n步,在你需要的位置将模型显示并可以选中
scene.add(axesHelper1);//将箭头加入到场景中//将箭头加入到我们的选中范围
objects.push(axesHelper1);
// 射线检测
intersects = raycaster.intersectObjects(objects);
  • 天才第n+1步,在你需要的位置将模型隐藏并不可以选中
scene.remove(ZTubeMesh);//将箭头"隐藏"到场景中
console.log('删掉了,兄弟们!');//调试代码
console.log('objects:', objects);//调试代码objects.pop(ZTubeMesh);//将管道Z取消在我们的选中范围
console.log('objects:', objects);//调试代码
// 射线检测
intersects = raycaster.intersectObjects(objects);

注意,如果你不写下面这两行,你就会发现该模型在场景中消失了,但是还能点到(这就是误触!)

objects.pop(ZTubeMesh);//将管道Z取消在我们的选中范围
// 射线检测
intersects = raycaster.intersectObjects(objects);

4.鼠标左键移动three.js中某个独立的个体

下面这种方法算是初步实现,具体细节还需进行优化

// 初始化鼠标坐标
var mouseX = 0;
var mouseY = 0;
var m_firstMouse = true;
window.addEventListener('mousemove', function (event) {console.log('鼠标移动');console.log('objects:', objects);/***************************** //2024-01-09*/if (hasTeethSelected && mouseDown) {//有牙齿被选中了,且鼠标已经按下了if (event.buttons === 1)//左键旋转{//如果是第一次进来则记录一下当前位置if (m_firstMouse) {// 更新鼠标坐标mouseX = event.clientX;mouseY = event.clientY;m_firstMouse = false;}// 计算鼠标坐标差值var deltaX = event.clientX - mouseX;var deltaY = event.clientY - mouseY;console.log('x:', deltaX, 'y:', deltaY);/**** 得到模型的中心 center*/const boundingBox = new THREE.Box3().setFromObject(selectedObject);const center = new THREE.Vector3();boundingBox.getCenter(center);// 更新鼠标坐标mouseX = event.clientX;mouseY = event.clientY;// 创建矩阵进行旋转var matrix = new THREE.Matrix4();// 根据鼠标坐标差值进行旋转matrix.makeRotationY(deltaX * 0.005); // Y轴旋转//左右拖动matrix.multiply(new THREE.Matrix4().makeRotationX(deltaY * 0.005)); // X轴旋转//上下拖动selectedObject.position.sub(center);//减去自身中心坐标,现在就回到了原点// 应用变换矩阵到立方体selectedObject.applyMatrix4(matrix);selectedObject.position.add(center);//加上自身中心坐标,现在就回到了原来的点}}
});

这里我要强掉一下,只要你是移动场景中的"某一个物体",你就要像下面这个逻辑一样,先减去自身中心点,再添加自身中心点

  • 逻辑
  1. 减去中心点
  2. 矩阵变换
  3. 加上中心点
  • demo
selectedObject.position.sub(center);//减去自身中心坐标,现在就回到了原点
// 应用变换矩阵到立方体
selectedObject.applyMatrix4(matrix);
selectedObject.position.add(center);//加上自身中心坐标,现在就回到了原来的点