当前位置:首页 > 学习笔记 > 正文内容

Three.js WebGL 3D 网页开发入门教程

廖万里10小时前学习笔记7
WebGL 让浏览器拥有了原生 3D 渲染能力,而 Three.js 则是通往这个世界的最佳门票。本文将从零开始,带你掌握 WebGL 基础、Three.js 核心概念、几何体与材质、光照系统、动画循环、交互控制,最终完成一个可交互的 3D 场景实战项目。

Three.js WebGL 3D 开发

一、WebGL 基础:浏览器 3D 渲染的基石

WebGL(Web Graphics Library)是一种在浏览器中无需插件即可渲染 3D 图形的 JavaScript API。它基于 OpenGL ES 2.0,直接与 GPU 通信,实现硬件加速的图形渲染。

1.1 WebGL 的核心优势

传统网页渲染依赖 CPU,而 WebGL 将计算任务交给 GPU,性能提升数十倍。这意味着:

  • 流畅的 3D 动画:每秒 60 帧以上的渲染性能
  • 复杂的视觉效果:粒子系统、后处理、物理模拟
  • 跨平台兼容:桌面、移动端、VR 设备统一支持

1.2 为什么需要 Three.js

原生 WebGL 编程极其繁琐——画一个三角形需要上百行着色器代码。Three.js 封装了底层细节,提供友好的 API,让你专注于创意而非底层实现。

// 原生 WebGL:画一个三角形需要这么多代码
const vertexShaderSource = `
    attribute vec4 aPosition;
    void main() {
        gl_Position = aPosition;
    }
`;
const fragmentShaderSource = `
    precision mediump float;
    void main() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
`;
// 还需要编译着色器、创建缓冲区、绑定属性...

// Three.js:三行代码搞定
const geometry = new THREE.Triangle();
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);

二、Three.js 核心概念:场景、相机、渲染器

Three.js 的三大支柱是场景(Scene)、相机(Camera)和渲染器(Renderer)。理解它们的协作关系,是掌握 Three.js 的第一步。

2.1 场景(Scene):3D 世界的容器

场景是所有 3D 对象的容器,包括几何体、灯光、相机等。你可以把它想象成一个虚拟的舞台。

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

// 设置背景色
scene.background = new THREE.Color(0x1a1a2e);

// 添加雾效(增加深度感)
scene.fog = new THREE.Fog(0x1a1a2e, 10, 50);

2.2 相机(Camera):观察世界的眼睛

相机决定了我们从哪个角度、以什么视野观察场景。最常用的是透视相机(PerspectiveCamera),模拟人眼的视觉效果。

// 创建透视相机
const camera = new THREE.PerspectiveCamera(
    75,                                     // 视野角度(FOV)
    window.innerWidth / window.innerHeight, // 宽高比
    0.1,                                    // 近裁剪面
    1000                                    // 远裁剪面
);

// 设置相机位置
camera.position.set(0, 5, 10);

// 让相机看向原点
camera.lookAt(0, 0, 0);

透视相机的四个参数决定了成像效果:

  • FOV:视野角度,越大看到越多,但物体会变小
  • Aspect Ratio:宽高比,通常与画布一致
  • Near Plane:近裁剪面,比这更近的物体不渲染
  • Far Plane:远裁剪面,比这更远的物体不渲染

2.3 渲染器(Renderer):将 3D 绘制到 2D

渲染器负责将 3D 场景投影到 2D 画布上。WebGLRenderer 是最常用的渲染器。

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
    antialias: true,    // 开启抗锯齿
    alpha: true         // 允许透明背景
});

// 设置渲染尺寸
renderer.setSize(window.innerWidth, window.innerHeight);

// 设置像素比(适配高分屏)
renderer.setPixelRatio(window.devicePixelRatio);

// 开启阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// 将画布添加到页面
document.body.appendChild(renderer.domElement);

三、几何体与材质:构建 3D 物体

