likes
comments
collection
share

用Three.js帮助小学生更好的理解立体图形最近全网最火的应该就是一位17岁中专女生在全国阿里巴巴数学竞赛获得12名的

作者站长头像
站长
· 阅读数 13

最近全网最火的应该就是一位17岁中专女生在全国阿里巴巴数学竞赛获得12名的荣誉,我感到非常自豪我们中国又出现了一位天才数学家。最近在学习Three.js嘛,突然想到小学数据中有一篇是立体图形的章节,在三维空间中从各个角度来观察这个图形画出所看到的二维图形。 我想用Three.js来实现这个功能来帮助这些没有三维空间感的学生来更好的理解。

结果展示

用Three.js帮助小学生更好的理解立体图形最近全网最火的应该就是一位17岁中专女生在全国阿里巴巴数学竞赛获得12名的

Three.js的基础我就在这里不详细的说明了,大概看到这篇文章的人应该都会有了解过或详细的学习过Three.js这个技术,所以基础的概念我就不说明了,我们直接开始干货吧。

创建项目

这里使用vite来创建一个空项目。

npm init vit@latest three-app

选择Vanilla,创建完成

用Three.js帮助小学生更好的理解立体图形最近全网最火的应该就是一位17岁中专女生在全国阿里巴巴数学竞赛获得12名的

安装Three.js

npm install three

创建一个三维场景

const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);

创建一个相机

const camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000);
camera.position.set(500, 800, 1300);
camera.lookAt(0, 0, 0); // 观察点

创建一个正方体模型使用基础材质,这个是用来鼠标悬浮跟随鼠标移动的

const rollOverGeo = new THREE.BoxGeometry(50, 50, 50);
const rollOverMaterial = new THREE.MeshBasicMaterial({ color: 0x999999, opacity: 0.5, transparent: true });
const rollOverMesh = new THREE.Mesh(rollOverGeo, rollOverMaterial);
scene.add(rollOverMesh);

创建正方体箱子模型,它是一个Lambert网格材质会受到光源的影响,设置了一个木制图片的贴图,这样看起来是个木箱子。

const map = new THREE.TextureLoader().load('1.jpg');
map.colorSpace = THREE.SRGBColorSpace;
const cubeGeo = new THREE.BoxGeometry(50, 50, 50);
const cubeMaterial = new THREE.MeshLambertMaterial({
  map: map,
})

创建一个网格地面

var gridHelper = new THREE.GridHelper(1000, 20);
scene.add(gridHelper);

它的第一个参数是坐标的尺寸,第二个是一行网格的个数。

因为创建的正方体模型是50的,每行20个,所以它的坐标尺寸设置1000

用Three.js帮助小学生更好的理解立体图形最近全网最火的应该就是一位17岁中专女生在全国阿里巴巴数学竞赛获得12名的

这里重点就来了,我们如何来监听鼠标在这个网格上浮动呢?这里官方文档没有任何事件。

这里我们创建一个平面模型,大小和网格的大小一样,然后铺在网格上,我们来监听平面模型的坐标就可以了。

创建一个平面模型,旋转-90度就成功铺在网格上了,然后将模型隐藏。

const geometry = new THREE.PlaneGeometry(1000, 1000);
geometry.rotateX(- Math.PI / 2);
const plane = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ visible: false }));
scene.add(plane);

创建一个模型集合数组,用来存储场景中的所有模型。

const objects = [];
// 将平面模型保存到模型集合中
objects.push(plane);

创建光源

创建一个环境光,目的是为了让物体的各个面都能看到

const ambientLight = new THREE.AmbientLight(0x606060, 3.0);
scene.add(ambientLight);

创建一个平行光

const directionalLight = new THREE.DirectionalLight(0xffffff, 8);
directionalLight.position.set(1, 0.75, 0.5);
scene.add(directionalLight);

用Three.js帮助小学生更好的理解立体图形最近全网最火的应该就是一位17岁中专女生在全国阿里巴巴数学竞赛获得12名的

可以看到平行光是在几乎中心点的位置的,那为什么物体的上方会被照亮呢?

这就是平行光和点光源的主要不同之处了。

平行关的position可以理解成光源的方向的,并不是设置光的发射位置的。

将环境光删除,平行关位置改成如下:

directionalLight.position.set(1, 0, 0.5);

用Three.js帮助小学生更好的理解立体图形最近全网最火的应该就是一位17岁中专女生在全国阿里巴巴数学竞赛获得12名的

可以看到物体的上方并没有被照亮

所以平行光的position可以理解成物体的正面、上面、侧面哪个面需要被照亮。

创建一个WebGL渲染器

const renderer = new THREE.WebGLRenderer({
  antialias: true, //抗锯齿
});
renderer.setPixelRatio(window.devicePixelRatio); // 设置像素比
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);

renderer.render(scene, camera);

创建一个相机轨道空间

const controls = new OrbitControls(camera, renderer.domElement);
controls.enabled = false;
controls.update();
controls.addEventListener('change', () => {
  renderer.render(scene, camera);
})

接下来就要实现交互的效果了,这里我们使用Raycaster(光线投射)Vector2(二维向量)

const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

声明几个按键变量,用来获取是否按住按键

