通过案例开始学习Threejs

89 阅读8分钟

前言

就像一个在一个房间里面,放入家具,灯什么的 房间:就像是场景(3D 容器) scene 家具(电视等):物体,就像是各种模型 model (mesh = geometry(几何,就像灯笼的骨架) + material(材质,就像灯笼的皮肤)) 灯:就像是台灯、太阳光(自然光)light 相机: 可以拍照的东西,镜头对准房间才看到房间的东西 camera 渲染器:和计算机打交道,会知道页面供人看到的效果 rederer

Three.js 程序组成

threejs程序组成.jpg

1. 配置开发环境

默认已经安装好 Node.js,没有安装 Node.js 的可以先查询资料安装好,再继续。

目录结构

.
├── client
│   ├── config
│   │   ├── webpack.common.js
│   │   ├── webpack.dev.js
│   │   └── webpack.prod.js
│   ├── index.html
│   ├── public
│   ├── src
│   │   └── index.ts
│   └── tsconfig.json
├── dist
│   ├── client
│   │   ├── images
│   │   ├── index.bundle.js
│   │   └── index.html
│   └── server
│       ├── server.js
│       └── server.js.map
├── package.json
├── package-lock.json
└── server
    ├── server.ts
    └── tsconfig.json

1.1 安装相关依赖

npm i webpack webpack-cli webpack-dev-server webpack-merge copy-webpack-plugin html-webpack-plugin typescript ts-loader nodemon rimraf npm-run-all express -D

1.2 package.json

{
  "scripts": {
    "dev:webpack": "webpack serve --config ./client/config/webpack.dev.js",
    "dev:webpack:watch": "webpack -w --config ./client/config/webpack.dev.js",
    "dev:serve": "nodemon ./dist/server/server.js",
    "tsc:watch": "tsc -w -p ./server",
    "dev": "npm-run-all -p dev:webpack dev:webpack:watch tsc:watch dev:serve",
    "--separator-1": "",
    "rimraf": "rimraf ./dist/server",
    "build:webpack": "webpack --config ./client/config/webpack.prod.js",
    "tsc:build": "tsc -p ./server",
    "build": "npm-run-all -s build:webpack rimraf tsc:build",
    "--separator-2": "",
    "serve": "node ./dist/server/server.js"
  }
}

npm-run-all 综合性命令(可顺序可并行)

  • run-s 简写,等价于 npm-run-all -s 顺序(sequentially)运行 npm-scripts
  • run-p 简写,等价于 npm-run-all -p 并行(parallel)运行 npm-scripts

1.3 webpack.common.js

// ./client/config/webpack.common.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
  entry: {
    index: path.resolve(__dirname, "../src/index.ts"),
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "../../dist/client"),
    clean: true, // clean dist
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "../index.html"),
      minify: true,
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, "../public"),
          to: path.resolve(__dirname, "../../dist/client/images"),
        },
      ],
    }),
  ],
};

1.4 webpack.dev.js

// ./client/config/webpack.dev.js

const { merge } = require("webpack-merge");
const path = require("path");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  mode: "development",
  devtool: "eval-source-map",
  devServer: {
    static: {
      directory: path.resolve(__dirname, "../../dist/client"),
    },
    hot: true,
  },
});

1.5 webpack.prod.js

// ./client/config/webpack.prod.js

const { merge } = require("webpack-merge");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  mode: "production",
  performance: {
    hints: false,
  },
});

./client/tsconfig.json

{
  "compilerOptions": {
    "target": "ES6",
    "module": "ES6",
    "outDir": "../dist/client",
    "moduleResolution": "Node",
    "baseUrl": ".",
    "paths": {
      // 指定引用的模块位置,相当与别名
    }
  },
  "include": ["./src/**/*.ts"]
}

1.6 server.ts

// ./server/server.ts

import path from "path";
import http from "http";
import express from "express";

const port: number = 3000;

class App {
  private server: http.Server;
  private port: number;

  constructor(port: number) {
    this.port = port;
    const app = express();

    const staticDir = express.static(path.join(__dirname, "../client"));
    app.use(staticDir);

    this.server = new http.Server(app);
  }

  public Start() {
    this.server.listen(this.port, () => {
      console.log(`Server listening on port ${this.port}.`);
    });
  }
}

new App(port).Start();

