用Three.js帮助小学生更好的理解立体图形最近全网最火的应该就是一位17岁中专女生在全国阿里巴巴数学竞赛获得12名的
最近全网最火的应该就是一位17岁中专女生在全国阿里巴巴数学竞赛获得12名的荣誉,我感到非常自豪我们中国又出现了一位天才数学家。最近在学习Three.js
嘛,突然想到小学数据中有一篇是立体图形的章节,在三维空间中从各个角度来观察这个图形画出所看到的二维图形。 我想用Three.js
来实现这个功能来帮助这些没有三维空间感的学生来更好的理解。
结果展示
Three.js
的基础我就在这里不详细的说明了,大概看到这篇文章的人应该都会有了解过或详细的学习过Three.js
这个技术,所以基础的概念我就不说明了,我们直接开始干货吧。
创建项目
这里使用vite
来创建一个空项目。
npm init vit@latest three-app
选择Vanilla,创建完成
安装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
这里重点就来了,我们如何来监听鼠标在这个网格上浮动呢?这里官方文档没有任何事件。
这里我们创建一个平面模型,大小和网格的大小一样,然后铺在网格上,我们来监听平面模型的坐标就可以了。
创建一个平面模型,旋转-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);
可以看到平行光是在几乎中心点的位置的,那为什么物体的上方会被照亮呢?
这就是平行光和点光源的主要不同之处了。
平行关的position
可以理解成光源的方向的,并不是设置光的发射位置的。
将环境光删除,平行关位置改成如下:
directionalLight.position.set(1, 0, 0.5);
可以看到物体的上方并没有被照亮
所以平行光的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
监听document
的pointermove
事件
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);
}
});
监听document
的pointdown
事件
部分代码和上面差不多,这里只说一下不同的地方。
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