/threejs-learning

Three.js学习历程

Primary LanguageJavaScriptMIT LicenseMIT

探索Three.js

本项目是学习Three.js的练习项目,主要学习Three.js的基本用法。代码目录根据探索Three.js 章节命名。例如1.1章,代码目录为1.1

在线Demo: https://www.samoy.site/threejs-learning
本地Demo: http://localhost:5173/index.html

1.1 Three.js 应用的结构

地址:http://localhost:5173/1.1/index.html

1.2 你的第一个Three.js场景

地址:http://localhost:5173/1.2/index.html

1.3 介绍世界应用程序

地址:http://localhost:5173/1.3/index.html

挑战

简单

  1. 更改场景背景的颜色。您可以输入任何标准颜色名称,例如红色、绿色、紫色等,以及一些不常见的名称,例如海蓝宝石或珊瑚色。你能猜出 140 个 CSS 颜色名称中的多少个?
    查看答案 A:在World/components/scene.js中做如下修改:
       scene.background = new Color('你想替换的颜色');
     

中等

  1. 将立方体更改为其他形状,例如矩形、球体、三角形或圆环。(提示: 在文档中搜索“BufferGeometry”。)
    查看答案 A:在World/components/cube.js中做如下修改:
    矩形:
       const geometry = new BoxBufferGeometry(2, 1, 2);
     
    球体:
      const geometry = new SphereBufferGeometry(1, 10, 10);
    
    三角形:
    const geometry = new BufferGeometry();
    const vertices = new Float32Array([
         -1.0, -1.0, 0.0,
         1.0, -1.0, 0.0,
         1.0, 1.0, 0.0,
    ]);
    geometry.setAttribute('position', new BufferAttribute(vertices, 3));
    
    圆环:
    const geometry = new TorusGeometry(1, 0.1, 100, 100);
    
  2. 添加第二个立方体并使用mesh.position.set(x, y, z)移动它(您需要找出从createCube 函数返回两个多维数据集的某种方法,或者添加第二个模块,如cube2.js)。
    查看答案 A:在World/components/cube.js中做如下修改:
     const geometry1 = new BoxBufferGeometry(2, 1, 2);
     const material1 = new MeshBasicMaterial();
     const cube1 = new Mesh(geometry1, material1);
     const geometry2 = new RoundedBoxGeometry(1,1);
     const material2 = new MeshBasicMaterial();
     const cube2 = new Mesh(geometry2, material2);
     cube2.position.set(2, -2, 0)
     return [cube1, cube2];
     
    World/components/world.js中做如下修改:
    const [cube1, cube2] = createCube();
    this.#scene.add(cube1, cube2);
    

困难

  1. 向 HTML 页面添加一个按钮,并延迟渲染场景,直到单击该按钮。无需 对 World 应用程序进行任何更改即可执行此操作。相反,在 index.html 中创建按钮并在 main.js 中设置它。
    查看答案index.html中做如下修改:
    <button id="load">加载</button>
    
    main.js中做如下修改:
    document.querySelector('#load').onclick = () => {
     main();
    };
    

1.4 基于物理的渲染和照明

地址:http://localhost:5173/1.4/index.html

挑战

简单

  1. 尝试改变材料的颜色。所有正常的颜色,如red、green或blue,以及更多奇特的颜色,如peachpuff、orchid或papayawhip,都可以使用。
    查看答案 A:在World/components/cube.js中做如下修改:
        const material = new MeshStandardMaterial({
         color: '你想要替换的颜色'
     });
     
  2. 尝试改变灯光的颜色。同样,您可以使用任何 CSS 颜色名称。观看如何设置各种灯光和材质颜色为立方体提供最终颜色。
    查看答案 A:在World/components/light.js中做如下修改:
    const light = new DirectionalLight('你想要替换的颜色', 8);
  3. 尝试移动灯光(使用light.position)并观察结果。
    查看答案 网格的显示效果会根据光照的位置发生相应的改变。

中等

  1. 测试其他直射光类型: PointLightSpotLight,和 RectAreaLight
    查看答案World/components/light.js中做如下修改:
       // const light = new PointLight('white', 8);
       // const light = new SpotLight('white', 8);
       // const light = new RectAreaLight('white', 8);
     
  2. MeshBasicMaterial并且MeshStandardMaterial不是唯一可用的材料。 three.js 核心**有十八种材质,任何名称中带有“mesh”字样的材质都可以与我们的立方体网格一起使用。测试其中一些(提示: 在文档中搜索" material")。您需要先导入其他灯光和材质类,然后才能使用它们!
    查看答案 查看https://threejs.org/docs/index.html?q=material#api/zh/materials/Material

困难

重新创建场景Lighting and Depth ,减去动画(提示:使用两个网格和两个材质)