./server/tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "outDir": "../dist/server",
    "sourceMap": true,
    "esModuleInterop": true
  },
  "include": ["**/*.ts"]
}

2. Three.js

2.1 安装依赖

npm i three

npm i @types/three -D

2.2 client/src/index.ts

import {
  AmbientLight,
  BoxGeometry,
  Mesh,
  MeshBasicMaterial,
  MeshNormalMaterial,
  PerspectiveCamera,
  Scene,
  WebGLRenderer,
} from "three";

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

// 容器
const container = document.getElementById("container");

// 创建场景
const scene: Scene = new Scene();

/**
 * 创建相机
 * 参数:视角、视角比例(宽度和高度比)、最近像素、最远像素
 */
const camera: PerspectiveCamera = new PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
// 调整相机所在的位置
camera.position.set(1, 1, 1);
// 调整相机的镜头对准的位置
camera.lookAt(0, 0, 0);

// 几何模型 (骨架)
const geometry: BoxGeometry = new BoxGeometry();
// 材质(皮肤)
const material: MeshNormalMaterial = new MeshNormalMaterial();

// 整合几何模型和材质形成网格物体,(骨架和皮肤构成模型,密密的网格就构成了物体)
const cube: Mesh = new Mesh(geometry, material);
scene.add(cube);

// 灯光
const light = new AmbientLight(0xffffff, 1);
scene.add(light);

// 渲染器:把眼睛看到的大千世界绘制到页面(canvas)中
const renderer: WebGLRenderer = new WebGLRenderer({
  antialias: true, // 抗锯齿
});
// 计算处理 dpi
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 设置画布大小
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);

new OrbitControls(camera, renderer.domElement);

window.addEventListener("resize", onWindowResize, false);
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  // 更新相机的投影矩阵
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  render();
}

function render() {
  // 渲染
  renderer.render(scene, camera);
}

let time = Date.now();

function animate() {
  const currentTime = Date.now();
  const deltaTime = currentTime - time;
  time = currentTime;
  console.log(deltaTime); // 基本都是在 16 ,17 范围

  cube.rotation.x += deltaTime * 0.001;
  cube.rotation.y += deltaTime * 0.001;
  // cube.rotation.z += deltaTime * 0.001;

  render();

  requestAnimationFrame(animate);
}
animate();

base-1.gif

PerspectiveCamera 、OrthographicCamera

3. Stats Panel

import Stats from "three/examples/jsm/libs/stats.module";
// Other Code ...
const stats = Stats();
container.appendChild(stats.domElement);
// Other Code ...

function animate() {
  // Other Code ...

  // render();
  // stats.update();
  stats.begin();
  render();
  stats.end();

  // Other Code ...
}

stats-3.gif

4. Dat.GUI

4.1 安装依赖

npm i dat.gui

npm i @types/dat.gui -D

4.2 client/src/index.ts

import { GUI } from "dat.gui";

const gui = new GUI();
const cubeFolder = gui.addFolder("Cube");

cubeFolder.add(cube, "visible", true);

const cubeRotation = cubeFolder.addFolder("Rotation");
cubeRotation.add(cube.rotation, "x", 0, Math.PI * 2, 0.01);
cubeRotation.add(cube.rotation, "y", 0, Math.PI * 2, 0.01);
cubeRotation.add(cube.rotation, "z", 0, Math.PI * 2, 0.01);

const cubePositionFolder = cubeFolder.addFolder("Position");
cubePositionFolder.add(cube.position, "x", -10, 10, 0.01);
cubePositionFolder.add(cube.position, "y", -10, 10, 0.011);
cubePositionFolder.add(cube.position, "z", -10, 10, 0.01);

const cubeScaleFolder = cubeFolder.addFolder("Scale");
cubeScaleFolder.add(cube.scale, "x", -5, 5, 0.01);
cubeScaleFolder.add(cube.scale, "y", -5, 5, 0.01);
cubeScaleFolder.add(cube.scale, "z", -5, 5, 0.01);
cubeFolder.open();

const cameraFolder = gui.addFolder("Camera");
cameraFolder.add(camera.position, "x", 1, 10);
cameraFolder.add(camera.position, "y", 1, 10);
cameraFolder.add(camera.position, "z", 1, 10);

GUI.gif

5. Object3D

