前言
就像一个在一个房间里面,放入家具,灯什么的 房间:就像是场景(3D 容器) scene 家具(电视等):物体,就像是各种模型 model (mesh = geometry(几何,就像灯笼的骨架) + material(材质,就像灯笼的皮肤)) 灯:就像是台灯、太阳光(自然光)light 相机: 可以拍照的东西,镜头对准房间才看到房间的东西 camera 渲染器:和计算机打交道,会知道页面供人看到的效果 rederer
Three.js 程序组成
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();
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 ...
}
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);
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();
6. Geometries
- BoxGeometry
- CircleGeometry
- CylinderGeometry
- ConeGeometry
- EdgesGeometry
- ExtrudeGeometry
- ShapeGeometry
- LatheGeometry
- PlaneGeometry
- RingGeometry
- SphereGeometry
- TorusGeometry
- TorusKnotGeometry
- TubeGeometry
- PolyhedronGeometry
- DodecahedronGeometry
- IcosahedronGeometry
- OctahedronGeometry
- TetrahedronGeometry
- WireframeGeometry
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
- LineBasicMaterial
- LineDashedMaterial
- MeshBasicMaterial
- MeshDepthMaterial
- MeshDistanceMaterial
- MeshLambertMaterial
- MeshMatcapMaterial
- MeshNormalMaterial
- MeshPhongMaterial
- MeshPhysicalMaterial
- MeshStandardMaterial
- MeshToonMaterial
- PointsMaterial
- RawShaderMaterial
- ShaderMaterial
- ShadowMaterial
- SpriteMaterial
. OrbitControls
通过鼠标可以控制物体(滚轮)缩放,(右键)移动,(左键)旋转
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
new OrbitControls(camera, renderer.domElement);
简单动画
- 方案一: 时间有延迟情况
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();