查看答案World/components>目录下创建cube1.js:
import {
      BoxBufferGeometry,
      Mesh, MeshBasicMaterial
} from 'three';
function createCube1() {
   // create a geometry
   const geometry = new BoxBufferGeometry(2, 2, 2);
   // create a default (white) Basic material
   const material = new MeshBasicMaterial({
      color: 'purple'
   });
   // create a Mesh containing the geometry and material
   const cube = new Mesh(geometry, material);
   cube.rotation.set(-0.5, -0.1, 0.8)
   cube.position.set(3, 0, 0)
   return cube;
}
export {createCube1};
World/World.js中做如下修改:
   import {createCube1} from "./components/cube1";
   class World {
      constructor(container) {
         //...
         const cube = createCube();
         const cube1 = createCube1();
         const light = createLights();
         this.#scene.add(cube, cube1, light);
         //...
   }
}

1.5 变换和坐标系

地址:http://localhost:5173/1.5/index.html

挑战

简单

  1. 打开 cube.js 模块并尝试使用cube.position、cube.rotation和cube.scale。

    查看答案 参见World/components/cube.js
  2. 打开 lights.js 模块并尝试使用light.position。注意设置light.rotation和light.scale没有效果。

    查看答案 参见World/components/light.js
  3. 在 camera.js 模块中对camera.position和camera.rotation进行实验。注意设置camera.scale没有效果。

    查看答案 参见World/components/camera.js

中等

  1. 创建第二个网格,称为meshB。让它变成不同的颜色或不同的形状,这样你就可以识别它。 将此新网格添加为第一个网格的子对象。从一个轴开始——也许是X轴 - 并调整每个网格的位置。尝试猜测当你这样做时两个网格最终位置将在哪里。注意平移是如何 叠加 的。如果您将两个网格平移5个单位,则子对象将总共移动10个单位。
    查看答案World/components/cube.js中做如下修改:
     const cube1 = new Mesh(geometry, new MeshStandardMaterial(
         {
             color: 'red'
         }
     ));
     cube1.position.x = 2;
     cube.add(cube1);
    
  2. 现在尝试设置两个网格的旋转。同样,首先将自己限制在一个轴上。再次注意,旋转是相加的。如果您旋转父对象45°, 子对象45° ,则子对象的最终旋转将是九十度。 请记住使用MathUtils.degToRad将度数转换为弧度。
    查看答案World/components/cube.js中做如下修改:
     // ...
     cube.rotation.x = MathUtils.degToRad(45);
     cube1.rotation.x = MathUtils.degToRad(45);
     // ...
    
  3. 最后,尝试设置两个网格的缩放比例。这一次,请注意缩放比例是 相乘 的。如果将父网格缩放2倍,将子网格缩放4倍,则子网格将增长到其初始大小的八倍。
    查看答案World/components/cube.js中做如下修改:
     // ...
     cube.scale.set(2, 2, 2)
     cube1.scale.set(4, 4, 4)
     // ...
    

困难

  1. 如果您熟悉弧度,请尝试不使用.degToRad方法进行上述练习。 您可以使用Math.PI
    查看答案 以下是度和弧度之间的转换:
    30° = Math.PI / 6
    45° = Math.PI / 4
    90° = Math.PI / 2
    180° = Math.PI
    360° = Math.PI * 2
    

1.6 使我们的场景育有响应性

地址:http://localhost:5173/1.6/index.html

挑战

简单

  1. 启用和禁用AA并比较差异。
    查看答案 开启AA: 边缘处较为平滑。 禁用AA:边缘处不平滑,有锯齿。
  2. 旋转立方体,直到边缘垂直和水平。现在,你能看出禁用AA时有什么不同吗?
    查看答案 几乎没有区别。
  3. 注释掉 World.js 中调整大小的代码,并比较调整窗口大小时的差异。
    查看答案 场景不会重新渲染
  4. 注释掉 World.js 中的自定义onResize钩子,看看当你调整窗口大小时会发生什么。
    查看答案 依然会重新渲染

中等

  1. 禁用抗锯齿。现在,放大立方体以更好地查看锯齿伪影。不要使用浏览器的缩放功能。相反,请尝试以下方法:
    • 使用cube.scale放大立方体。
    • 使用cube.position.z使立方体更靠近您。
    • 使用camera.position.z使相机更靠近立方体。
    查看答案
    • cube.scale.set(10, 10, 10)
    • cube.position.z = 4
    • camera.position.z = 3
  2. 仍然禁用AA,使用camera.position.x(水平移动)和camera.position.y(垂直移动)放大立方体的右上角。
    查看答案
       camera.position.x = 1;
       camera.position.y = 1;
    
  3. 重复2.,但这一次,使用cube.position.xcube.position.y
    查看答案
       cube.position.x = 2;
       cube.position.y = 2;
    

困难

  1. 不要使用容器来调整场景大小,而是尝试手动输入一些数字。例如,创建一个宽高64像素或宽高256像素的场景。您可能希望在此处更改场景的背景颜色以更轻松地查看。
    查看答案
       renderer.setSize(256, 256);
    
  2. 玩玩devicePixelRatio。尝试为DPR设置更高的值,例如4或8(不过不要太高!)。如果您将值设置为低于1,例如0.5,会发生什么情况?如果您为DPR设置高值并禁用AA,会发生什么情况?立方体的边缘看起来如何?
    查看答案
       renderer.setPixelRatio(4);
    
    在禁用AA的情况下,DPR越小,边缘锯齿越明显。