Object3D 是三种类型中许多对象的基类。js提供了在3D空间中操纵对象的方法和属性。

Meshes, Lights, Cameras, Groups 都继承自 Object3D.

使用Object3D执行的最常见的操作:Rotation,Position,Scale,Visibility

5.1 Object3D Hierarchy(Object3D 层次结构)

场景是 Object3D。可以将其他 Object3Ds 添加到场景中,它们将成为场景的子对象。场景本身是从Object3D基类派生的。

scene.add(cube)

如果旋转场景、缩放场景或平移其位置,它将影响其所有子对象。

还可以将Object3Ds添加到已经是场景一部分的其他 Object3Ds 中。对 Object3D 的任何更改(如位置、比例和旋转)都将同等地影响同一父对象下的所有子对象。

通过不断向任何现有对象添加新对象,可以创建Object3Ds的层次结构。

scene

​ |--object1 (Red Ball)

​ |--object2 (Green Ball)

​ |--object3 (Blue Ball)

5.2 client/src/index.ts

import {
  AmbientLight,
  AxesHelper,
  BoxGeometry,
  Mesh,
  MeshNormalMaterial,
  MeshPhongMaterial,
  OrthographicCamera,
  PerspectiveCamera,
  PointLight,
  Scene,
  SphereBufferGeometry,
  Vector3,
  WebGLRenderer,
} from "three";

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

import { GUI } from "dat.gui";
import Stats from "three/examples/jsm/libs/stats.module";

const container = document.getElementById("container");
const scene = new Scene();
scene.add(new AxesHelper(5));

const camera = new PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.set(4, 4, 4);

const renderer = new WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(8, 0, 0);

const light1 = new PointLight();
light1.position.set(10, 10, 10);
scene.add(light1);

const light2 = new PointLight();
light2.position.set(-10, 10, 10);
scene.add(light2);

const object1 = new Mesh(
  new BoxGeometry(),
  new MeshPhongMaterial({ color: 0xff0000 })
);
object1.position.set(4, 0, 0);
scene.add(object1);
object1.add(new AxesHelper(5));

const object2 = new Mesh(
  new BoxGeometry(),
  new MeshPhongMaterial({ color: 0x00ff00 })
);
object2.position.set(4, 0, 0);
object1.add(object2);
object2.add(new AxesHelper(5));

const object3 = new Mesh(
  new BoxGeometry(),
  new MeshPhongMaterial({ color: 0x0000ff })
);
object3.position.set(4, 0, 0);
object2.add(object3);
object3.add(new AxesHelper(5));

window.addEventListener("resize", onWindowResize, false);
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  render();
}

const gui = new GUI();
const object1Folder = gui.addFolder("Object1");
object1Folder.add(object1.position, "x", 0, 10, 0.01).name("X Position");
object1Folder
  .add(object1.rotation, "x", 0, Math.PI * 2, 0.01)
  .name("X Rotation");
object1Folder.add(object1.scale, "x", 0, 2, 0.01).name("X Scale");
object1Folder.open();
const object2Folder = gui.addFolder("Object2");
object2Folder.add(object2.position, "x", 0, 10, 0.01).name("X Position");
object2Folder
  .add(object2.rotation, "x", 0, Math.PI * 2, 0.01)
  .name("X Rotation");
object2Folder.add(object2.scale, "x", 0, 2, 0.01).name("X Scale");
object2Folder.open();
const object3Folder = gui.addFolder("Object3");
object3Folder.add(object3.position, "x", 0, 10, 0.01).name("X Position");
object3Folder
  .add(object3.rotation, "x", 0, Math.PI * 2, 0.01)
  .name("X Rotation");
object3Folder.add(object3.scale, "x", 0, 2, 0.01).name("X Scale");
object3Folder.open();

const stats = Stats();
container.appendChild(stats.dom);

const debug = document.getElementById("debug1") as HTMLDivElement;