每个 3D 物体由几何体(Geometry)和材质(Material)组成。几何体定义形状,材质定义外观。

3.1 内置几何体

Three.js 提供了丰富的内置几何体,满足大部分需求:

// 基础几何体
const box = new THREE.BoxGeometry(1, 1, 1);        // 立方体
const sphere = new THREE.SphereGeometry(0.5, 32, 32);  // 球体
const cylinder = new THREE.CylinderGeometry(0.5, 0.5, 1, 32); // 圆柱体
const cone = new THREE.ConeGeometry(0.5, 1, 32);  // 圆锥体
const torus = new THREE.TorusGeometry(0.5, 0.2, 16, 100); // 圆环

// 平面几何体
const plane = new THREE.PlaneGeometry(10, 10);    // 平面

// 自定义几何体(通过顶点)
const vertices = new Float32Array([
    0, 1, 0,    // 顶点 1
    -1, -1, 0,  // 顶点 2
    1, -1, 0    // 顶点 3
]);
const customGeometry = new THREE.BufferGeometry();
customGeometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));

3.2 材质系统

材质决定了物体如何与光照交互:

// 基础材质(不受光照影响)
const basicMaterial = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    wireframe: true  // 线框模式
});

// 标准材质(PBR 物理渲染)
const standardMaterial = new THREE.MeshStandardMaterial({
    color: 0xff6b6b,
    metalness: 0.5,    // 金属度
    roughness: 0.3,    // 粗糙度
    envMapIntensity: 1  // 环境贴图强度
});

// 物理材质(更真实的 PBR)
const physicalMaterial = new THREE.MeshPhysicalMaterial({
    color: 0x4ecdc4,
    metalness: 0.9,
    roughness: 0.1,
    clearcoat: 1.0,     // 清漆层
    clearcoatRoughness: 0.1
});

// Lambert 材质(漫反射)
const lambertMaterial = new THREE.MeshLambertMaterial({
    color: 0x00d4ff,
    emissive: 0x001122  // 自发光
});

// Phong 材质(镜面高光)
const phongMaterial = new THREE.MeshPhongMaterial({
    color: 0xffcc00,
    shininess: 100,     // 高光强度
    specular: 0xffffff  // 高光颜色
});

3.3 网格(Mesh):几何体 + 材质

网格是将几何体和材质组合后的可渲染对象:

// 创建网格
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshStandardMaterial({ color: 0x00d4ff });
const cube = new THREE.Mesh(geometry, material);

// 设置位置、旋转、缩放
cube.position.set(0, 1, 0);
cube.rotation.y = Math.PI / 4;
cube.scale.set(1, 1, 1);

// 开启阴影
cube.castShadow = true;      // 投射阴影
cube.receiveShadow = true;   // 接收阴影

// 添加到场景
scene.add(cube);

四、光照系统:让场景生动起来

没有光照,3D 场景就是一片黑暗。Three.js 提供多种光源类型,模拟真实世界的光照效果。

4.1 光源类型

// 环境光(均匀照亮所有物体)
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);

// 平行光(模拟太阳光)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 7);
directionalLight.castShadow = true;  // 开启阴影
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);

// 点光源(像灯泡)
const pointLight = new THREE.PointLight(0xff6b6b, 1, 100);
pointLight.position.set(0, 5, 0);
scene.add(pointLight);

// 聚光灯(像手电筒)
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(0, 10, 0);
spotLight.angle = Math.PI / 6;        // 光锥角度
spotLight.penumbra = 0.5;             // 边缘柔和度
spotLight.castShadow = true;
scene.add(spotLight);

// 半球光(天空光 + 地面反射)
const hemisphereLight = new THREE.HemisphereLight(
    0x87ceeb,  // 天空颜色
    0x8b4513,  // 地面颜色
    0.6
);
scene.add(hemisphereLight);

4.2 阴影设置

真实的阴影能极大提升场景真实感:

// 渲染器开启阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// 光源投射阴影
directionalLight.castShadow = true;
directionalLight.shadow.camera.near = 0.1;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;

// 物体投射和接收阴影
cube.castShadow = true;
ground.receiveShadow = true;

五、动画循环:让世界动起来

动画的本质是每帧更新物体状态,然后重新渲染。Three.js 使用 requestAnimationFrame 实现流畅的动画循环。

5.1 基础动画循环

// 动画循环函数
function animate() {
    // 请求下一帧
    requestAnimationFrame(animate);
    
    // 更新物体状态
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
    
    // 渲染场景
    renderer.render(scene, camera);
}

// 启动动画
animate();

5.2 使用 Clock 实现时间动画

const clock = new THREE.Clock();

function animate() {
    requestAnimationFrame(animate);
    
    // 获取经过的时间
    const elapsedTime = clock.getElapsedTime();
    
    // 使用时间驱动动画
    cube.position.y = Math.sin(elapsedTime) * 2;
    cube.rotation.y = elapsedTime * 0.5;
    
    renderer.render(scene, camera);
}

5.3 高级动画:GSAP 和 Tween

对于复杂的动画,可以使用 GSAP 或 Tween.js:

// 使用 GSAP
import gsap from 'gsap';

gsap.to(cube.position, {
    duration: 2,
    x: 5,
    y: 3,
    ease: 'power2.inOut',
    repeat: -1,      // 无限循环
    yoyo: true       // 来回播放
});

gsap.to(cube.rotation, {
    duration: 4,
    y: Math.PI * 2,
    ease: 'none',
    repeat: -1
});

六、交互控制:与 3D 世界互动

用户交互是现代 3D 应用的核心。Three.js 提供了多种控制器和射线检测功能。

6.1 轨道控制器(OrbitControls)

轨道控制器让用户可以旋转、缩放、平移场景:

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);

// 配置
controls.enableDamping = true;        // 开启阻尼效果
controls.dampingFactor = 0.05;        // 阻尼系数
controls.minDistance = 2;             // 最小缩放距离
controls.maxDistance = 50;            // 最大缩放距离
controls.maxPolarAngle = Math.PI / 2; // 限制垂直旋转角度

// 在动画循环中更新
function animate() {
    requestAnimationFrame(animate);
    controls.update();  // 更新控制器
    renderer.render(scene, camera);
}

6.2 射线检测(Raycaster)

射线检测用于判断鼠标是否点击了 3D 物体:

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// 监听鼠标点击
window.addEventListener('click', (event) => {
    // 将鼠标坐标转换为归一化设备坐标(-1 到 1)
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    
    // 从相机发射射线
    raycaster.setFromCamera(mouse, camera);
    
    // 检测与物体的交叉
    const intersects = raycaster.intersectObjects(scene.children);
    
    if (intersects.length > 0) {
        const clickedObject = intersects[0].object;
        // 改变点击物体的颜色
        clickedObject.material.color.setHex(Math.random() * 0xffffff);
    }
});

6.3 鼠标悬停效果

let hoveredObject = null;

window.addEventListener('mousemove', (event) => {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    
    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(scene.children);
    
    // 重置之前悬停物体
    if (hoveredObject) {
        hoveredObject.material.emissive.setHex(0x000000);
    }
    
    if (intersects.length > 0) {
        hoveredObject = intersects[0].object;
        hoveredObject.material.emissive.setHex(0x333333);
        document.body.style.cursor = 'pointer';
    } else {
        hoveredObject = null;
        document.body.style.cursor = 'default';
    }
});

七、实战案例:交互式 3D 展示场景

现在我们将前面学到的知识整合,创建一个完整的交互式 3D 场景:

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

// === 1. 初始化场景、相机、渲染器 ===
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);

const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
);
camera.position.set(0, 5, 10);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

// === 2. 添加光源 ===
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);