1.7 动画循环

地址: http://localhost:5173/1.7/index.html

挑战

简单

  1. 玩一玩动画速度。使立方体每百秒旋转一圈,然后每秒旋转一圈。
    查看答案 每百秒旋转一圈:
    const radiansPerSecond = MathUtils.degToRad(360 / 100);
    
    每秒旋转一圈:
    const radiansPerSecond = MathUtils.degToRad(360);
    
  2. 您可以为任何东西设置动画,而不仅仅是旋转。尝试为网格的其他一些属性设置动画。
    查看答案
    cube.position.x += delta / 2;
    cube.position.y += delta / 2;
    cube.position.z += delta / 2;
    cube.scale.x += delta / 2;
    cube.scale.y += delta / 2;
    cube.scale.z += delta / 2;
    

中等

  1. 给相机添加一个.tick方法,然后让它慢慢缩小。尝试以每秒一米左右的速度缩小。
    查看答案World/components/camera.js中添加以下代码:
    const meterPerSecond = 1;
    camera.tick = (delta) => {
       camera.position.z += delta * meterPerSecond;
    };
    
    World/World.js中添加以下代码:
    this.#loop.updatables.push(this.#camera);
    
  2. 向灯光添加一个.tick方法,并对light.position.x, .y或.z参数进行动画处理。
    查看答案World/components/light.js中添加以下代码:
    light.tick = function (delta) {
       light.position.x += delta * 10;
       light.position.y += delta * 10;
       light.position.z += delta * 10;
    }
    
    World/World.js中添加以下代码:
    this.#loop.updatables.push(light);
    
  3. 添加一个启动和停止动画循环的click事件监听器(或者,如果你想花哨的话,一个按钮)。在main.js中使用World.startWorld.stop执行此操作。
    查看答案main.js做如下改造:
     const container = document.querySelector("#scene-container");
     const button = document.createElement("button");
     let isAnimate = true;
     container.append(button);
     const world = new World(container);
     button.innerHTML = '结束动画'
     button.onclick = () => {
         if (isAnimate) {
             button.innerHTML = '开始动画';
             world.stop();
         }else {
             button.innerHTML = '结束动画'
             world.start();
         }
         isAnimate = !isAnimate;
     }
     world.start();
    

困难

  1. 使用模运算符为立方体、相机或灯光设置.position动画。让相机反复缩小十米。让立方体一遍又一遍地从屏幕的左到右进行动画。
    查看答案World/components/cube.js中添加以下代码:
    cube.position.x =  cube.position.x % 10 + 1;
    
    World/components/camera.js中添加以下代码:
    camera.tick = (delta) => {
       camera.position.z = camera.position.z % 10 + 1;
    }
    
  2. 让相机缩小十米,然后反方向再次放大。在屏幕上从左到右为立方体设置动画,然后,当它到达屏幕的右边缘(大致)时,让它反向并移回起点。
    查看答案World/components/camera.js中添加以下代码:
    let i = 0;
    let isAdd = true;
    camera.tick = (delta) => {
       // 让相机缩小十米,然后反方向再次放大
       let x = i % 10;
       camera.position.z = camera.position.z + (isAdd ? x : -x);
       if (x === 9) {
             isAdd = !isAdd;
       }
       i++;
    };
    
    World/components/cube.js中添加以下代码:
    let isAdd = true;
    cube.tick = (delta) => {
       if (isAdd) {
          cube.position.x += 0.1;
          if (cube.position.x > 5) {
             isAdd = false;
          }
       } else {
          cube.position.x -= 0.1;
          if (cube.position.x < -5) {
             isAdd = true;
          }
       }
    }
    

1.8 纹理映射简介

地址: http://localhost:5173/1.8/index.html

挑战

简单

  1. 更改材质的颜色。尝试紫色、红色、绿色、蓝色或您喜欢的任何其他颜色。注意每种颜色如何与黑白纹理相结合。
    查看答案
    const material = new MeshStandardMaterial({
       map: texture,
       color: 'purple'
    });
    
  2. /assets/textures文件夹中包含了第二个纹理文件,称为uv-test-col.png。你能加载这个文件并将它应用到材质的.map槽中吗?
    查看答案
    const texture = textureLoader.load('/1.8/assets/textures/uv-test-col.png');
    
  3. 尝试将立方体切换为其他形状。 在文档中搜索“BufferGeometry”以查看所有可用的几何体。注意纹理是如何映射到不同形状上的。
    查看答案
    const geometry = new SphereGeometry(1,100, 100);
    
  4. 打开MeshStandardMaterial文档 页面。该材质共有11个纹理贴图插槽,每个插槽的名称中都包含map。你能找到所有的吗?
    查看答案
    该文档中所有以map结束的属性都表示纹理贴图槽。
    