function animate() {
  requestAnimationFrame(animate);
  controls.update();
  render();
  const object1WorldPosition = new Vector3();
  object1.getWorldPosition(object1WorldPosition);
  const object2WorldPosition = new Vector3();
  object2.getWorldPosition(object2WorldPosition);
  const object3WorldPosition = new Vector3();
  object3.getWorldPosition(object3WorldPosition);
  debug.innerText = `Red
    Local Pos X : ${object1.position.x.toFixed(2)}
    World Pos X : ${object1WorldPosition.x.toFixed(2)}}

    Green
    Local Pos X : ${object2.position.x.toFixed(2)}
    World Pos X : ${object2WorldPosition.x.toFixed(2)}

    Blue
    Local Pos X : ${object3.position.x.toFixed(2)}
    World Pos X : ${object3WorldPosition.x.toFixed(2)}
    `;
  stats.update();
}

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

animate();

object-hierarchy.gif

6. Geometries

import {
  AmbientLight,
  AxesHelper,
  BoxGeometry,
  IcosahedronGeometry,
  Mesh,
  MeshBasicMaterial,
  MeshNormalMaterial,
  MeshPhongMaterial,
  OrthographicCamera,
  PerspectiveCamera,
  PointLight,
  Scene,
  SphereBufferGeometry,
  SphereGeometry,
  Vector3,
  WebGLRenderer,
} from "three";

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

import { GUI } from "dat.gui";
import Stats from "three/examples/jsm/libs/stats.module";

const container = document.getElementById("container");

const scene = new Scene();
scene.add(new AxesHelper(5));

const camera = new PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.x = -2;
camera.position.y = 4;
camera.position.z = 5;

const renderer = new WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

new OrbitControls(camera, renderer.domElement);

const boxGeometry = new BoxGeometry();
const sphereGeometry = new SphereGeometry();
const icosahedronGeometry = new IcosahedronGeometry();

const material = new MeshBasicMaterial({
  color: 0x00ff00,
  wireframe: true,
});

const cube = new Mesh(boxGeometry, material);
cube.position.x = 3;
scene.add(cube);

const sphere = new Mesh(sphereGeometry, material);
sphere.position.x = -3;
scene.add(sphere);

const icosahedron = new Mesh(icosahedronGeometry, material);
scene.add(icosahedron);

window.addEventListener("resize", onWindowResize, false);
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  render();
}

const stats = Stats();
document.body.appendChild(stats.dom);

const gui = new GUI();
const cubeFolder = gui.addFolder("Cube");
const cubeRotationFolder = cubeFolder.addFolder("Rotation");
cubeRotationFolder.add(cube.rotation, "x", 0, Math.PI * 2, 0.01);
cubeRotationFolder.add(cube.rotation, "y", 0, Math.PI * 2, 0.01);
cubeRotationFolder.add(cube.rotation, "z", 0, Math.PI * 2, 0.01);
const cubePositionFolder = cubeFolder.addFolder("Position");
cubePositionFolder.add(cube.position, "x", -10, 10);
cubePositionFolder.add(cube.position, "y", -10, 10);
cubePositionFolder.add(cube.position, "z", -10, 10);
const cubeScaleFolder = cubeFolder.addFolder("Scale");
cubeScaleFolder
  .add(cube.scale, "x", -5, 5, 0.1)
  .onFinishChange(() => console.dir(cube.geometry));
cubeScaleFolder.add(cube.scale, "y", -5, 5, 0.1);
cubeScaleFolder.add(cube.scale, "z", -5, 5, 0.1);
cubeFolder.add(cube, "visible", true);
cubeFolder.open();

const cubeData = {
  width: 1,
  height: 1,
  depth: 1,
  widthSegments: 1,
  heightSegments: 1,
  depthSegments: 1,
};
const cubePropertiesFolder = cubeFolder.addFolder("Properties");
cubePropertiesFolder
  .add(cubeData, "width", 1, 30)
  .onChange(regenerateBoxGeometry)
  .onFinishChange(() => console.dir(cube.geometry));
cubePropertiesFolder
  .add(cubeData, "height", 1, 30)
  .onChange(regenerateBoxGeometry);
cubePropertiesFolder
  .add(cubeData, "depth", 1, 30)
  .onChange(regenerateBoxGeometry);
cubePropertiesFolder
  .add(cubeData, "widthSegments", 1, 30)
  .onChange(regenerateBoxGeometry);
cubePropertiesFolder
  .add(cubeData, "heightSegments", 1, 30)
  .onChange(regenerateBoxGeometry);
cubePropertiesFolder
  .add(cubeData, "depthSegments", 1, 30)
  .onChange(regenerateBoxGeometry);

