1. 项目概述
作为一名长期奋战在前端3D可视化领域的老兵,最近用Three.js完整还原了一个真实教室场景。这个项目最让我兴奋的是,从黑板粉笔槽的木质纹理到窗帘随风摆动的物理效果,每个细节都力求真实。整个场景严格按照8×9×4米的标准教室尺寸构建,误差控制在10%以内,连课桌上的书本堆叠角度都参考了真实教室的随机分布模式。
这个3D教室不仅是个静态模型,更实现了7类交互功能:点击查看设备信息、控制窗帘开关、调节灯光亮度等。技术栈采用Vue3+Three.js的组合,通过模块化设计将场景元素拆分为20+独立组件。在解决实时时钟渲染、动态阴影优化等难题时,积累了不少值得分享的实战经验。
2. 场景构建核心技术解析
2.1 三维坐标系与场景初始化
在Three.js中创建场景时,坐标系设定是首要工作。教室的长宽高分别对应X/Y/Z轴,我习惯将地板中心设为原点(0,0,0)。初始化代码包含三个核心对象:
javascript复制const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0); // 浅灰背景色
const camera = new THREE.PerspectiveCamera(
75, // 视场角
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近裁面
1000 // 远裁面
);
camera.position.set(0, 2, 5); // 初始视角高度1.8米
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true; // 启用阴影
renderer.setSize(window.innerWidth, window.innerHeight);
关键细节:将renderer的physicallyCorrectLights设为true可以更真实模拟光照物理效果,但会显著增加性能开销,需要根据设备性能权衡。
2.2 建筑结构建模实践
2.2.1 参数化墙体构建
四面墙体的创建采用了参数化设计,通过配置对象动态生成:
javascript复制function createWall(options) {
const geometry = new THREE.PlaneGeometry(options.width, options.height);
const material = new THREE.MeshStandardMaterial({
map: loadTexture(options.textureUrl),
roughness: 0.7,
metalness: 0.1
});
const wall = new THREE.Mesh(geometry, material);
wall.receiveShadow = true;
wall.position.set(options.x, options.y, options.z);
wall.rotation.set(options.rotX, options.rotY, options.rotZ);
return wall;
}
// 示例:创建后墙
const backWall = createWall({
width: 8, height: 4,
x: 0, y: 2, z: -4.5,
rotX: 0, rotY: Math.PI, rotZ: 0,
textureUrl: '/textures/wall-concrete.jpg'
});
2.2.2 复合地板实现
地板需要同时满足视觉效果和物理交互需求:
- 基础层:使用PlaneGeometry创建10×10米平面(略大于教室尺寸)
- 纹理层:加载800×800mm地砖无缝贴图,设置repeat属性实现平铺
- 细节层:添加法线贴图增强立体感
- 碰撞层:为后续交互预留物理碰撞检测接口
javascript复制const floorGeometry = new THREE.PlaneGeometry(10, 10);
const floorMaterial = new THREE.MeshStandardMaterial({
map: textureLoader.load('/textures/floor-tiles.jpg'),
normalMap: textureLoader.load('/textures/floor-tiles-normal.jpg'),
roughness: 0.3,
metalness: 0.1
});
floorMaterial.map.wrapS = floorMaterial.map.wrapT = THREE.RepeatWrapping;
floorMaterial.map.repeat.set(12, 12); // 按实际尺寸计算平铺次数
3. 教学设备建模详解
3.1 黑板系统实现
黑板是教室的核心元素,由多个部件复合而成:
- 主体板面:使用PlaneGeometry,应用黑板专用纹理(含粉笔痕迹效果)
- 木质边框:四个BoxGeometry组合,通过UV映射实现木纹走向一致
- 粉笔槽:特殊设计的凹槽几何体,底部添加粉笔模型
- 电子屏:内嵌可切换内容的Mesh,支持视频播放功能
javascript复制// 黑板主体
const blackboardGeometry = new THREE.PlaneGeometry(5.5, 2);
const blackboardMaterial = new THREE.MeshStandardMaterial({
map: textureLoader.load('/textures/blackboard.jpg'),
roughness: 0.9,
metalness: 0
});
// 木质边框(顶部)
const topBorderGeometry = new THREE.BoxGeometry(5.7, 0.15, 0.1);
const topBorderMaterial = new THREE.MeshStandardMaterial({
map: textureLoader.load('/textures/wood-map.jpg'),
normalMap: textureLoader.load('/textures/wood-normal.jpg'),
roughness: 0.6
});
// UV调整确保木纹方向正确
topBorderGeometry.attributes.uv.array = [...]; // 具体UV坐标计算略
3.2 动态时钟实现方案
LED电子时钟需要实时显示当前时间,采用Canvas+Texture的方案:
javascript复制function createClock() {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 128;
const ctx = canvas.getContext('2d');
function updateClock() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const now = new Date();
const timeStr = now.toLocaleTimeString();
const dateStr = now.toLocaleDateString();
// 绘制时钟样式
ctx.fillStyle = '#00ff00';
ctx.font = 'bold 48px digital';
ctx.textAlign = 'center';
ctx.fillText(timeStr, canvas.width/2, 60);
ctx.font = '24px Arial';
ctx.fillText(dateStr, canvas.width/2, 100);
texture.needsUpdate = true;
}
const texture = new THREE.CanvasTexture(canvas);
setInterval(updateClock, 1000);
const geometry = new THREE.PlaneGeometry(1.2, 0.3);
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true
});
return new THREE.Mesh(geometry, material);
}
性能优化:使用requestAnimationFrame替代setInterval可以避免页面后台运行时的不必要更新。
4. 家具与环境元素
4.1 课桌椅系统设计
课桌椅采用实例化渲染优化性能,主要考虑点:
- 单套桌椅建模:使用BufferGeometry合并相似部件
- 位置算法:根据行列数计算坐标,添加随机偏移量
- LOD控制:距离远的模型使用简化几何体
javascript复制// 单套桌椅组合
function createDeskChairSet() {
const group = new THREE.Group();
// 桌面(圆角长方体)
const desktopGeometry = new THREE.RoundedBoxGeometry(0.6, 0.04, 0.4, 2, 0.02);
const desktop = new THREE.Mesh(desktopGeometry, woodMaterial);
// 桌腿(圆柱体)
const legGeometry = new THREE.CylinderGeometry(0.02, 0.02, 0.7, 8);
const legPositions = [
[0.25, -0.35, 0.15], [-0.25, -0.35, 0.15],
[0.25, -0.35, -0.15], [-0.25, -0.35, -0.15]
];
legPositions.forEach(pos => {
const leg = new THREE.Mesh(legGeometry, metalMaterial);
leg.position.set(...pos);
group.add(leg);
});
// 类似方法添加椅子...
return group;
}
// 批量生成4排桌椅
const rows = 4, cols = 6;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const set = createDeskChairSet();
set.position.set(
(c - cols/2) * 0.7 + Math.random()*0.1,
0,
r * 0.8 - 3
);
scene.add(set);
}
}
4.2 窗户与光照系统
右侧墙的三扇窗户需要实现:
- 物理正确的玻璃材质(折射率1.5)
- 可交互的窗帘系统
- 动态日光效果
javascript复制// 窗户玻璃材质
const glassMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
transmission: 0.9,
roughness: 0.1,
ior: 1.5,
thickness: 0.01,
envMap: envTexture
});
// 窗帘布料模拟
const curtainGeometry = new THREE.PlaneGeometry(1, 1.5, 20, 20);
const curtainMaterial = new THREE.MeshStandardMaterial({
map: textureLoader.load('/textures/curtain-fabric.jpg'),
side: THREE.DoubleSide
});
const curtain = new THREE.Mesh(curtainGeometry, curtainMaterial);
curtain.castShadow = true;
// 添加布料物理效果
const curtainCloth = new Cloth(curtain, {
gravity: [0, -0.1, 0],
pins: [/* 顶部固定点索引 */]
});
5. 交互系统实现
5.1 射线检测与对象拾取
通过Raycaster实现点击交互:
javascript复制const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseClick(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(interactiveObjects);
if (intersects.length > 0) {
const obj = intersects[0].object;
handleObjectClick(obj);
}
}
// 可交互对象需要提前添加到interactiveObjects数组
const interactiveObjects = [blackboard, screen, clock, ...lights];
5.2 灯光控制面板
实现灯光开关和亮度调节:
javascript复制class LightController {
constructor(light) {
this.light = light;
this.originalIntensity = light.intensity;
}
toggle() {
this.light.visible = !this.light.visible;
}
setIntensity(value) {
this.light.intensity = value * this.originalIntensity;
}
}
// 创建灯光控制实例
const lightControllers = lights.map(light => new LightController(light));
// 与UI界面绑定
document.getElementById('light-slider').addEventListener('input', (e) => {
lightControllers.forEach(ctrl => ctrl.setIntensity(e.target.value));
});
6. 性能优化实战经验
6.1 纹理加载策略
- 使用压缩纹理格式(KTX2)
- 实现纹理流式加载
- 共享材质实例
javascript复制const textureLoader = new THREE.TextureLoader().setPath('textures/');
const textureQueue = [
{ name: 'wood', url: 'wood.jpg' },
{ name: 'metal', url: 'metal.jpg' },
// ...其他纹理
];
const textures = {};
function loadTextures() {
textureQueue.forEach((item, index) => {
textureLoader.load(item.url, (tex) => {
textures[item.name] = tex;
updateProgress((index + 1) / textureQueue.length);
});
});
}
6.2 渲染性能监控
添加stats.js监控帧率:
javascript复制import Stats from 'stats.js';
const stats = new Stats();
stats.showPanel(0); // 0: fps, 1: ms, 2: mb
document.body.appendChild(stats.dom);
function animate() {
stats.begin();
// 渲染逻辑...
stats.end();
requestAnimationFrame(animate);
}
7. 移动端适配方案
7.1 响应式布局处理
javascript复制function handleResize() {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
// 根据屏幕尺寸调整控制参数
if (width < 768) {
controls.enablePan = false;
camera.position.z = 7;
} else {
controls.enablePan = true;
}
}
7.2 触摸事件处理
javascript复制const touchManager = {
startX: 0,
startY: 0,
handleTouchStart(e) {
this.startX = e.touches[0].clientX;
this.startY = e.touches[0].clientY;
},
handleTouchMove(e) {
const dx = e.touches[0].clientX - this.startX;
const dy = e.touches[0].clientY - this.startY;
// 转换为相机旋转
camera.rotation.y += dx * 0.01;
camera.rotation.x += dy * 0.01;
this.startX = e.touches[0].clientX;
this.startY = e.touches[0].clientY;
}
};
canvas.addEventListener('touchstart', touchManager.handleTouchStart.bind(touchManager));
canvas.addEventListener('touchmove', touchManager.handleTouchMove.bind(touchManager));
在实现这个3D教室项目过程中,最深的体会是:真实感来自于细节的堆砌。比如黑板边框的木纹走向、窗帘的物理摆动、课桌上随机摆放的书本角度,这些看似不起眼的细节组合起来,才构成了令人信服的虚拟场景。另一个重要经验是性能优化需要从设计阶段就考虑,比如使用实例化渲染处理重复元素、按需加载纹理资源等。这些策略在项目后期往往难以补救。