中等

  1. 尝试将我们加载的纹理分配给材质上的其他贴图槽。它们可能并非都有效,但您会得到一些有趣的结果。
    查看答案
    const material = new MeshStandardMaterial({
       normalMap:texture,
    });
    
  2. uv-test-col.png做同样的事情。然后,一次加载两个纹理并将它们同时分配到不同的插槽。
    查看答案
    const texture = textureLoader.load('/1.8/assets/textures/uv-test-bw.png');
    const texture1 = textureLoader.load('/1.8/assets/textures/uv-test-col.png');
    const material = new MeshStandardMaterial({
       map: texture1,
       normalMap:texture,
    });
    
  3. 打开Texture文档 。通读可以在纹理上设置的各种属性。尝试调整.offset.repeat.rotation.center属性。 这些(除了.rotation)中的每一个都是一个Vector2,因此您可以使用.set(x,y)它们来调整它们。
    查看答案
    const texture = textureLoader.load('/1.8/assets/textures/uv-test-bw.png');
    texture.center.set(5, 5);
    texture.wrapS = RepeatWrapping;
    texture.wrapT = RepeatWrapping;
    texture.repeat.set(4, 4);
    texture.offset.set(0.1, 0.1);
    texture.rotation = MathUtils.degToRad(90)
    const material = new MeshStandardMaterial({
       map: texture,
    });
    

困难

  1. 材质中的每个纹理槽都与一个或多个属性(如.color.ma p)相关联。贴图要么是一个调制属性 (同样,像.color.map),或者它本身被一些其他属性调制(像.bumpMapand.bumpScale )。当您测试不同插槽中的纹理时,请尝试调整这些调制属性。其中一些是颜色(如.coloremissive), 另一些是矢量(如.normalScale),但大多数是简单数字(如.bumpScale.displacementScale)。在每种情况下,文档都清楚地说明了这一点。
    查看答案 请参考https://threejs.org/docs/#api/zh/materials/MeshStandardMaterial
  2. 我们在上面提到,Texture类是HTML图像的包装器。如果您将texture 打印到控制台,您应该能够找到该图像。您可以在控制台中找到uv-test-bw.png的URL并在新浏览器选项卡中打开它吗?
    查看答案 uv-test-bw.png的url是http://localhost:5173/1.8/assets/textures/uv-test-bw.png

1.9 使用相机控制插件扩展three

地址: http://localhost:5173/1.9/index.html

挑战

简单

  1. 尝试调整控件的最小和最大缩放级别 。如果你让这两个值相等会发生什么?或使minDistance大于maxDistance
    查看答案 无法进行缩放,代码如下:
    controls.minDistance = 7;
    controls.maxDistance = 5;
    
  2. 启用自动旋转,然后尝试调整旋转速度。
    查看答案
    controls.autoRotate = true;
    controls.autoRotateSpeed = 1;
    

尝试禁用三种控件模式中的每一种 ,一次禁用一种,然后观察结果。