function regenerateBoxGeometry() {
  const newGeometry = new BoxGeometry(
    cubeData.width,
    cubeData.height,
    cubeData.depth,
    cubeData.widthSegments,
    cubeData.heightSegments,
    cubeData.depthSegments
  );
  cube.geometry.dispose();
  cube.geometry = newGeometry;
}

const sphereData = {
  radius: 1,
  widthSegments: 8,
  heightSegments: 6,
  phiStart: 0,
  phiLength: Math.PI * 2,
  thetaStart: 0,
  thetaLength: Math.PI,
};
const sphereFolder = gui.addFolder("Sphere");
const spherePropertiesFolder = sphereFolder.addFolder("Properties");
spherePropertiesFolder
  .add(sphereData, "radius", 0.1, 30)
  .onChange(regenerateSphereGeometry);
spherePropertiesFolder
  .add(sphereData, "widthSegments", 1, 32)
  .onChange(regenerateSphereGeometry);
spherePropertiesFolder
  .add(sphereData, "heightSegments", 1, 16)
  .onChange(regenerateSphereGeometry);
spherePropertiesFolder
  .add(sphereData, "phiStart", 0, Math.PI * 2)
  .onChange(regenerateSphereGeometry);
spherePropertiesFolder
  .add(sphereData, "phiLength", 0, Math.PI * 2)
  .onChange(regenerateSphereGeometry);
spherePropertiesFolder
  .add(sphereData, "thetaStart", 0, Math.PI)
  .onChange(regenerateSphereGeometry);
spherePropertiesFolder
  .add(sphereData, "thetaLength", 0, Math.PI)
  .onChange(regenerateSphereGeometry);

function regenerateSphereGeometry() {
  const newGeometry = new SphereGeometry(
    sphereData.radius,
    sphereData.widthSegments,
    sphereData.heightSegments,
    sphereData.phiStart,
    sphereData.phiLength,
    sphereData.thetaStart,
    sphereData.thetaLength
  );
  sphere.geometry.dispose();
  sphere.geometry = newGeometry;
}

const icosahedronData = {
  radius: 1,
  detail: 0,
};
const icosahedronFolder = gui.addFolder("Icosahedron");
const icosahedronPropertiesFolder = icosahedronFolder.addFolder("Properties");
icosahedronPropertiesFolder
  .add(icosahedronData, "radius", 0.1, 10)
  .onChange(regenerateIcosahedronGeometry);
icosahedronPropertiesFolder
  .add(icosahedronData, "detail", 0, 5)
  .step(1)
  .onChange(regenerateIcosahedronGeometry);

function regenerateIcosahedronGeometry() {
  const newGeometry = new IcosahedronGeometry(
    icosahedronData.radius,
    icosahedronData.detail
  );
  icosahedron.geometry.dispose();
  icosahedron.geometry = newGeometry;
}

const debug = document.getElementById("debug1") as HTMLDivElement;

function animate() {
  requestAnimationFrame(animate);

  render();

  debug.innerText = `Matrix
  ${cube.matrix.elements.toString().replace(/,/g, "\n")}
  `;

  stats.update();
}

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

animate();

6.1 BufferGeometry

  • 画线
const points = [];
points.push(new Vector3(-1, 0, 0));
points.push(new Vector3(1, 0, 0));

const geometry = new BufferGeometry();
geometry.setFromPoints(points);
geometry.computeVertexNormals();
const line = new Line(geometry, new LineBasicMaterial({ color: 0x888888 }));
scene.add(line);

7. Materials

. OrbitControls

通过鼠标可以控制物体(滚轮)缩放,(右键)移动,(左键)旋转

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
new OrbitControls(camera, renderer.domElement);

OrbitControls-2.gif

简单动画

  • 方案一: 时间有延迟情况
setInterval(() => {
  cube.rotation.z += 0.01;
  renderer.render(scene, camera);
}, 1000 / 60);
  • 方案二: 不同的设备的刷新率可能不一样导致效果有差异
