• 用Three.js打造酷炫3D个人网站(含源码)


    引言

    个人网站是程序员的第二张简历。如果你有酷炫的个人网页,面试官对你的好感度会蹭蹭蹭往上涨。

    在疫情隔离期间,我用Three.jsAmmo.js制作了一个可交互的3D个人网页。

    在线预览地址: www.ryan-floyd.com/

    Three.js的3D世界

    当我在Google Experiments闲逛时,我发现非常多的作品都是用three.js写的。

    three.js是一个让3D网页应用开发变得简单的库。它诞生于2010年,作者是Ricardo Cabello (Mr.doob),,在github上有超过1300多的贡献者,在所有仓库中star数排行第38。

    当看到Google Experiments上那些酷炫的3D效果后,我决定开始学习three.js

    Three.js的工作机制

    (3D应用的组件结构,图片来自discoverthreejs.com)

    Three.js使得在浏览器展示3D图像变得容易,它的底层是基于WebGL,它使浏览器能借助系统显卡在canvas中绘制3D画面。

    WebGL自身只能绘制点(points)、线(lines)和三角形(triangles),而Three.jsWebGL进行了封装,使我们能够非常方便地创建 物体(objects), 纹理(textures), 进行 3D 计算等操作。

    使用Three.js,我们将所有物体(objects)添加到场景(scene)中,然后将需要渲染的数据传递给渲染器(renderer),渲染器负责将场景在 画布上绘制出来。

    (Three.js 应用架构,图片来自threejsfundamentals.org

    对于一个 Three.js 应用,最核心的就是场景(scene object),上面是一张场景图(scene graph)。

    在一个3D引擎中,场景图是一个层级结构的树状图,树中的每一个节点代表空间中的一部分。这种结构有点像DOM树,但Three.js的场景(scene)更像虚拟DOM,它只更新和渲染场景中有变化的部分。而这一切的基础,是 Three.js 的 WebGLRenderer 类,它把我们的代码转换成 GPU 中的数据,浏览器再将这些数据渲染出来。

    场景中的物体,也叫Mesh。在 Three.js 的世界中,Mesh 是由 几何体Geometry(决定物体形状) + 材质Material(决定物体外观)构成。

    场景中的另一个重要元素,就是相机camera,它决定了场景中 哪些部分以怎样的视觉效果 被绘制在canvas画布上。

    然后是动画,为了实现动画,渲染器(renderer)通常使用requestAnimationFrame()方法,以每秒60次的频率将场景更新绘制在canvas上。requestAnimationFrame()方法的原理和使用可以参考MDN

    下面这个例子来自Three.js官方文档,创建了一个旋转的 3D 立方体。

    1. <html>
    2. <head>
    3. <title>My first three.js app</title>
    4. <style>
    5. body {
    6. margin: 0;
    7. }
    8. canvas {
    9. display: block;
    10. }
    11. </style>
    12. </head>
    13. <body>
    14. <script src="https://unpkg.com/three@0.119.0/build/three.js"></script>
    15. <script>
    16. //创建场景和相机
    17. var scene = new THREE.Scene();
    18. var camera = new THREE.PerspectiveCamera(
    19. 75,
    20. window.innerWidth / window.innerHeight,
    21. 0.1,
    22. 1000
    23. );
    24. //创建渲染器,设置尺寸为窗口尺寸,并将渲染后的元素添加到body
    25. var renderer = new THREE.WebGLRenderer();
    26. renderer.setSize(window.innerWidth, window.innerHeight);
    27. document.body.appendChild(renderer.domElement);
    28. //创建一个Mesh(绿色的3D立方体),并添加到场景中
    29. var geometry = new THREE.BoxGeometry();
    30. var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
    31. var cube = new THREE.Mesh(geometry, material);
    32. scene.add(cube);
    33. //设置照相机的位置
    34. camera.position.z = 5;
    35. //浏览器每次渲染的时候更新立方体的旋转角度
    36. var animate = function () {
    37. requestAnimationFrame(animate);
    38. cube.rotation.x += 0.01;
    39. cube.rotation.y += 0.01;
    40. renderer.render(scene, camera);
    41. };
    42. animate();
    43. </script>
    44. </body>
    45. </html>
    46. 复制代码

    效果如下:

    转存失败重新上传取消

    Ammo.js物理引擎

    Ammo.js 是将 Bullet物理引擎 直接移植到JavaScript的产物(Bullet Physics是一个开源的物理模拟引擎)。我对物理引擎底层的工作原理理解得不太深入,简而言之,物理引擎根据你传入的参数(比如重力),创建循环,在每次循环中更新状态,从而模拟出自然的物理运动和碰撞等效果。

    循环中的物体(通常也是刚体),具有力、质量、惯性、摩擦力等物理属性。每次循环,通过不断检查所有物体的位置、状态和运动来检测碰撞和交互。如果发生交互,对象位置将根据经过的时间和对象的物理属性进行更新。下面是我代码中的一个片段,显示了如何创建物理引擎循环以及如何将它添加到Three.js的sphere球体中。

    1. //引入库
    2. import * as THREE from "three";
    3. import * as Ammo from "./builds/ammo";
    4. import {scene} from "./resources/world";
    5. //初始化 Ammo.js 物理引擎
    6. Ammo().then((Ammo) => {
    7. // 创建物理世界
    8. function createPhysicsWorld() {
    9. //完全碰撞检测算法
    10. let collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
    11. // 重叠对/碰撞的调度计算
    12. let dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
    13. // 所有可能碰撞对的宽相位碰撞检测列表
    14. let overlappingPairCache = new Ammo.btDbvtBroadphase();
    15. // 使物体正确地交互,考虑重力、力、碰撞等
    16. let constraintSolver = new Ammo.btSequentialImpulseConstraintSolver();
    17. // 根据这些参数创建物理世界。 参考bullet physics文档
    18. let physicsWorld = new Ammo.btDiscreteDynamicsWorld(
    19. dispatcher,
    20. overlappingPairCache,
    21. constraintSolver,
    22. collisionConfiguration
    23. );
    24. // 添加重力
    25. physicsWorld.setGravity(new Ammo.btVector3(0, -9.8, 0));
    26. }
    27. //创建球体
    28. function createBall(){
    29. //球体参数
    30. let pos = {x: 0, y: 0, z: 0};
    31. let radius = 2;
    32. let quat = {x: 0, y: 0, z: 0, w: 1};
    33. let mass = 3;
    34. //three.js相关代码
    35. //创建球体并添加到场景中
    36. let ball = new THREE.Mesh(new THREE.SphereBufferGeometry(radius), new THREE.MeshStandardMaterial({color: 0xffffff}));
    37. ball.position.set(pos.x, pos.y, pos.z);
    38. scene.add(ball);
    39. //Ammo.js相关代码
    40. //设置位置和旋转
    41. let transform = new Ammo.btTransform();
    42. transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
    43. transform.setRotation(
    44. new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w)
    45. );
    46. //设置物体运动
    47. let motionState = new Ammo.btDefaultMotionState(transform);
    48. //设置碰撞边界框
    49. let collisionShape = new Ammo.btSphereShape(radius);
    50. collisionShape.setMargin(0.05);
    51. //设置惯性
    52. let localInertia = new Ammo.btVector3(0, 0, 0);
    53. collisionShape.calculateLocalInertia(mass, localInertia);
    54. //生成创建刚体(物体)的结构信息
    55. let rigidBodyStructure = new Ammo.btRigidBodyConstructionInfo(
    56. mass,
    57. motionState,
    58. collisionShape,
    59. localInertia
    60. );
    61. //基于上面的结构信息创建物体
    62. let body = new Ammo.btRigidBody(rigidBodyStructure);
    63. //当物体运动时,为其添加摩擦力
    64. body.setFriction(10);
    65. body.setRollingFriction(10);
    66. // 将物体添加到物理世界,这样Ammo.js引擎才能不断更新物体的状态
    67. physicsWorld.addRigidBody(body);
    68. }
    69. createPhysicsWorld();
    70. createBall()
    71. }
    72. 复制代码

    运动和交互

    在Ammo.js模拟的物理世界中,交互是基于属性和力计算的。

    每个对象有一个边界框(bounding box)属性,物理引擎会根据这个边界框来检测物体的位置。

    在每个动画循环中检查所有对象的边界框后,如果任意两个对象的边界框位于同一位置,引擎将记录为“碰撞”,并相应地更新对象。 对于刚体来说,这意味着阻止两个物体处于同一位置。

    下面是我的代码片段,显示了渲染循环和世界物理是如何更新的。

    1. //渲染框架
    2. function renderFrame() {
    3. //记录上一次渲染的时间
    4. let deltaTime = clock.getDelta();
    5. //基于用户输入,计算球会受到的力和产生的速度
    6. moveBall();
    7. //根据时间更新物理世界状态
    8. updatePhysics(deltaTime);
    9. //进行渲染
    10. renderer.render(scene, camera);
    11. // 循环
    12. requestAnimationFrame(renderFrame);
    13. }
    14. //更新物理世界状态的方法定义
    15. function updatePhysics(deltaTime) {
    16. physicsWorld.stepSimulation(deltaTime, 10);
    17. //遍历“刚体”列表,并更新物理世界中的所有刚体状态
    18. for (let i = 0; i < rigidBodies.length; i++) {
    19. //变量定义:three.js需要的meshObject,和ammo.js需要的ammoObject
    20. let meshObject = rigidBodies[i];
    21. let ammoObject = meshObject.userData.physicsBody;
    22. //获取物体当前运动状态
    23. let objectMotion = ammoObject.getMotionState();
    24. //如果物体正在移动,则获取物体的当前位置和旋转信息
    25. if (objectMotion) {
    26. objectMotion.getWorldTransform(transform);
    27. let mPosition = transform.getOrigin();
    28. let mQuaternion = transform.getRotation();
    29. // 更新物体的位置和旋转状态
    30. meshObject.position.set(mPosition.x(), mPosition.y(), mPosition.z());
    31. meshObject.quaternion.set(mQuaternion.x(), mQuaternion.y(), mQuaternion.z(), mQuaternion.w());
    32. }
    33. }
    34. }
    35. 复制代码

    用户输入

    我们希望用户在桌面和触摸屏移动设备上都能够在应用中移动球体。

    对于键盘事件,当按下箭头键时,通过监听“keydown”和“keyup”事件对球体添加相应方向的力。

    对于触摸屏,在屏幕上创建了一个操纵杆控制器。然后,我们将“touchstart”、“touchmove”和“touchend”事件监听器添加到用于控制的div元素(控制器)中。

    控制器会跟踪用户手指移动的起始、当前和结束坐标,然后在每次渲染时相应地更新球的受力。

    下面只是控制器代码的一个片段,展示了一些大致的概念。有关完整代码,请从本文底部的源代码地址获取。

    1. // 在坐标平面上保持对当前球体运动的跟踪
    2. let moveDirection = { left: 0, right: 0, forward: 0, back: 0 };
    3. //控制器div在屏幕上的位置坐标
    4. let coordinates = { x: 0, y: 0 };
    5. //保存触摸事件的起始坐标的变量
    6. let dragStart = null;
    7. //创建控制器div元素
    8. const stick = document.createElement("div");
    9. //监听用户触摸点的移动
    10. function handleMove(event) {
    11. //没有移动,返回
    12. if (dragStart === null) return;
    13. //有移动,获取新的触摸点的x、y坐标
    14. if (event.changedTouches) {
    15. event.clientX = event.changedTouches[0].clientX;
    16. event.clientY = event.changedTouches[0].clientY;
    17. }
    18. //根据触摸点的移动,计算出控制器div的实时坐标
    19. const xDiff = event.clientX - dragStart.x;
    20. const yDiff = event.clientY - dragStart.y;
    21. const angle = Math.atan2(yDiff, xDiff);
    22. const distance = Math.min(maxDiff, Math.hypot(xDiff, yDiff));
    23. const xNew = distance * Math.cos(angle);
    24. const yNew = distance * Math.sin(angle);
    25. coordinates = { x: xNew, y: yNew };
    26. //根据实时坐标更新样式
    27. stick.style.transform = `translate3d(${xNew}px, ${yNew}px, 0px)`;
    28. //根据坐标计算出球的运动方向
    29. touchEvent(coordinates);
    30. }
    31. //根据用户的触摸点移动坐标计算出球的运动方向
    32. function touchEvent(coordinates) {
    33. // 向右运动
    34. if (coordinates.x > 30) {
    35. moveDirection.right = 1;
    36. moveDirection.left = 0;
    37. // 向左运动
    38. } else if (coordinates.x < -30) {
    39. moveDirection.left = 1;
    40. moveDirection.right = 0;
    41. } else {
    42. moveDirection.right = 0;
    43. moveDirection.left = 0;
    44. }
    45. //向前运动
    46. if (coordinates.y > 30) {
    47. moveDirection.back = 1;
    48. moveDirection.forward = 0;
    49. //向后运动
    50. } else if (coordinates.y < -30) {
    51. moveDirection.forward = 1;
    52. moveDirection.back = 0;
    53. } else {
    54. moveDirection.forward = 0;
    55. moveDirection.back = 0;
    56. }
    57. }

  • 相关阅读:
    Spring之AOP
    cnn卷积神经网络反向传播,卷积神经网络维度变化
    数据结构 第四章:串
    【Java】多线程编程面试题总结
    计算机毕设(附源码)JAVA-SSM基于Java学生宿舍管理系统
    利用 Linux grep 和 awk 完成日志过滤
    jdk11新特性——移除的一些其他内容
    1688商品详情(商品主图、sku)
    N沟道场效应管 FDA69N25深度图解 工作原理应用
    大商创的开源代码中有很多后门,以方便官方监控系统的使用,官方做的真是无孔不入啊,我找到了下面几种
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/127766018