查看答案
controls.enableRotate = false;
controls.enableZoom = false;
controls.enablePan = false;
4. [调整阻尼速度](https://discoverthreejs.com/zh/book/first-steps/camera-controls/#enable-damping-for-added-realism) (`.dampingFactor`) 以了解阻尼的工作原理。大于0和小于1的值效果最好。
查看答案
   controls.dampingFactor = 0.6;
   

中等

尝试调整控件的水平和垂直旋转限制 。请记住,如果您以度为单位,则必须转换为弧度。如果您需要提醒它是如何工作的,请查看cube.js

查看答案
// 水平:
controls.minAzimuthAngle = -Math.PI;
controls.maxAzimuthAngle = Math.PI;
// 垂直:
controls.minPolarAngle = 0;
controls.maxPolarAngle = Math.PI;
2. 向页面添加一个按钮(或单击事件侦听器),并且每当您单击该按钮时,将相机和控件的目标移动到一个新的随机位置。尝试限制移动,使立方体始终位于屏幕上的某个位置。
查看答案src/World/World.js中添加以下代码:
   translate() {
        let rangeSize = 4 - (-4) + 1;
        let random = Math.random() * rangeSize - 4;
        this.#camera.position.set(random, 0, 10);
        this.render();
    }

reset() { this.#camera.position.set(0, 0, 10); this.render(); }

src/main.js中做如下修改:

   const button = document.createElement('button');
   button.innerText = "Start";
   let isTranslate = false;
   button.addEventListener('click', () => {
      if (isTranslate) {
         button.innerText = "Start";
         world.reset();
      } else {
         world.translate();
         button.innerText = "Reset";
      }
      isTranslate = !isTranslate;
    });
   container.append(button);
   

困难

设置在使用控件时按需渲染 ,包括在纹理加载后以及在调整场景大小时生成新帧。

查看答案src/World/components/cube.js中做如下修改:
function createCube(callback) {
   const geometry = new BoxBufferGeometry(2, 2, 2);
   const material = createMaterial(callback);
   const cube = new Mesh(geometry, material);
   cube.rotation.set(-0.5, -0.1, 0.8);
   const radiansPerSecond = MathUtils.degToRad(30);
   cube.tick = (delta) => {
     cube.rotation.x += radiansPerSecond * delta;
     cube.rotation.y += radiansPerSecond * delta;
     cube.rotation.z += radiansPerSecond * delta;
   }
   return cube;
}
function createMaterial(callback) {
  const textureLoader = new TextureLoader();
  const texture = textureLoader.load('/1.8/assets/textures/uv-test-bw.png', callback);
  const material = new MeshStandardMaterial({
    map: texture,
  });
  return material;
}
src/World/World.js中做如下修改:
const cube = createCube(()=>{
   this.render();
});
resizer.onResize = () => {
   this.render();
}
2. 你能在几秒钟内让相机和控件的目标动画到一个新的位置吗?也许在页面上添加一个按钮,当你点击它时,播放动画。看看当您只为相机或目标设置动画时会发生什么, 或者当您在制作动画时不禁用控件时会发生什么。 设置此动画的最佳位置是在控件controls模块中。
查看答案src/World/components/controls.js中做如下修改:
   controls.tick = () => {
      if (controls.target.x >= -4) {
         controls.target.x -= 0.1;
      }
      controls.update();
   }
   
src/World/World.js中做如下修改:
   stop() {
      this.#loop.stop();
      this.#controls.reset();
   }
   
src/main.js中做如下修改:
   const button = document.createElement('button');
    button.innerText = 'Start'
    let isReset = true;
    button.addEventListener('click', () => {
      if (isReset) {
         world.start();
         button.innerText = 'Reset'
      } else {
         world.stop();
         button.innerText = 'Start'
      }
      isReset = !isReset;
   })
   

1.10 环境光:来自各个方向的光照

地址: http://localhost:5173/1.10/index.html

挑战

简单

  1. 暂时在编辑器中禁用mainLight,然后单独测试两个环境光类中的每一个。有几种方法可以禁用灯光。设置.intensity 为零,不向场景添加灯光,或设置mainLight.visiblefalse
    查看答案
    const mainLight = new DirectionalLight('white', 0);
    // 或
    mainLight.visible = false;
    
  2. HemisphereLight的效果来自四个属性的相互作用:天空.color.groundColor.intensity.position 。尝试调整其中的每一个并观察结果。如果您先禁用主灯,您可能会发现这更容易查看。
    查看答案
    const ambientLight = new HemisphereLight(
       'blue',
       'yellow',
       5,
    );
    ambientLight.position.set(1,1,1);
    

中等

  1. 在编辑器中,我们给HemisphereLightDirectionalLight 都赋予了5的强度。我们这样做是为了突出环境光的效果,但是,通常情况下,我们会使直射光比环境光强。你可以通过调整两盏灯的强度和颜色来提高照明质量吗?
    查看答案
    const mainLight = new DirectionalLight('white', 10);
    
  2. 添加更多的直射光怎么样,DirectionalLight或者是其他类型的一种?当你添加更多这些光照,并从不同的方向照射过来时,场景光照会有所改善吗?
    查看答案
    // 创建一个环境光光源
    const ambientLight = new HemisphereLight('white', 'white', 5);
    ambientLight.position.set(-10, -10, -10);
    // 创建一个平行光光源
    const mainLight = new DirectionalLight('yellow', 100);
    mainLight.position.set(0, 0, 0);
    // 创建一个平行光光源2
    const mainLight2 = new DirectionalLight('blue', 50);
    mainLight2.position.set(10, 10, 10);
    return {
       ambientLight,
       mainLight,
       mainLight2
    };
    
  3. 更多的环境光呢?还是同时添加一个AmbientLight和一个HemisphereLight?这对现场有什么影响?
    查看答案
    function createLights() {
       // 创建一个环境光光源
       const ambientLight = new AmbientLight('red', 5);
       // 创建一个半球光光源
       const hemisphereLight = new HemisphereLight('green', 'darkslategrey', 6);
       hemisphereLight.position.set(-10, -10, -10);
       // 创建一个平行光光源
       const mainLight = new DirectionalLight('blue', 8);
       mainLight.position.set(10, 10, 10);
       return {
         ambientLight,
         hemisphereLight,
         mainLight
       };
    }
    

困难

  1. 从本章开始我们的问题的另一个解决方案是添加一个光作为相机的子级。这样,当相机移动时,光线也会移动。你可以把它想象成一个相机和手电筒绑在一边。使用这种方法, 我们可以使用单个DirectionalLightSpotLight照亮场景。试试这个。首先,删除ambientLight ,然后将相机添加到场景中,最后将mainLight添加到相机中。
    查看答案src/World/World.js中做如下修改:
    this.#scene.add(cube, this.#camera);
    this.#camera.add(mainLight);
    

1.11 组织你的场景

地址: http://localhost:5173/1.11/index.html

挑战

简单

  1. 通过更改循环中的值0.05来增加和减少球体的数量。在进行更改之前尝试计算您想要多少个球体,而不是输入随机数。
    查看答案
    球体数量 = 1 / 步进数(0.05)
    
  2. 尝试除球体和盒子之外的其他形状。比如 锥体、 圆柱体、 圆环,或 正十二面体?对于本练习,只需将SphereBufferGeometry 替换为其他缓冲区几何体类之一。 每种几何体的构造函数采用不同的参数,因此请仔细阅读文档,并记住在使用之前导入它们。
    查看答案 参见1.3 介绍世界应用程序#中等
  3. 尝试调整widthSegmentsheightSegments。在您注意到帧速率下降之前,您最高可以设置多高?值非常低的球体是什么样的?如果两个参数不使用相同的数字会怎样?
    查看答案 widthSegmentsheightSegments的值越高,球体越细腻,加载时间越长。两者不一样时,会导致球体出现锯齿。

中等

  1. group.tick方法内部,我们每一帧都减去一个旋转:.rotation.z -= ...。这将导致顺时针旋转。切换到+=,并注意旋转如何变为 逆时针。 如果添加旋转,则运动将逆时针。如果减去旋转,运动将是顺时针方向。three.js中的正旋转是逆时针的
    查看答案
    // group.rotation.z -= 0.05;
    group.rotation.z += 0.05
    
  2. 你能在这里创建一些其他的动画吗?请记住,您可以为任何可以更改的属性设置动画。
    查看答案
     group.position.x += delta * 0.5;
     group.position.y += delta * 0.5;
    

困难

你猜对了!你能让编辑器中的场景与上面的场景 完全匹配吗?

查看答案src/World/components/camera.js中做如下修改:
camera.position.set(-3, 0, 2);
src/World/components/meshGroup.js中做如下修改:
for (let i = 0; i < 1; i += 0.001) {
   const sphere = protoSphere.clone();
   sphere.position.x = Math.cos(2 * Math.PI * i);
   sphere.position.y = Math.sin(2 * Math.PI * i);
   sphere.position.z = -i * 5;
   sphere.scale.multiplyScalar(0.01 + i);
   group.add(sphere);
}
group.scale.multiplyScalar(2);
const radiansPerSecond = MathUtils.degToRad(30);
group.tick = (delta) => {
   group.rotation.z -= delta / 2 ;
}
2. 回到原来的场景,你能在圆圈周围交替使用两种不同的形状吗?比如说,十个球体和十个盒子?如何在三种不同的形状之间交替?或者十种不同的形状呢?
查看答案
   function createMeshGroup() {
      const group = new Group();
      const geometry = new SphereBufferGeometry(0.25, 16, 16);
      const material = new MeshStandardMaterial({
         color: 'indigo',
      });
      const protoSphere = new Mesh(geometry, material);
      group.add(protoSphere);
      for (let i = 0; i < 1; i += 0.1) {
         let index = Math.floor(i * 20) % 3;
         const geometry = createGeometry(index);
         const mesh = new Mesh(geometry, material);
         mesh.position.x = Math.cos(2 * Math.PI * i);
         mesh.position.y = Math.sin(2 * Math.PI * i);
         mesh.scale.multiplyScalar(0.01 + i);
         group.add(mesh);
      }
      group.scale.multiplyScalar(2);
      const radiansPerSecond = MathUtils.degToRad(30);
      group.tick = (delta) => {
         // group.rotation.z -= delta * radiansPerSecond;
      }
      return group;
   }
   function createGeometry(index) {
      return {
         0: new BoxGeometry(0.25, 0.25, 0.25),
         1: new SphereBufferGeometry(0.25, 16, 16),
         2: new RingGeometry(0.25, 0.5, 16,)
      }[index];
   }
   
3. 虽然您确实可以为任何属性设置动画,但最难的部分是制作平滑、重复的运动。旋转是一种特殊情况,因为您可以不断增加,并且物体会绕圈转。 要为其他属性创建类似的行为,您可以使用三角函数`sin`、`cos`和`tan`。我们使用`cos`和`sin` 将球体放置在一个圆圈中,您可以执行类似的操作来将组的位置移动到一个圆圈中。 你能做到吗?没有提示,毕竟,这应该是一个艰巨的挑战!
查看答案
   let i = 0;
   group.tick = (delta) => {
      // group.rotation.z += delta * radiansPerSecond;
      group.position.x -= Math.cos(Math.PI * i * 2) * 0.1;
      group.position.y -= Math.sin(Math.PI * i * 2) * 0.1;
      i += 0.01;
    }
   

1.12 内置几何体

地址: http://localhost:5173/1.12/index.html

挑战

简单

  1. 有什么比玩具火车更好的呢?两个玩具火车怎么样?你可以.clone 整个火车之后在创建它。现在就这样做,然后调整第二列火车的.position。不要忘记将它添加到场景中!
    查看答案src/World/World.js中做如下修改:
    const train2 = train.clone();
    train2.position.set(4, -3, 0);
    this.#loop.updatables.push(controls, train);
    this.#scene.add(ambientLight, mainLight, train, train2);
    
  2. 有什么比两辆玩具火车更好的呢? 在循环中创建一大堆火车。在循环中,确保移动每辆新火车,使它们不会全部堆叠在一起,然后将它们添加到场景中。看看有多少有趣的方式可以定位克隆的火车。
    查看答案src/World/World.js中做如下修改:
    for (let i = 0; i < 10; i ++) {
       const newTrain = train.clone();
       newTrain.position.y -= i * 4;
       this.#scene.add(newTrain);
    }
    

中等

  1. 你能在货舱里创造一个窗户吗?没有办法在几何体上打孔(不使用外部库),因此您必须从几个盒子几何体中重建货舱。一种方法是为地板创建一个大盒子,然后为屋顶创建另一个大盒子, 最后,围绕屋顶边缘创建四个用于支柱的小盒子(或圆柱体)。
    查看答案src/World/components/Train/geometries.js中做如下修改:
    function createGeometries() {
       const top = new BoxBufferGeometry(2, 0.5, 1.5);
       const bottom = new BoxBufferGeometry(2, 0.5, 1.5);
       // 创建柱子
       const cylinder = new CylinderBufferGeometry(0.1, 0.1, 1.5, 12);
       const nose = new CylinderBufferGeometry(0.75, 0.75, 3, 12);
       const wheel = new CylinderBufferGeometry(0.4, 0.4, 1, 16);
       const chimney = new CylinderBufferGeometry(0.3, 0.1, 0.5);
       return {
          cylinder,
          top,
          bottom,
          nose,
          wheel,
          chimney
       }
    }
    
    src/World/components/Train/meshes.js中做如下修改:
    const top = new Mesh(geometries.top, materials.body);
    top.position.set(1.5, 2.25, 0);
    const bottom = new Mesh(geometries.bottom, materials.body);
    bottom.position.set(1.5, 0.5, 0);
    // 柱子1
    const cylinder = new Mesh(geometries.cylinder, materials.detail);
    cylinder.position.set(0.6, 1.5, 0.65);
    // 柱子2
    const cylinder2 = cylinder.clone();
    cylinder2.position.set(2.4, 1.5, 0.65);
    // 柱子3
    const cylinder3 = cylinder.clone();
    cylinder3.position.set(2.4, 1.5, -0.65);
    // 柱子4
    const cylinder4 = cylinder.clone();
    cylinder4.position.set(0.6, 1.5, -0.65);
    const cabin = new Group();
    cabin.add(cylinder, cylinder2, cylinder3, cylinder4);
    
    src/World/components/Train/Train.js中做如下修改:
    this.add(
       this.meshes.top,
       this.meshes.bottom,
       this.meshes.cabin,
       this.meshes.nose,
       this.meshes.chimney,
       this.meshes.smallWheelRear,
       this.meshes.smallWheelCenter,
       this.meshes.smallWheelFront,
       this.meshes.bigWheel
    )
    
  2. 没有轨道的火车走不了多远!在车轮下添加一些轨道。创建两个主要轨道,然后在轨道下创建一个枕木并使用克隆创建其余部分。
    查看答案src/World/components/Train/geometries.js中做如下修改:
    // 轨道
    const track = new CylinderBufferGeometry(0.1, 0.1, 5, 16);
    // 枕木
    const pillar = new CylinderBufferGeometry(0.1, 0.1, 1, 16);
    return {
       ...
       track,
       pillar
    }
    
    src/World/components/Train/meshes.js中做如下修改:
    // 轨道
    const track = new Mesh(geometries.track, materials.body);
    track.rotation.z = Math.PI / 2;
    track.position.set(0, 0, 0.4);
    const track2 = track.clone();
    track2.position.set(0, 0, -0.4);
    // 创建枕木
    const pillar = new Mesh(geometries.pillar, materials.detail);
    pillar.rotation.x = Math.PI / 2;
    const pillars = new Group();
    for (let i = -2.4; i < 2.6; i+=0.3) {
       const newPillar = pillar.clone();
       newPillar.position.set(i, 0, 0);
       pillars.add(newPillar);
    }
    return {
       ...,
       track,
       track2,
       pillars
    }
    
    src/World/components/Train/Train.js中做如下修改:
    this.add(
       ...,
       this.meshes.track,
       this.meshes.track2,
       this.meshes.pillars
    )
    
  3. 每辆火车都需要一名售票员!创建一个站在火车旁边的简单人形(如乐高角色)。
    查看答案src/World/components/Train/geometries.js中做如下修改:
    // 售票员头部
    const head = new SphereGeometry(0.3, 16, 16);
    // 售票员身体
    const body = new BoxBufferGeometry(1, 0.6, 0.6);
    // 左腿
    const leftLeg = new CylinderBufferGeometry(0.1, 0.1, 0.7,32);
    // 右腿
    const rightLeg = new CylinderBufferGeometry(0.1, 0.1, 0.7,32);
    
    src/World/components/Train/meshes.js中做如下修改:
    // 售票员头部
    const head = new Mesh(geometries.head, materials.detail);
    head.position.set(1.5,1.5, 1.5);
    // 售票员身体
    const body = new Mesh(geometries.body, materials.body);
    body.position.set(1.5,0.9,1.5);
    // 左腿
    const leftLeg = new Mesh(geometries.leftLeg, materials.detail);
    leftLeg.position.set(1.3,0.32,1.5);
    // 右腿
    const rightLeg = new Mesh(geometries.rightLeg, materials.detail);
    rightLeg.position.set(1.7,0.32,1.5);
    
    新建一个src/World/components/Train/Conductor.js文件,然后在其中中添加:
    import {Group} from "three";
    import {createMeshes} from "./meshes";
    class Conductor extends Group {
       constructor() {
          super();
          this.meshes = createMeshes();
          this.add(
             this.meshes.head,
             this.meshes.body,
             this.meshes.leftLeg,
             this.meshes.rightLeg
          )
       }
    }
    export {
       Conductor
    }
    
    src/World/World.js中做如下修改:
    const conductor = new Conductor();
    this.#scene.add(ambientLight, mainLight, train, conductor);
    

困难

  1. 你还能做些什么来改善这个场景?从火车的烟囱冒出一些气泡怎么样(用SphereBufferGeometry来制造气泡)。天上有些云怎么样?如何为烟雾和云设置动画?
    查看答案 气泡:
    import {Mesh, MeshStandardMaterial, SphereBufferGeometry} from "three";
    class Bubble extends Mesh {
       constructor(x, y, z) {
          super();
          // 设置气泡材料
          const material = new MeshStandardMaterial({
             color: 'white',
             transparent: true,
          })
          // 设置气泡几何体
          const geometry = new SphereBufferGeometry(0.15, 32, 32)
          // 设置气泡
          const bubble = new Mesh(geometry, material);
          let position = bubble.position;
          bubble.position.set(x, y, z);
          let i = 0;
          let j = 0;
          bubble.tick = (delta) => {
             bubble.position.x = x + i;
             bubble.position.y = y + j;
             if (i >= 0.24) {
                i = 0;
             }
             if (j >= 0.24) {
                j = 0;
             }
             i += 0.02;
             j += 0.02;
          }
          return bubble;
       }
    }
    export {
       Bubble
    }
    
    src/World/World.js中做如下修改:
    const bubbles = new Group();
    for (let i = 0; i < 3; i++) {
       const bubble = new Bubble(-2 + 0.4 * i, 2.4 + 0.4 * i, 0);
       bubbles.add(bubble);
    }
    this.#scene.add(...,...bubbles.children);
    

1.13 以glTF格式加载3D模型

地址:http://localhost:5173/1.13/index.html

挑战

简单

  1. 看看那只抢风头的鹦鹉!切换鸟的位置,让鹳和火烈鸟各自轮流带领鸟群。
    查看答案 鹳:在src/World/components/birds/birds.js中做如下修改:
    parrot.position.set(0, -2.5, -10);
    stork.position.set(0, 0, 2.5);
    
    火烈鸟:在src/World/components/birds/birds.js中做如下修改:
    parrot.position.set(7.5, 0, -10);
    flamingo.position.set(0, 0, 2.5);
    
  2. 或者,将鸟类留在原地,并尝试将controls.target注意力集中在另外两只鸟中的一只而不是鹦鹉身上。
    查看答案
    // this.#controls.target.copy(stork.position);
    this.#controls.target.copy(flamingo.position);
    

中等

  1. 添加一个带有Switch Focus文本的<button>的元素。每当您单击此按钮时,相机应聚焦在下一只鸟身上。你可以随心所欲地实现它, 但是,如果你想按照我们目前的工作来做,你应该在main.js中设置按钮,然后用一个方法扩展World类接口,将焦点移到下一个鸟。 您可以命名此方法为World.focusNext或类似的方法。
    查看答案 请自行实现它!

困难

  1. 实现上面的按钮后,您将拥有三个摄像机视图,每只鸟一个。添加第四个视图,它是场景的缩小概览,可让您看到所有三只鸟。对于这第四个视图, 您可能需要调整camera.position以及controls.target
    查看答案 请自行实现它!
  2. 现在,让相机从一个视点平滑地动画化到下一个视点。您必须同时为camera.positioncontrols.target设置动画。最好的地方是在controls.tick方法内。
    查看答案 请自行实现它!

1.14 Three.js动画系统

地址:http://localhost:5173/1.14/index.html

挑战

简单

AnimationAction有更多的动画控件.play.stop。现在试试其中的一些。

  1. 您可以使用.startAt延迟动画的开始。测试一下。
  2. 您可以使用.timeScale属性控制动画的速度。您可以直接设置值,也可以使用.setEffectiveTimeScale方法。
  3. 利用.halt逐渐减慢动画停止。
    查看答案
     action.startAt(1)
       .setEffectiveTimeScale(1)
       .halt(6)
       .play()