Three.js - 渲染器与响应式设计
Three.js - 场景、渲染器与响应式布局
Section titled “Three.js - 场景、渲染器与响应式布局”场景图:组织你的世界
Section titled “场景图:组织你的世界”THREE.Scene 对象是称为场景图(Scene Graph)的根结构。你可以将其想象成一棵树,场景是树干,而你添加的每个对象——网格(meshes)、光源(lights)、相机(cameras),甚至是其他对象组——都是树枝。这种层级结构是3D引擎组织和渲染内容的基础。
添加和移除对象
Section titled “添加和移除对象”你可以使用 .add() 方法将任何继承自 THREE.Object3D 的对象添加到场景中,并使用 .remove() 方法将其移除。
const scene = new THREE.Scene();const mesh = new THREE.Mesh(/* ... */);
// 将网格添加到场景中scene.add(mesh);
// 场景的 children 属性是一个包含所有顶层对象的数组console.log(scene.children.length); // Output: 1
// 从场景中移除网格scene.remove(mesh);console.log(scene.children.length); // Output: 0重要提示:内存管理与销毁
Section titled “重要提示:内存管理与销毁”调用 scene.remove(object) 仅仅是从场景图中断开对象的连接。它不会释放其几何体(geometry)、材质(material)或纹理(textures)所占用的内存。在频繁添加和移除对象的应用中,这可能导致内存泄漏。要正确清理,你必须对这些资源调用 .dispose() 方法。
function disposeObject(object) { // 遍历对象及其子对象 object.traverse(child => { if (child.isMesh) { // 销毁几何体和材质 if (child.geometry) { child.geometry.dispose(); } if (child.material) { // 如果材质是一个数组,则销毁其中的每个材质 if (Array.isArray(child.material)) { child.material.forEach(material => material.dispose()); } else { child.material.dispose(); } } } }); // 最后,将对象从场景中移除 scene.remove(object);}
// 用法:disposeObject(myMeshGroup);渲染器:让你的场景栩栩如生
Section titled “渲染器:让你的场景栩栩如生”渲染器(Renderer)是一个组件,它接收你的 Scene 和 Camera,并将结果绘制到网页上的 <canvas> 元素中。最强大且常用的渲染器是 THREE.WebGLRenderer,它利用 WebGL API 调用用户的 GPU 来实现高性能渲染。
虽然存在其他渲染器(如 CSS2DRenderer 和 CSS3DRenderer),但它们是专用工具,通常用于在 WebGL 画布之上叠加基于HTML的用户界面元素,而不是用于渲染主要的3D场景。
实现响应式布局
Section titled “实现响应式布局”3D场景必须像普通网站一样适应浏览器窗口的大小。这通过监听窗口的 resize 事件并相应地更新相机和渲染器来实现。
window.addEventListener('resize', () => { // 1. 更新画布和视口大小 const width = window.innerWidth; const height = window.innerHeight;
// 2. 更新相机的宽高比 camera.aspect = width / height; camera.updateProjectionMatrix(); // 在修改 'aspect' 等属性后必须调用此方法
// 3. 更新渲染器大小 renderer.setSize(width, height);
// 4. 调整高DPI显示器(例如Retina屏幕)的像素比例 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));});updateProjectionMatrix() 调用至关重要;它会通知相机根据新的宽高比重新计算其透视效果。
示例:场景管理与响应式布局
Section titled “示例:场景管理与响应式布局”此示例演示了如何动态地从场景中添加和移除对象。它采用了简洁现代的结构,并包含了必要的响应式逻辑。
index.html
Section titled “index.html”<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Three.js - 场景管理</title> <style> body { margin: 0; } canvas.webgl { display: block; } .ui-controls { position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.5); padding: 10px; border-radius: 5px; } button { margin: 5px; padding: 8px 12px; cursor: pointer; } </style></head><body> <div class="ui-controls"> <button id="add-cube">添加立方体</button> <button id="remove-cube">移除最后一个立方体</button> </div> <canvas class="webgl"></canvas> <script type="module" src="/main.js"></script></body></html>main.js
Section titled “main.js”import * as THREE from 'three';import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
// --- 核心设置 ---const scene = new THREE.Scene();const canvas = document.querySelector('canvas.webgl');const sizes = { width: window.innerWidth, height: window.innerHeight };
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);camera.position.set(10, 10, 15);scene.add(camera);
const renderer = new THREE.WebGLRenderer({ canvas });renderer.setSize(sizes.width, sizes.height);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const controls = new OrbitControls(camera, canvas);controls.enableDamping = true;
// --- 光照与环境 ---const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);scene.add(ambientLight);const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);directionalLight.position.set(5, 5, 5);scene.add(directionalLight);
// --- 场景操作逻辑 ---let cubeCounter = 0;
document.getElementById('add-cube').addEventListener('click', () => { const geometry = new THREE.BoxGeometry(2, 2, 2); const material = new THREE.MeshStandardMaterial({ color: new THREE.Color(Math.random(), Math.random(), Math.random()) }); const cube = new THREE.Mesh(geometry, material);
// 赋予它一个唯一的名称以便识别 cube.name = `cube-${cubeCounter++}`;
// 随机定位 cube.position.set( (Math.random() - 0.5) * 20, (Math.random()) * 5 + 1, (Math.random() - 0.5) * 20 );
scene.add(cube); console.log(`Added ${cube.name}. Total objects in scene: ${scene.children.length}`);});
document.getElementById('remove-cube').addEventListener('click', () => { // 查找最后一个添加的立方体 const lastCube = scene.children.find(obj => obj.name.startsWith('cube-'));
if (lastCube) { console.log(`Removing ${lastCube.name}`);
// 正确的清理 lastCube.geometry.dispose(); lastCube.material.dispose(); scene.remove(lastCube);
console.log(`Removed cube. Total objects in scene: ${scene.children.length}`); } else { console.log('No more cubes to remove.'); }});
// --- 动画循环 ---const tick = () => { controls.update(); renderer.render(scene, camera); window.requestAnimationFrame(tick);};tick();
// --- 响应式处理 ---window.addEventListener('resize', () => { sizes.width = window.innerWidth; sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height; camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));});