const pointLight = new THREE.PointLight(0x00d4ff, 0.5);
pointLight.position.set(-5, 3, 0);
scene.add(pointLight);

// === 3. 创建地面 ===
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({
    color: 0x2d2d44,
    roughness: 0.8
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);

// === 4. 创建多个几何体 ===
const geometries = [
    new THREE.BoxGeometry(1.5, 1.5, 1.5),
    new THREE.SphereGeometry(0.8, 32, 32),
    new THREE.ConeGeometry(0.8, 1.5, 32),
    new THREE.TorusGeometry(0.6, 0.25, 16, 100)
];

const materials = [
    new THREE.MeshStandardMaterial({ color: 0x00d4ff, metalness: 0.3, roughness: 0.4 }),
    new THREE.MeshStandardMaterial({ color: 0xff6b6b, metalness: 0.5, roughness: 0.3 }),
    new THREE.MeshStandardMaterial({ color: 0x4ecdc4, metalness: 0.7, roughness: 0.2 }),
    new THREE.MeshStandardMaterial({ color: 0xffcc00, metalness: 0.4, roughness: 0.5 })
];

const meshes = [];
const positions = [
    [-4, 0.75, 0],
    [-1.3, 0.8, 0],
    [1.3, 1, 0],
    [4, 0.85, 0]
];

geometries.forEach((geometry, i) => {
    const mesh = new THREE.Mesh(geometry, materials[i]);
    mesh.position.set(...positions[i]);
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    meshes.push(mesh);
    scene.add(mesh);
});

// === 5. 添加轨道控制器 ===
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 3;
controls.maxDistance = 30;
controls.maxPolarAngle = Math.PI / 2 - 0.1;

// === 6. 射线检测交互 ===
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedMesh = null;

window.addEventListener('click', (event) => {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    
    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(meshes);
    
    if (intersects.length > 0) {
        if (selectedMesh) {
            selectedMesh.material.emissive.setHex(0x000000);
        }
        selectedMesh = intersects[0].object;
        selectedMesh.material.emissive.setHex(0x222222);
    }
});

// === 7. 动画循环 ===
const clock = new THREE.Clock();

function animate() {
    requestAnimationFrame(animate);
    
    const time = clock.getElapsedTime();
    
    // 物体浮动动画
    meshes.forEach((mesh, i) => {
        mesh.position.y = positions[i][1] + Math.sin(time + i) * 0.2;
        mesh.rotation.y = time * 0.5 + i;
    });
    
    // 点光源环绕
    pointLight.position.x = Math.sin(time) * 5;
    pointLight.position.z = Math.cos(time) * 5;
    
    controls.update();
    renderer.render(scene, camera);
}

animate();

// === 8. 窗口大小自适应 ===
window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

八、总结

本文从 WebGL 基础出发,系统讲解了 Three.js 的核心概念和实战技能:

  • WebGL 基础:理解 GPU 渲染原理,认识 Three.js 的封装价值
  • 三大核心:场景是容器,相机是眼睛,渲染器是画笔
  • 几何体与材质:内置几何体快速建模,材质系统实现真实质感
  • 光照系统:多种光源类型,阴影让场景更真实
  • 动画循环:requestAnimationFrame 驱动,时间函数控制节奏
  • 交互控制:轨道控制器实现视角操控,射线检测实现点击交互

Three.js 的学习曲线相对平缓,但要精通需要大量实践。建议从简单几何体开始,逐步添加材质、光照、动画,最后实现复杂交互。官方文档和示例是最好的学习资源。

下一步可以探索:后处理效果(Bloom、SSAO)、物理引擎(Cannon.js)、粒子系统、GLTF 模型加载等进阶话题。3D 世界的大门已经打开,尽情创造吧!

本文链接:https://www.kkkliao.cn/?id=955 转载需授权!

分享到:

版权声明:本文由廖万里的博客发布,如需转载请注明出处。


发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。