var isShiftDown = false; // 是否按了Shift
var isSpaceDown = false; // 是否按了空格
// 这个实现上下左右按键来切换不同的视角的
var isMoving = false; // 场景是否在动画移动
var targetPosition = new THREE.Vector3(0, 0, 0); // 存储上下左右的视角位置
var timer = null; // 存放requestAnimationFrame

监听documentpointermove事件

document.addEventListener('pointermove', (event) => {
 // 将像素坐标转换设备坐标
  pointer.set((event.clientX / window.innerWidth) * 2 - 1, - (event.clientY / window.innerHeight) * 2 + 1);
  raycaster.setFromCamera(pointer, camera);
  // 监听场景中所有的模型
  const intersects = raycaster.intersectObjects(objects, false);

  if (intersects.length > 0) {
    const intersect = intersects[0];
    // 将透明真放题跟随鼠标移动
    rollOverMesh.position.copy(intersect.point).add((intersect as any).face.normal);
   // 坐标设置成50的倍数,这里的增加25是因为距离原点为25 
   rollOverMesh.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25);

    renderer.render(scene, camera);
  }
});

监听documentpointdown事件

部分代码和上面差不多,这里只说一下不同的地方。

document.addEventListener('pointerdown', (event) => {
  pointer.set((event.clientX / window.innerWidth) * 2 - 1, - (event.clientY / window.innerHeight) * 2 + 1);

  raycaster.setFromCamera(pointer, camera);
  const intersects = raycaster.intersectObjects(objects, false);
  if (intersects.length > 0) {
    const intersect = intersects[0];
    
    // 如果是按住shift按键的话则删除选中的模型
    if (isShiftDown && !isSpaceDown) {
      if (intersect.object !== plane) {
        scene.remove(intersect.object); 
        objects.splice(objects.indexOf(intersect.object), 1);
      }
    } else if (!isSpaceDown) {
      // 没有按住空格表示不是移动,则创建模型箱子
      const voxel = new THREE.Mesh(cubeGeo, cubeMaterial);
      voxel.position.copy(intersect.point).add((intersect as any).face.normal);
      voxel.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25);
      scene.add(voxel);
      // 添加到场景集合中
      objects.push(voxel);
    }
    renderer.render(scene, camera);

  }
})

监听键盘按下事件

document.addEventListener('keydown', (event) => {
  isMoving = false;
  cancelAnimationFrame(timer!);
  switch (event.keyCode) {
    case 16: { // shift按键
      isShiftDown = true;
      rollOverMesh.visible = false;
    }; break;
    case 32: { // 空格按键
      isSpaceDown = true;
      rollOverMesh.visible = false; // 透明模型隐藏
      controls.enabled = true; // 开启相机滚动控件
    } break;
  }
});

监听键盘抬起事件

document.addEventListener('keyup', (event) => {
  switch (event.keyCode) {
    case 16: {
      isShiftDown = false
      rollOverMesh.visible = true;
    }; break;
    case 32: {
      isSpaceDown = false;
      rollOverMesh.visible = true;
      controls.enabled = false;
    } break;
    case 37: { // 按左键
      isMoving = true;
      controls.enabled = false;
      targetPosition.set(-1606, 0.42, -13.6) // 设置左视角
      animate();
    } break;
    case 38: { // 按上键
      isMoving = true;
      controls.enabled = false;
      targetPosition.set(-0.006, 1605, 28.4) // 设置上视角
      animate();
    } break;
    case 39: { // 按右键
      isMoving = true;
      controls.enabled = false;
      targetPosition.set(1604, -1.83, 66.1) // 设置右视角
      animate();
    } break;
    case 40: { // 按下键
      isMoving = true;
      controls.enabled = false;
      targetPosition.set(-10.4, -1.59, 1606) // 设置正面视角
      animate();
    } break;
    case 82: { // 按R键
      // 删除处了地板平面模型意外的所有模型
      for (let i = 1; i < objects.length; i++) {
        scene.remove(objects[i])
      }
      objects = [objects[0]]
      isMoving = true;
      targetPosition.set(500, 800, 1300) // 设置初始化相机视角
      animate();
      renderer.render(scene, camera);
    } break;
    case 86: { // 按V键
      isMoving = true;
      targetPosition.set(500, 800, 1300) // 设置初始化相机视角
      animate();
      renderer.render(scene, camera);
    } break;
  }
});

相机位置平滑过度动画

function animate() {
  timer =requestAnimationFrame(animate);
  if (isMoving) {
    camera.position.lerp(targetPosition, 0.1);
    if (camera.position.distanceTo(targetPosition) < 0.01) {
      // 过度完成,将相机位置改变,并停止requestAnimationFrame函数
      camera.position.copy(targetPosition);
      isMoving = false;
      cancelAnimationFrame(timer);
    }
    controls.update(); // 更新OrbitControls
  }
}

整个项目的所有功能到这里就结束了,希望能给学习Three.js的兄弟们得到帮助,希望大家都能在Three.js的道路上越走越远,也希望前端的未来会越来越好。

项目源代码地址 gitee.com/yi_wo_zuo/t…

转载自:https://juejin.cn/post/7382421853766516762
评论
请登录