效果图

在 3D 可视化领域,真实的反射效果是提升场景沉浸感的关键因素之一。本文将详细介绍如何使用 Three.js 结合 Vue3 实现高质量的镜面反射效果,包括地面反射和墙面反射,并通过动态物体展示反射效果的实时变化。
效果展示
本文实现的 3D 场景包含以下元素:
- 一个水平放置的圆形地面镜面
- 一个垂直放置的矩形墙面镜面
- 动态旋转的半球体物体
- 沿轨迹运动的小球体
- 多面彩色墙壁形成的封闭空间
- 多角度光源系统
通过这些元素的组合,我们可以观察到物体在不同镜面中的反射效果,以及光线与反射之间的相互作用。
实现方案
我们使用 Vue3 的单文件组件 (SFC) 结构,结合 Three.js 实现这个 3D 场景。核心技术点包括:Three.js 的 Reflector 类实现镜面反射、OrbitControls 实现相机控制、以及 Vue 的生命周期管理 Three.js 资源。
完整代码实现
以下是完整的 Vue3 组件代码,包含详细注释:
html
<!-- 镜面效果 -->
<template>
<div class="mirror-scene">
<!-- Three.js渲染容器:用于挂载WebGL画布 -->
<div ref="container" class="scene-container"></div>
<!-- 场景状态控制面板:展示渲染状态并提供交互按钮 -->
<div class="status-info" v-if="showInfo">
<p>场景状态: {{ isRendering ? '渲染中' : '已停止' }}</p>
<button @click="toggleRendering">
{{ isRendering ? '停止' : '启动' }}渲染
</button>
</div>
</div>
</template>
<script setup>
// 1. 导入依赖库
// 导入Three.js核心库:包含所有3D渲染所需的基础类(场景、相机、几何体等)
import * as THREE from 'three';
// 导入轨道控制器:允许鼠标拖拽旋转视角、滚轮缩放、右键平移
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 导入反射器类:Three.js官方扩展,用于创建真实的镜面反射效果
import { Reflector } from 'three/addons/objects/Reflector.js';
// 导入Vue3生命周期钩子和响应式API
import { onMounted, onUnmounted, ref, watch } from 'vue';
// 2. DOM元素引用
// 绑定模板中的渲染容器,用于后续挂载Three.js的WebGL画布
const container = ref(null);
// 3. Vue响应式状态管理
// 控制场景是否持续渲染(true=渲染中,false=暂停)
const isRendering = ref(true);
// 控制是否显示状态控制面板(true=显示,false=隐藏)
const showInfo = ref(true);
// 存储动画循环ID:用于后续停止动画(requestAnimationFrame返回的唯一标识)
const animationId = ref(null);
// 4. Three.js核心对象声明(全局变量,方便各函数访问)
let camera; // 相机:模拟人眼视角,决定场景中哪些内容会被渲染
let scene; // 场景:3D世界的容器,所有物体(模型、光源、镜面)都需添加到场景中
let renderer; // 渲染器:将场景和相机的内容渲染成2D图像并显示在画布上
let cameraControls; // 相机控制器:处理用户交互(旋转、缩放、平移)
let sphereGroup; // 球体组:用于统一管理多个球体对象(方便批量旋转)
let smallSphere; // 小球体:场景中的动态物体(做轨迹运动)
let groundMirror; // 地面镜面:水平放置的圆形反射面
let verticalMirror; // 垂直镜面:墙面放置的矩形反射面
/**
* 5. 初始化Three.js场景(核心入口函数)
* 作用:创建场景、相机、渲染器,加载所有3D物体并完成初始配置
*/
const initScene = () => {
// 5.1 创建WebGL渲染器
// WebGLRenderer:Three.js的核心渲染器,基于WebGL技术渲染3D场景
// 参数说明:
// - antialias: true → 开启抗锯齿(使图像边缘更平滑,避免锯齿状)
// - powerPreference: 'high-performance' → 告诉浏览器优先使用高性能GPU(提升渲染效率)
renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: 'high-performance',
});
// 设置像素比:匹配设备屏幕的像素密度(避免高清屏幕下图像模糊)
// window.devicePixelRatio → 获取设备像素比(如Retina屏为2)
renderer.setPixelRatio(window.devicePixelRatio);
// 设置渲染画布大小:与容器尺寸一致(占满整个屏幕)
renderer.setSize(container.value.clientWidth, container.value.clientHeight);
// 将渲染器生成的画布(domElement)挂载到Vue模板的容器中
container.value.appendChild(renderer.domElement);
// 5.2 创建场景容器
// Scene:Three.js的场景容器,所有3D物体都需要通过scene.add()添加到场景中
// 场景本身不显示内容,仅用于组织和管理物体
scene = new THREE.Scene();
// 5.3 创建透视相机(最常用的相机类型,模拟人眼透视效果)
// PerspectiveCamera构造参数:
// 1. fov: 45 → 视场角(Field of View):相机视角的垂直角度(单位:度),值越大看到的范围越广
// 2. aspect: 宽高比 → 相机视口的宽高比例(通常设为容器宽高比,避免图像拉伸)
// 3. near: 1 → 近裁剪面:距离相机小于该值的物体不会被渲染(避免渲染过近的物体导致性能问题)
// 4. far: 500 → 远裁剪面:距离相机大于该值的物体不会被渲染(避免渲染过远的物体导致性能问题)
camera = new THREE.PerspectiveCamera(
45,
container.value.clientWidth / container.value.clientHeight,
1,
500
);
// 设置相机位置(Three.js中默认坐标系:X轴右、Y轴上、Z轴前)
// position.set(x, y, z) → 这里将相机放在(0,75,160),即场景上方偏后位置,能看到整个场景
camera.position.set(0, 75, 160);
// 5.4 初始化相机控制器(用户交互)
initControls();
// 5.5 创建镜面反射效果
createMirrors();
// 5.6 创建场景中的3D物体(球体、半球体等)
createSceneObjects();
// 5.7 创建光源(没有光源场景会是黑色,光源决定物体的明暗和颜色)
createLights();
// 5.8 创建场景边界(墙壁、顶部、底部,形成一个封闭空间)
createWalls();
};
/**
* 6. 初始化相机控制器(OrbitControls)
* 作用:处理用户交互,允许通过鼠标/触摸控制相机视角
*/
const initControls = () => {
// OrbitControls构造参数:
// 1. object: camera → 要控制的相机对象
// 2. domElement: renderer.domElement → 监听交互事件的DOM元素(渲染画布)
cameraControls = new OrbitControls(camera, renderer.domElement);
// 设置相机目标点:相机始终看向该点(这里设为(0,40,0),即场景中心偏上位置)
cameraControls.target.set(0, 40, 0);
// 设置相机最大/最小距离:限制用户缩放的范围(避免过近或过远导致看不到场景)
cameraControls.maxDistance = 400; // 最远能拉到400单位距离
cameraControls.minDistance = 10; // 最近能推到10单位距离
// 更新控制器状态:初始化后必须调用一次,确保控制器参数生效
cameraControls.update();
};
/**
* 7. 创建镜面反射效果(基于Reflector类)
* 作用:生成真实的镜面,能反射场景中的物体(核心原理:用虚拟相机渲染场景到纹理,再贴到镜面表面)
*/
const createMirrors = () => {
// 7.1 创建地面镜面(圆形)
// CircleGeometry:创建圆形几何体(参数:半径、分段数)
// 1. radius: 40 → 圆形半径(40单位)
// 2. segments: 64 → 分段数(值越大圆形越平滑,64足够满足视觉效果)
const groundGeometry = new THREE.CircleGeometry(40, 64);
// Reflector:Three.js官方扩展的反射器类,用于创建镜面
// 第一个参数:镜面的几何体(决定镜面的形状和大小)
// 第二个参数:镜面配置项(核心参数说明)
// - clipBias: 0.003 → 裁剪偏差(解决镜面反射中物体与镜面边缘的Z轴冲突,避免出现"穿模"闪烁)
// - textureWidth/textureHeight: 反射纹理的分辨率(设为屏幕分辨率×像素比,保证反射清晰度)
// - color: 0xb5b5b5 → 镜面颜色(十六进制,这里是浅灰色,模拟真实镜子的底色)
groundMirror = new Reflector(groundGeometry, {
clipBias: 0.003,
textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio,
color: 0xb5b5b5,
});
// 设置地面镜面位置:y=0.5(略微高于地面,避免与地面重叠)
groundMirror.position.y = 0.5;
// 旋转镜面:绕X轴旋转-90度(Math.PI/2弧度=90度),使圆形几何体从垂直变为水平(地面效果)
groundMirror.rotateX(-Math.PI / 2);
// 将镜面添加到场景中(不添加则不会被渲染)
scene.add(groundMirror);
// 7.2 创建垂直镜面(墙面矩形)
// PlaneGeometry:创建平面几何体(参数:宽度、高度)
// 1. width: 100 → 平面宽度(100单位)
// 2. height: 100 → 平面高度(100单位)
const verticalGeometry = new THREE.PlaneGeometry(100, 100);
// 创建垂直镜面(配置项与地面镜面一致,颜色略浅)
verticalMirror = new Reflector(verticalGeometry, {
clipBias: 0.003,
textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio,
color: 0xc1cbcb,
});
// 设置垂直镜面位置:y=50(垂直居中),z=-50(放在场景后方,模拟墙面)
verticalMirror.position.y = 50;
verticalMirror.position.z = -50;
// 将垂直镜面添加到场景中
scene.add(verticalMirror);
};
/**
* 8. 创建场景中的3D物体(球体组、半球体、小球体)
* 作用:生成场景中的可视物体,丰富场景内容
*/
const createSceneObjects = () => {
// 8.1 创建球体组(Object3D是所有3D物体的基类,可作为容器管理多个子物体)
// 用球体组管理半球体,后续旋转球体组时,半球体也会跟着旋转
sphereGroup = new THREE.Object3D();
scene.add(sphereGroup);
// 8.2 创建半球体(由圆柱顶+半球组成)
// MeshPhongMaterial:Phong着色材质(支持镜面高光,适合模拟有光泽的物体)
// 参数说明:
// - color: 0xffffff → 物体基础颜色(白色)
// - emissive: 0x8d8d8d → 自发光颜色(浅灰色,使物体在暗处也能显示,模拟微弱反光)
const sphereMaterial = new THREE.MeshPhongMaterial({
color: 0xffffff,
emissive: 0x8d8d8d,
});
// 8.2.1 创建球体顶部(圆柱几何体,模拟半球的"底座")
// CylinderGeometry:圆柱几何体(参数:顶部半径、底部半径、高度、径向分段、高度分段)
// 1. radiusTop: 0.1 → 顶部半径(接近0,形成尖顶)
// 2. radiusBottom: 15*cos(30°) → 底部半径(与半球半径匹配,确保衔接平滑)
// 3. height: 0.1 → 圆柱高度(很薄,仅作为衔接)
// 4. radialSegments: 24 → 径向分段数(值越大圆柱越平滑)
// 5. heightSegments: 1 → 高度分段数(1即可,因为高度很薄)
const sphereCapGeometry = new THREE.CylinderGeometry(
0.1,
15 * Math.cos((Math.PI / 180) * 30), // 角度转弧度:30度=Math.PI/6弧度,cos(30°)=√3/2≈0.866
0.1,
24,
1
);
// Mesh:几何体+材质的组合(Three.js中可渲染的物体必须是Mesh类型)
const sphereCap = new THREE.Mesh(sphereCapGeometry, sphereMaterial);
// 调整圆柱顶位置:与半球底部衔接(计算半球底部的Y坐标,确保无缝连接)
sphereCap.position.y = -15 * Math.sin((Math.PI / 180) * 30) - 0.05;
// 旋转圆柱顶:绕X轴旋转180度(Math.PI弧度),使尖顶朝下(与半球衔接)
sphereCap.rotateX(-Math.PI);
// 8.2.2 创建半球(SphereGeometry的部分区域)
// SphereGeometry:球体几何体(参数:半径、宽度分段、高度分段、起始经度、经度范围、起始纬度、纬度范围)
// 这里通过参数控制,只生成球体的1/3(模拟半球):
// 1. radius: 15 → 球体半径(15单位)
// 2. widthSegments: 24 → 水平方向分段数
// 3. heightSegments: 24 → 垂直方向分段数
// 4. phiStart: Math.PI/2 → 起始经度(90度,从Y轴正方向开始)
// 5. phiLength: Math.PI*2 → 经度范围(360度,绕Y轴一周)
// 6. thetaStart: 0 → 起始纬度(0度,从Z轴正方向开始)
// 7. thetaLength: Math.PI*120/180 → 纬度范围(120度,仅生成上半部分)
const halfSphereGeometry = new THREE.SphereGeometry(
15,
24,
24,
Math.PI / 2,
Math.PI * 2,
0,
(Math.PI / 180) * 120
);
const halfSphere = new THREE.Mesh(halfSphereGeometry, sphereMaterial);
// 将圆柱顶添加到半球作为子物体(这样圆柱顶会跟随半球一起运动)
halfSphere.add(sphereCap);
// 旋转半球:调整角度使其倾斜放置(更自然的视觉效果)
halfSphere.rotateX((-Math.PI / 180) * 135); // 绕X轴旋转-135度
halfSphere.rotateZ((-Math.PI / 180) * 20); // 绕Z轴旋转-20度
// 调整半球位置:使其悬浮在场景中(y坐标=7.5+半球底部高度)
halfSphere.position.y = 7.5 + 15 * Math.sin((Math.PI / 180) * 30);
// 将半球添加到球体组(后续旋转球体组时,半球会一起旋转)
sphereGroup.add(halfSphere);
// 8.3 创建小球体(做轨迹运动的动态物体)
// IcosahedronGeometry:二十面体几何体(参数:半径、细分级别)
// 1. radius: 5 → 球体半径(5单位,比半球小)
// 2. detail: 0 → 细分级别(0为基础二十面体,值越大越接近球体)
const smallSphereGeometry = new THREE.IcosahedronGeometry(5, 0);
// 小球体材质:开启平面着色(flatShading=true,使面与面之间有明显边界,风格化效果)
const smallSphereMaterial = new THREE.MeshPhongMaterial({
color: 0xffffff,
emissive: 0x7b7b7b,
flatShading: true, // 平面着色(关闭平滑着色,突出几何体的面结构)
});
smallSphere = new THREE.Mesh(smallSphereGeometry, smallSphereMaterial);
// 将小球体添加到场景中(不加入球体组,单独做轨迹运动)
scene.add(smallSphere);
};
/**
* 9. 创建场景边界(墙壁、顶部、底部)
* 作用:形成封闭空间,让镜面能反射到更多内容,增强场景真实感
*/
const createWalls = () => {
// PlaneGeometry:平面几何体(100.1×100.1,比100大0.1是为了避免墙面衔接处出现缝隙)
const planeGeo = new THREE.PlaneGeometry(100.1, 100.1);
// 9.1 顶部墙面(白色,模拟天花板)
const planeTop = new THREE.Mesh(
planeGeo,
new THREE.MeshPhongMaterial({ color: 0xffffff }) // 白色漫反射材质
);
planeTop.position.y = 100;
planeTop.rotateX(Math.PI / 2);
scene.add(planeTop);
// 底部
const planeBottom = new THREE.Mesh(
planeGeo,
new THREE.MeshPhongMaterial({ color: 0xffffff })
);
planeBottom.rotateX(-Math.PI / 2);
scene.add(planeBottom);
// 前面
const planeFront = new THREE.Mesh(
planeGeo,
new THREE.MeshPhongMaterial({ color: 0x7f7fff })
);
planeFront.position.z = 50;
planeFront.position.y = 50;
planeFront.rotateY(Math.PI);
scene.add(planeFront);
// 右侧
const planeRight = new THREE.Mesh(
planeGeo,
new THREE.MeshPhongMaterial({ color: 0x00ff00 })
);
planeRight.position.x = 50;
planeRight.position.y = 50;
planeRight.rotateY(-Math.PI / 2);
scene.add(planeRight);
// 左侧
const planeLeft = new THREE.Mesh(
planeGeo,
new THREE.MeshPhongMaterial({ color: 0xff0000 })
);
planeLeft.position.x = -50;
planeLeft.position.y = 50;
planeLeft.rotateY(Math.PI / 2);
scene.add(planeLeft);
};
// 创建光源
const createLights = () => {
// 主光源
const mainLight = new THREE.PointLight(0xe7e7e7, 2.5, 250, 0);
mainLight.position.y = 60;
scene.add(mainLight);
// 彩色光源
const greenLight = new THREE.PointLight(0x00ff00, 0.5, 1000, 0);
greenLight.position.set(550, 50, 0);
scene.add(greenLight);
const redLight = new THREE.PointLight(0xff0000, 0.5, 1000, 0);
redLight.position.set(-550, 50, 0);
scene.add(redLight);
const blueLight = new THREE.PointLight(0xbbbbfe, 0.5, 1000, 0);
blueLight.position.set(0, 50, 550);
scene.add(blueLight);
};
// 窗口大小调整
const onWindowResize = () => {
if (!container.value) return;
const width = container.value.clientWidth;
const height = container.value.clientHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
// 更新镜面尺寸
const pixelRatio = window.devicePixelRatio;
groundMirror
.getRenderTarget()
.setSize(width * pixelRatio, height * pixelRatio);
verticalMirror
.getRenderTarget()
.setSize(width * pixelRatio, height * pixelRatio);
};
/**
* 动画循环函数:负责更新场景中物体的状态并持续渲染场景
* 采用requestAnimationFrame实现高效动画,浏览器会自动优化渲染时机
* 与setInterval相比,能更好地与浏览器刷新频率同步,减少性能浪费
*/
const animate = () => {
// 如果渲染状态为停止,则直接返回,不执行后续动画逻辑
if (!isRendering.value) return;
// 请求下一帧动画,并保存动画ID用于后续停止动画
// requestAnimationFrame会在浏览器下一次重绘前调用指定函数
animationId.value = requestAnimationFrame(animate);
// 计算计时器:基于当前时间生成随时间线性变化的数值
// Date.now()返回当前时间戳(毫秒),乘以0.01将其转换为更易处理的时间单位
const timer = Date.now() * 0.01;
// 更新物体动画:球体组绕Y轴缓慢旋转
// rotation.y表示绕Y轴的旋转角度(弧度),每次减少0.002产生逆时针旋转效果
// 负值表示逆时针旋转,正值表示顺时针旋转
sphereGroup.rotation.y -= 0.002;
// 更新小球体位置:使其沿三维空间中的椭圆形轨迹运动
// 使用三角函数实现平滑的周期性运动
smallSphere.position.set(
// X轴位置:基于余弦函数,形成左右方向的周期性运动
// timer * 0.1控制X轴运动周期(值越小周期越长)
// 乘以30控制X轴方向的运动幅度(轨迹半径)
Math.cos(timer * 0.1) * 30,
// Y轴位置:使用绝对值确保小球始终在Y轴正方向运动
// Math.abs(Math.cos(...))使运动轨迹在Y轴方向形成上下波动的"笑脸"曲线
// +5确保小球最低位置不会低于Y=5,避免与地面镜面过度重叠
Math.abs(Math.cos(timer * 0.2)) * 20 + 5,
// Z轴位置:基于正弦函数,形成前后方向的周期性运动
// 与X轴使用相同周期但不同函数(正弦vs余弦),形成圆形轨迹
Math.sin(timer * 0.1) * 30
);
// 更新小球体自身旋转:绕Y轴旋转
// 旋转角度与位置同步,使小球始终面向运动方向
smallSphere.rotation.y = Math.PI / 2 - timer * 0.1;
// 绕Z轴快速旋转:增加视觉动感
// 0.8的系数使旋转速度快于位置变化,形成更丰富的动画效果
smallSphere.rotation.z = timer * 0.8;
// 渲染场景:将当前状态的3D场景通过相机视角渲染到画布
// 这是Three.js动画的最后一步,将所有状态更新反映到屏幕上
renderer.render(scene, camera);
};
// 切换渲染状态
const toggleRendering = () => {
isRendering.value = !isRendering.value;
if (isRendering.value) {
animate();
}
};
// 组件挂载时初始化
onMounted(() => {
if (container.value) {
initScene();
animate();
window.addEventListener('resize', onWindowResize);
}
});
// 组件卸载时清理
onUnmounted(() => {
// 停止动画
if (animationId.value) {
cancelAnimationFrame(animationId.value);
}
// 移除事件监听
window.removeEventListener('resize', onWindowResize);
// 清理控制器
if (cameraControls) {
cameraControls.dispose();
}
// 清理渲染器
if (renderer) {
renderer.dispose();
if (container.value && renderer.domElement) {
container.value.removeChild(renderer.domElement);
}
}
// 清理场景
if (scene) {
scene.clear();
}
});
// 监听渲染状态变化
watch(isRendering, (newVal) => {
if (newVal && !animationId.value) {
animate();
}
});
</script>
<style scoped>
.mirror-scene {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.scene-container {
width: 100%;
height: 100%;
}
.render-container {
width: 100%;
height: 100%;
}
.status-info {
position: absolute;
top: 5rem;
left: 10rem;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 0.75rem 1rem;
border-radius: 4px;
font-family: sans-serif;
font-size: 0.9rem;
z-index: 10;
display: flex;
gap: 1rem;
align-items: center;
}
.status-info button {
background-color: #42b983;
color: white;
border: none;
padding: 0.4rem 0.8rem;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s;
}
.status-info button:hover {
background-color: #35956a;
}
.status-info button:active {
background-color: #2a7d56;
}
</style>
核心技术解析
1. 镜面反射原理
Three.js 的 Reflector 类是实现镜面反射的核心,其工作原理如下:
- 创建一个与镜面大小相同的虚拟相机
- 将虚拟相机放置在镜面的对称位置(相对于真实相机)
- 使用虚拟相机渲染场景到一个纹理 (Texture)
- 将这个纹理应用到镜面表面
这种方法能产生非常真实的反射效果,但也会增加渲染开销,因为每面镜子都需要额外渲染一次场景。
2. 关键参数配置
在创建 Reflector 时,有几个关键参数需要特别注意:
new Reflector(geometry, {
clipBias: 0.003, // 解决Z轴冲突,避免反射物体与镜面边缘闪烁
textureWidth: window.innerWidth * window.devicePixelRatio, // 反射纹理宽度
textureHeight: window.innerHeight * window.devicePixelRatio, // 反射纹理高度
color: 0xb5b5b5, // 镜面颜色,影响反射色调
});
clipBias
:解决 Z-fighting 问题,当物体接近镜面时可能出现的闪烁- 纹理尺寸:直接影响反射效果的清晰度,值越大效果越好但性能消耗也越大
color
:镜面本身的颜色,会与反射内容混合
3. 性能优化建议
由于镜面反射需要额外的渲染通道,可能会影响性能,特别是在移动设备上。以下是一些优化建议:
- 适当降低反射纹理的分辨率(例如使用 0.5 倍屏幕分辨率)
- 减少镜面数量,避免过多的反射面
- 使用
powerPreference: 'high-performance'
让浏览器优先选择高性能 GPU - 复杂场景可考虑使用简化的反射模型,如环境贴图 (Environment Map)
使用方法
- 确保已安装 Three.js:
npm install three
- 将上述代码保存为
MirrorScene.vue
组件 - 在你的 Vue 应用中引入并使用该组件
- 运行应用,你将看到一个带有镜面反射效果的 3D 场景
- 可以通过以下方式与场景交互:
- 鼠标左键拖拽:旋转视角
- 鼠标滚轮:缩放场景
- 鼠标右键拖拽:平移视角
- 点击 "停止 / 启动渲染" 按钮:控制动画状态
扩展思路
这个基础实现可以通过以下方式进一步扩展:
- 添加更多不同形状和位置的镜面,观察反射之间的相互影响
- 实现镜面反射的开关控制,方便对比有无反射的效果差异
- 添加更复杂的 3D 模型替代简单几何体,观察复杂物体的反射效果
- 尝试不同的镜面材质参数,如增加粗糙度 (roughness) 模拟非完美镜面
- 实现动态调整镜面颜色和反射强度的控制面板
案例源码可访问:three.js exampleshttp://www.yanhuangxueyuan.com/threejs/examples/?q=PlaneGeometry#webgl_mirror
一键三连,感谢关注