前端图形入门在WebGL中实现3D物体的选中和操作
最近一年一直在做3D图形相关的应用,入坑前端图形也了一年,想在这里跟大家分享一下自己学习到一些图形基础知识。今天跟大家分享一下在Web中实现3D物体的选中和移动旋转缩放操作。
我们先看一下实现的最终效果:
实现思路初始化3D场景 加载3D模型 处理mousemove事件和click事件 模型选中样式更新 菜单显示 菜单操作实现 实现说明初始化3D场景
ThreeJS中的一些基本概念:场景、相机、灯光、渲染器、3D物体 Scene(场景):一个容器,用于放置各种物体:物体、灯等等 Camera(相机):模拟人眼观察,需要放置到场景中 Light(灯光):模拟各类灯光类型 Renderer(渲染器):用于渲染所有信息 Mesh(3D网格物体):Mesh信息
首先我们需要在ThreeJS中初始化场景 // 创建一个场景 this._scene = new THREE.Scene(); this._scene.background = new THREE.Color( 0xbbbbbb ); // 创建渲染器 this._renderer = new THREE.WebGLRenderer({ canvas: this._canvas }); const { width, height } = this._domContainer.getBoundingClientRect(); this._renderer.setSize( width, height); this._renderer.setPixelRatio( window.devicePixelRatio ); // 相机 this._camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); this._camera.position.set(80, 60, 0); this._camera.lookAt(0,0,0);加载3D模型
ThreeJS中可以使用API生成3D物体,也可以通过Loader加载其他3D格式的模型,如常见的obj模型或者其他glTF格式的模型数据, 创建本地3D模型 const geometry = new THREE.BoxGeometry(10, 10, 10); const material = new THREE.MeshBasicMaterial( { color: 0xc20053 } ); material.side = THREE.DoubleSide; const mesh = new THREE.Mesh( geometry, material );加载第三方3D模型 // 使用GLTFLoader加载模型 const loader = new GLTFLoader().setPath("assets/"); loader.load("Soldier.glb", (gltf: GLTF) => { this._scene.add( gltf.scene ); });添加灯光
在真实的世界中,我们能看到物体是因为眼睛接收到物体表面反射光,形成的图像,在我们这里使用HemisphereLight模拟一个简单的户外光照效果 const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444 ); hemiLight.position.set( 0, 20, 0 ); this._scene.add(hemiLight);渲染
渲染器、场景、物体、相机和灯光都准备好后,我们就可以在浏览器中渲染出这个模型 private _animationFrame = () => { window.requestAnimationFrame(this._animationFrame); if (this.application.renderNextFrame) { this._renderer.render(this._scene, this._camera); } };//帧循环渲染 window.requestAnimationFrame(this._animationFrame);
提问:requestAnimationFrame属于微任务还是宏任务(字节面试题)? 处理mousemove事件和click事件
经过上面的操作,我们已经可以在canvas中显示3D场景和物体了
那我们要实现在不同角度对3D物体的选中,就需要在canvas中监听鼠标事件,根据鼠标的位置查找选中的物体信息
在3D中实现对物体的选中,就是在鼠标所在的位置发出一条摄像,和场景中的物体做相交,ThreeJS中可以直接使用Raycaster this._mousePoint.x = event.clientX / this._domContainer.clientWidth * 2 - 1; this._mousePoint.y = - ( event.clientY / this._domContainer.clientHeight ) * 2 + 1; this._raycaster.setFromCamera( this._mousePoint, this._camera ); // this._meshes包含了所有物体信息 const intersects = this._raycaster.intersectObjects( this._meshes );
这里setFromCamera就是在当前鼠标位置和相机设置设定射线发射器,raycaster.intersectObjects来做射线和指定的物体相交,返回检测到的物体列表
需要注意的是 ,ThreeJS中,一个物体可能由N级单元构成,而射线只能检测到最小单元,在返回的intersects中,我们需要去找到对应的物体,所以在添加物体到场景中时,我们需要建立一个单元 -> 物体的映射// gltf.scene是加载出来的一个机器人整体 gltf.scene.traverse( ( object: THREE.Object3D ) => { if (object instanceof THREE.Object3D) { // meshes存储所有物体单元信息 this._meshes.push(object); // 存储单元 -> 机器人的映射 this._objects.set(object.uuid, gltf.scene); } });
根据raycaster.intersectObjects返回的信息,我们可以拿到当前鼠标选中的物体 if (intersects.length > 0) { // 选中的最小单元 const mesh = intersects[0].object; // 找到对应的物体 const picked = this._objects.get(mesh.uuid); }模型选中样式更新
这个时候我们已经拿到了选中的物体,那么就是要给个反馈呗,这里我们做个简单的样式更改,把选中的物体材质透明度改为原始值的一半,那么在下一帧刷新后,我们就可以看到一个透明的效果 mesh.traverse((child: THREE.Object3D) => { if (child instanceof THREE.Mesh) { const material = child.material as THREE.Material; // 存储之前的透明度,鼠标移出后记得把材质修改回来 this._meshOpacity.set(child.uuid, material.opacity); // 设置透明度 material.opacity = material.opacity / 3; material.transparent = true; material.needsUpdate = true; } });菜单显示
经过以上步骤后,我们就了解了在ThreeJS中如何去选中物体,那一般我们的业务需要选中后,有一些操作,就比如游戏中的走路,这里我们就来看看ThreeJS中最常见的三种变换:移动、旋转、缩放
当我们在3D中选中物体后,告诉我们的菜单,这个时候你该出场了,我项目中用了一个简单的发布/订阅者模式,当物体选中后,会向系统发出物体选中事件 const selections = objects.map((mesh) => new J3DSelection(mesh)); this.application.addSelections(selections); this.application.emitSelectionAdd({ selections, event });
而在菜单功能那边,会监听选中事件 this._application.listenSelectionAdd(this._onSelectionAdded);
从而在收到物体选中事件后,把自己show出来 private _updateMenu = (event: MouseEvent) => { const { clientX, clientY } = event; this.setState({ xPosition: clientX + 10, yPosition: clientY + 10, visible: this._application.selections.length > 0 }); };
效果如下:
菜单操作实现
菜单展示出来后,点击对应的按钮,根据选中的物体信息执行相应的操作即可 移动private _move = (direction: number) => { const delta: number = direction > 0 ? -5 : 5; this._application.selections.forEach((item) => { item.mesh.position.z = item.mesh.position.z + delta; }); };旋转private _rotate = (direction: number) => { const angle = direction > 0 ? -Math.PI / 4 : Math.PI / 4; this._application.selections.forEach((item) => { item.mesh.rotateY(angle); }); };缩放private _scale = (direction: number) => { const delta: number = direction > 0 ? 2 : 0.5; this._application.selections.forEach((item) => { const {x, y, z} = item.mesh.scale; item.mesh.scale.set(x * delta, y * delta, z * delta); }); };总结
第一次写技术博客,主要是用一个直观的项目展示基于ThreeJS的3D基础知识,后续还会持续分享一些自己的前端和图形相关的知识,如有错误欢迎留言指正。 项目源码
Github地址:GitHub - jaliy/j3d-pick: 在WebGL中实现3D物体的选中和操作(ThreeJS)
官方文档ThreeJS:基于WebGL封装的一个3D JS库,关于ThreeJS,可以看ThreeJS官网Three.js – JavaScript 3D Library
WebGL:基于OpenGL ES规范的Web端3D图形接口规范,更多知识可以看WebGL官网WebGL Overview - The Khronos Group Inc
OpenGL:用于访问图形硬件(GPU)的接口标准,OpenGL官网OpenGL Overview - The Khronos Group Inc