function tick() {
  cube.rotation.z += 0.01;
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();
  • 方案三:设置一个频率和刷新率没关系的(比较不错的方法)
let time = Date.now();
function tick() {
  const currentTime = Date.now();
  const deltaTime = currentTime - time;
  time = currentTime;
  console.log(deltaTime); // 基本都是在 16 ,17 范围
  cube.rotation.z += deltaTime * 0.001;

  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();
  • 方案四:threejs 提供的 THREE.Clock()
const clock = new THREE.Clock();
function tick() {
  const time = clock.getDelta();
  cube.rotation.z += time;
  console.log(time);

  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();
  • 方案五:threejs 提供的 THREE.Clock()
const clock = new THREE.Clock();
function tick() {
  const time = clock.getElapsedTime();
  cube.rotation.z = time;
  console.log(time);

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

案例一

一堆立方体动画

const domContainer = document.querySelector(".container");
// 获得容器的高度,宽度
const { width, height } = domContainer.getBoundingClientRect();

// 创建场景
const scene = new THREE.Scene();

const objects = [];
function createObject() {
  const size = Math.random();
  // 几何(骨架)
  const geometry = new THREE.BoxGeometry(size, size, size);
  // 材质(皮肤)
  const material = new THREE.MeshBasicMaterial({
    color: 0xffffffff * Math.random(),
  });
  // 骨架和皮肤构成模型,密密的网格就构成了物体
  const cube = new THREE.Mesh(geometry, material);

  cube.position.x = (Math.random() - 0.5) * 4; // -2 ~ 2
  cube.position.y = (Math.random() - 0.5) * 4; // -2 ~ 2
  cube.position.z = (Math.random() - 0.5) * 4; // -2 ~ 2

  objects.push(cube);
  scene.add(cube);
}

let n = 15;
for (let i = 0; i < n; i++) {
  createObject();
}

// 灯光
const light = new THREE.AmbientLight(0xffffffff, 1);
scene.add(light);

// 相机(视角,宽/高比,最近能看到的距离,最远能看到的距离)
const camera = new THREE.PerspectiveCamera(45, width / height, 1, 500);
// 调整相机所在的位置
camera.position.set(5, 5, 5);
// 调整相机的镜头对准的位置
camera.lookAt(0, 0, 0);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });

// 设置画布大小
renderer.setSize(width, height);
// 把场景和相机融合到渲染器中
renderer.render(scene, camera);

// 把画布添加到页面的容器中
domContainer.appendChild(renderer.domElement);

const clock = new THREE.Clock();
function tick() {
  const time = clock.getElapsedTime();

  objects.forEach((cube, i) => {
    cube.rotation.x = time + i;
    cube.rotation.y = time + i;
  });

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

案例二

  • 基础车
const domContainer = document.querySelector(".container");
const { width, height } = domContainer.getBoundingClientRect();

const scene = new THREE.Scene();

// 车
const car = new THREE.Group();

// 车身
const body = new THREE.Group();

// 车地盘
const chassis = new THREE.Mesh(
  new THREE.BoxGeometry(1, 2, 0.5),
  new THREE.MeshNormalMaterial()
);

// 车上的人
const person = new THREE.Mesh(
  new THREE.BoxGeometry(0.5, 0.5, 0.5),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
person.position.z = 0.5;
body.add(person);
body.add(chassis);
car.add(body);

// 左前轮轮毂
const leftFrontWheel = new THREE.Group();
const wheel1 = new THREE.Mesh(
  new BoxGeometry(0.1, 0.4, 0.4),
  new THREE.MeshNormalMaterial()
);
leftFrontWheel.position.set(-0.7, 0.6, 0);
leftFrontWheel.add(wheel1);
car.add(leftFrontWheel);
// 右前轮轮毂
const rigthFrontWheel = new THREE.Group();
const wheel2 = new THREE.Mesh(
  new BoxGeometry(0.1, 0.4, 0.4),
  new THREE.MeshNormalMaterial()
);
rigthFrontWheel.position.set(0.7, 0.6, 0);
rigthFrontWheel.add(wheel2);
car.add(rigthFrontWheel);
// 左后轮轮毂
const leftBackWheel = leftFrontWheel.clone();
leftBackWheel.position.y = -0.6;
car.add(leftBackWheel);
// 右后轮轮毂
const rightBackWheel = rigthFrontWheel.clone();
rightBackWheel.position.y = -0.6;
car.add(rightBackWheel);

// 轮子轮胎
const circle = new THREE.Group();
let n = 20;
for (let i = 0; i < n; i++) {
  let r = 0.5;
  const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
  const material = new THREE.MeshNormalMaterial();
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.x = r * Math.cos(((Math.PI * 2) / n) * i);
  mesh.position.y = r * Math.sin(((Math.PI * 2) / n) * i);
  circle.add(mesh);
}
circle.rotation.y = -(1 / 2) * Math.PI;
scene.add(circle);

leftFrontWheel.add(circle);
rigthFrontWheel.add(circle.clone());
leftBackWheel.add(circle.clone());
rightBackWheel.add(circle.clone());
scene.add(car);

const light = new THREE.AmbientLight(0xffffffff, 1);
scene.add(light);

const camera = new THREE.PerspectiveCamera(45, width / height, 1, 500);
camera.position.set(-1, 0, 5);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true });

renderer.setSize(width, height);
renderer.render(scene, camera);

domContainer.appendChild(renderer.domElement);

const clock = new THREE.Clock();
function tick() {
  const time = clock.getElapsedTime();

  //#region
  leftFrontWheel.rotation.x = -time;
  rigthFrontWheel.rotation.x = -time;
  leftBackWheel.rotation.x = -time;
  rightBackWheel.rotation.x = -time;

  car.position.y = (time % 2) - 1; // Math.sin(time) * 2
  //#endregion

  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();
  • 进阶车
const domContainer = document.querySelector(".container");
const { width, height } = domContainer.getBoundingClientRect();

const scene = new THREE.Scene();

//#region
const material = new THREE.MeshNormalMaterial();
const car = new THREE.Group();
const frontWheels = new THREE.Group();
const wheel1 = new THREE.Group();

// 轮胎
const wheelGeometry = new THREE.TorusGeometry(0.5, 0.1, 10, 120);
const wheelMesh = new THREE.Mesh(wheelGeometry, material);

// 轮轴
const n = 10;
for (let i = 0; i < n; i++) {
  const geomerty = new THREE.CylinderGeometry(0.03, 0.03, 1);
  const mesh = new THREE.Mesh(geomerty, material);
  mesh.rotation.z = ((Math.PI * 2) / n) * i;
  wheel1.add(mesh);
}
const len = 2;
// 轮横桥
const cylinderGeometry = new THREE.CylinderGeometry(0.05, 0.05, len);
const cylinder = new THREE.Mesh(cylinderGeometry, material);
cylinder.rotation.x = -0.5 * Math.PI;
wheel1.position.z = -len / 2;
wheel1.add(wheelMesh);

const wheel2 = wheel1.clone();
wheel2.position.z = len / 2;
frontWheels.rotation.y = 0.5 * Math.PI;
frontWheels.position.y = -1;
frontWheels.add(wheel1, cylinder, wheel2);

const backWheels = frontWheels.clone();
backWheels.position.y = 1;

const body = new THREE.Group();
const cubeGeometry = new THREE.BoxGeometry(1.4, 3.4, 0.6);
const cube = new THREE.Mesh(cubeGeometry, material);
const roofGeometry = new THREE.CylinderGeometry(
  1,
  1,
  1.4,
  3,
  1,
  false,
  -Math.PI / 2,
  Math.PI
);
const roof = new THREE.Mesh(roofGeometry, material);
roof.rotation.z = Math.PI / 2;
body.add(cube, roof);
car.add(frontWheels, backWheels, body);
car.rotation.x = (-1 / 2) * Math.PI;
car.position.y = 0.6;
scene.add(car);

const planGeometry = new THREE.PlaneGeometry(5, 6);
const plane = new THREE.Mesh(
  planGeometry,
  new MeshBasicMaterial({ color: 0xcccccc })
);
plane.rotation.x = (-1 / 2) * Math.PI;
scene.add(plane);
//#endregion

const light = new THREE.AmbientLight(0xffffffff, 1);
scene.add(light);

const camera = new THREE.PerspectiveCamera(45, width / height, 1, 500);
camera.position.set(-3, 2, 5);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({ antialias: true });

renderer.setSize(width, height);
renderer.render(scene, camera);

domContainer.appendChild(renderer.domElement);

const clock = new THREE.Clock();
function tick() {
  const time = clock.getElapsedTime();

  //#region
  backWheels.rotation.x = time;
  frontWheels.rotation.x = time;
  car.position.z = (time % 2) - 1;
  //#endregion

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

案例三

VR 沉浸式看房