第41节:第三阶段总结:打造一个AR家具摆放应用 - 指南
第41节:第三阶段总结:打造一个AR家具摆放应用
概述
本节将综合运用前面学习的3D模型加载、AR标记跟踪、交互控制等手艺,构建一个完整的AR家具摆放应用。用户可以通过手机摄像头在真实环境中预览和摆放虚拟家具。

应用架构概览:
核心实现
AR家具摆放应用
<script>
import { onMounted, onUnmounted, ref, reactive, computed } from 'vue';
import * as THREE from 'three';
// AR场景管理器
class ARFurnitureScene {
constructor(renderer, camera) {
this.renderer = renderer;
this.camera = camera;
this.scene = new THREE.Scene();
this.placedObjects = new Map();
this.nextObjectId = 1;
this.setupScene();
}
setupScene() {
// 环境光照
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7);
this.scene.add(directionalLight);
}
// 添加家具到场景
async addFurniture(modelType, position, rotation, scale = 1) {
const geometry = this.createGeometry(modelType);
const material = this.createMaterial(modelType);
const mesh = new THREE.Mesh(geometry, material);
mesh.position.copy(position);
mesh.rotation.set(rotation.x, rotation.y, rotation.z);
mesh.scale.setScalar(scale);
const objectId = this.nextObjectId++;
mesh.userData = {
id: objectId,
type: 'furniture',
modelType: modelType
};
this.scene.add(mesh);
this.placedObjects.set(objectId, mesh);
return mesh;
}
// 创建几何体
createGeometry(type) {
switch (type) {
case 'chair':
return this.createChairGeometry();
case 'table':
return this.createTableGeometry();
case 'sofa':
return this.createSofaGeometry();
case 'bed':
return this.createBedGeometry();
case 'lamp':
return this.createLampGeometry();
default:
return new THREE.BoxGeometry(1, 1, 1);
}
}
// 创建椅子几何体
createChairGeometry() {
const group = new THREE.Group();
// 椅腿
const legGeo = new THREE.CylinderGeometry(0.05, 0.05, 0.8, 8);
const legMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const positions = [
[0.4, -0.4, 0.4], [-0.4, -0.4, 0.4],
[0.4, -0.4, -0.4], [-0.4, -0.4, -0.4]
];
positions.forEach(pos => {
const leg = new THREE.Mesh(legGeo, legMat);
leg.position.set(pos[0], pos[1], pos[2]);
group.add(leg);
});
// 椅面
const seatGeo = new THREE.BoxGeometry(1, 0.1, 1);
const seatMat = new THREE.MeshStandardMaterial({ color: 0xD2691E });
const seat = new THREE.Mesh(seatGeo, seatMat);
seat.position.y = 0;
group.add(seat);
// 椅背
const backGeo = new THREE.BoxGeometry(1, 1, 0.1);
const backMat = new THREE.MeshStandardMaterial({ color: 0xD2691E });
const back = new THREE.Mesh(backGeo, backMat);
back.position.set(0, 0.5, -0.45);
group.add(back);
return group;
}
// 创建桌子几何体
createTableGeometry() {
const group = new THREE.Group();
// 桌腿
const legGeo = new THREE.CylinderGeometry(0.08, 0.08, 1.2, 8);
const legMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const positions = [
[0.8, -0.6, 0.8], [-0.8, -0.6, 0.8],
[0.8, -0.6, -0.8], [-0.8, -0.6, -0.8]
];
positions.forEach(pos => {
const leg = new THREE.Mesh(legGeo, legMat);
leg.position.set(pos[0], pos[1], pos[2]);
group.add(leg);
});
// 桌面
const topGeo = new THREE.CylinderGeometry(1.2, 1.2, 0.1, 32);
const topMat = new THREE.MeshStandardMaterial({ color: 0xD2691E });
const top = new THREE.Mesh(topGeo, topMat);
top.position.y = 0.05;
group.add(top);
return group;
}
// 创建沙发几何体
createSofaGeometry() {
const group = new THREE.Group();
// 底座
const baseGeo = new THREE.BoxGeometry(2, 0.5, 1);
const baseMat = new THREE.MeshStandardMaterial({ color: 0x8B0000 });
const base = new THREE.Mesh(baseGeo, baseMat);
base.position.y = -0.25;
group.add(base);
// 靠背
const backGeo = new THREE.BoxGeometry(2, 1, 0.1);
const backMat = new THREE.MeshStandardMaterial({ color: 0x8B0000 });
const back = new THREE.Mesh(backGeo, backMat);
back.position.set(0, 0.25, -0.45);
group.add(back);
return group;
}
// 创建床几何体
createBedGeometry() {
const group = new THREE.Group();
// 床垫
const mattressGeo = new THREE.BoxGeometry(2, 0.3, 1.5);
const mattressMat = new THREE.MeshStandardMaterial({ color: 0x87CEEB });
const mattress = new THREE.Mesh(mattressGeo, mattressMat);
group.add(mattress);
// 床头板
const headboardGeo = new THREE.BoxGeometry(2, 0.8, 0.1);
const headboardMat = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const headboard = new THREE.Mesh(headboardGeo, headboardMat);
headboard.position.set(0, 0.25, -0.7);
group.add(headboard);
return group;
}
// 创建台灯几何体
createLampGeometry() {
const group = new THREE.Group();
// 灯座
const baseGeo = new THREE.CylinderGeometry(0.2, 0.2, 0.1, 32);
const baseMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
const base = new THREE.Mesh(baseGeo, baseMat);
base.position.y = -0.45;
group.add(base);
// 灯杆
const poleGeo = new THREE.CylinderGeometry(0.03, 0.03, 1, 8);
const poleMat = new THREE.MeshStandardMaterial({ color: 0x666666 });
const pole = new THREE.Mesh(poleGeo, poleMat);
group.add(pole);
// 灯罩
const shadeGeo = new THREE.ConeGeometry(0.3, 0.5, 32);
const shadeMat = new THREE.MeshStandardMaterial({
color: 0xFFFFFF,
transparent: true,
opacity: 0.8
});
const shade = new THREE.Mesh(shadeGeo, shadeMat);
shade.position.y = 0.25;
group.add(shade);
return group;
}
// 创建材质
createMaterial(type) {
const materials = {
chair: new THREE.MeshStandardMaterial({ color: 0xD2691E }),
table: new THREE.MeshStandardMaterial({ color: 0x8B4513 }),
sofa: new THREE.MeshStandardMaterial({ color: 0x8B0000 }),
bed: new THREE.MeshStandardMaterial({ color: 0x87CEEB }),
lamp: new THREE.MeshStandardMaterial({ color: 0xFFFFFF })
};
return materials[type] || new THREE.MeshStandardMaterial({ color: 0x6495ED });
}
// 删除对象
removeObject(objectId) {
const object = this.placedObjects.get(objectId);
if (object) {
this.scene.remove(object);
this.placedObjects.delete(objectId);
}
}
// 获取所有对象
getAllObjects() {
return Array.from(this.placedObjects.values());
}
// 渲染场景
render() {
this.renderer.render(this.scene, this.camera);
}
}
// AR跟踪管理器
class ARTrackingManager {
constructor(videoElement, canvasElement) {
this.video = videoElement;
this.canvas = canvasElement;
this.ctx = canvasElement.getContext('2d');
this.isTracking = false;
this.markerDetector = new SimpleMarkerDetector();
this.poseEstimator = new PoseEstimator();
}
async start() {
this.isTracking = true;
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: 1280,
height: 720,
facingMode: 'environment'
}
});
this.video.srcObject = stream;
this.startTrackingLoop();
} catch (error) {
console.error('无法访问相机:', error);
throw error;
}
}
stop() {
this.isTracking = false;
if (this.video.srcObject) {
this.video.srcObject.getTracks().forEach(track => track.stop());
}
}
startTrackingLoop() {
const processFrame = async () => {
if (!this.isTracking) return;
this.ctx.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
try {
const markers = await this.markerDetector.detectMarkers(this.video);
if (this.onMarkersDetected) {
this.onMarkersDetected(markers);
}
} catch (error) {
console.warn('标记检测失败:', error);
}
requestAnimationFrame(processFrame);
};
processFrame();
}
// 估计放置位置(简化版平面检测)
estimatePlacementPosition(screenX, screenY) {
// 在实际应用中,这里应该使用ARCore/ARKit的平面检测
// 简化实现:在相机前方固定距离放置
const distance = 2;
const fov = 60 * Math.PI / 180;
const aspect = this.video.videoWidth / this.video.videoHeight;
const x = (screenX / this.canvas.width - 0.5) * distance * Math.tan(fov/2) * aspect;
const y = -(screenY / this.canvas.height - 0.5) * distance * Math.tan(fov/2);
return new THREE.Vector3(x, y, -distance);
}
}
export default {
name: 'ARFurnitureApp',
setup() {
// 响应式状态
const videoElement = ref(null);
const arCanvas = ref(null);
const isLoading = ref(true);
const loadingProgress = ref(0);
const trackingStatus = ref('searching');
const interactionMode = ref('place');
const selectedFurniture = ref(null);
const selectedObject = ref(null);
const currentCategory = ref('living');
const isSaving = ref(false);
const showInstruction = ref(true);
// 家具数据
const categories = ref([
{ id: 'living', name: '客厅' },
{ id: 'bedroom', name: '卧室' },
{ id: 'dining', name: '餐厅' },
{ id: 'decor', name: '装饰' }
]);
const furnitureItems = ref({
living: [
{ id: 'sofa', name: '沙发', thumbnail: '/thumbnails/sofa.png' },
{ id: 'chair', name: '椅子', thumbnail: '/thumbnails/chair.png' },
{ id: 'table', name: '茶几', thumbnail: '/thumbnails/table.png' },
{ id: 'lamp', name: '台灯', thumbnail: '/thumbnails/lamp.png' }
],
bedroom: [
{ id: 'bed', name: '床', thumbnail: '/thumbnails/bed.png' },
{ id: 'nightstand', name: '床头柜', thumbnail: '/thumbnails/nightstand.png' }
],
dining: [
{ id: 'dining_table', name: '餐桌', thumbnail: '/thumbnails/dining_table.png' },
{ id: 'dining_chair', name: '餐椅', thumbnail: '/thumbnails/dining_chair.png' }
],
decor: [
{ id: 'plant', name: '植物', thumbnail: '/thumbnails/plant.png' },
{ id: 'vase', name: '花瓶', thumbnail: '/thumbnails/vase.png' }
]
});
// 计算属性
const currentFurniture = computed(() =>
furnitureItems.value[currentCategory.value] || []
);
const statusText = computed(() => {
const texts = {
searching: '寻找平面...',
tracking: '跟踪中',
placing: '放置模式',
moving: '移动模式',
rotating: '旋转模式'
};
return texts[trackingStatus.value] || '未知状态';
});
const statusIcon = computed(() => {
const icons = {
searching: '',
tracking: '✅',
placing: '',
moving: '',
rotating: ''
};
return icons[trackingStatus.value] || '❓';
});
// AR系统实例
let arTracker, sceneManager, renderer, camera;
let animationFrameId;
// 初始化
const init = async () => {
try {
await initARSystem();
await initScene();
await startARTracking();
isLoading.value = false;
// 3秒后隐藏指引
setTimeout(() => {
showInstruction.value = false;
}, 3000);
} catch (error) {
console.error('初始化失败:', error);
trackingStatus.value = 'searching';
}
};
// 初始化AR系统
const initARSystem = async () => {
// 初始化渲染器
renderer = new THREE.WebGLRenderer({
canvas: arCanvas.value,
alpha: true,
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 初始化相机
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 1.6, 3);
// 初始化场景管理器
sceneManager = new ARFurnitureScene(renderer, camera);
loadingProgress.value = 50;
// 初始化AR跟踪
arTracker = new ARTrackingManager(videoElement.value, arCanvas.value);
arTracker.onMarkersDetected = handleMarkersDetected;
loadingProgress.value = 100;
};
// 初始化场景
const initScene = async () => {
// 可以预加载一些资源或设置初始场景
return new Promise(resolve => setTimeout(resolve, 500));
};
// 开始AR跟踪
const startARTracking = async () => {
await arTracker.start();
trackingStatus.value = 'tracking';
startRenderLoop();
};
// 渲染循环
const startRenderLoop = () => {
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
sceneManager.render();
};
animate();
};
// 处理标记检测
const handleMarkersDetected = (markers) => {
if (markers.length > 0) {
trackingStatus.value = 'tracking';
} else {
trackingStatus.value = 'searching';
}
};
// 选择家具
const selectFurniture = (item) => {
selectedFurniture.value = item.id;
interactionMode.value = 'place';
trackingStatus.value = 'placing';
};
// 设置交互模式
const setInteractionMode = (mode) => {
interactionMode.value = mode;
trackingStatus.value = mode;
if (mode !== 'place') {
selectedFurniture.value = null;
}
};
// 放置家具
const placeFurniture = async (position) => {
if (!selectedFurniture.value) return;
const rotation = new THREE.Vector3(0, 0, 0);
const object = await sceneManager.addFurniture(
selectedFurniture.value,
position,
rotation,
1
);
selectedObject.value = object;
selectedFurniture.value = null;
interactionMode.value = 'move';
trackingStatus.value = 'moving';
};
// 统一缩放
const uniformScale = (event) => {
if (selectedObject.value) {
const scale = parseFloat(event.target.value);
selectedObject.value.scale.set(scale, scale, scale);
}
};
// 删除选中对象
const deleteSelected = () => {
if (selectedObject.value) {
sceneManager.removeObject(selectedObject.value.userData.id);
selectedObject.value = null;
}
};
// 保存场景
const saveScene = async () => {
isSaving.value = true;
try {
const sceneData = {
objects: sceneManager.getAllObjects().map(obj => ({
id: obj.userData.id,
type: obj.userData.modelType,
position: obj.position.toArray(),
rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z],
scale: obj.scale.toArray()
})),
timestamp: new Date().toISOString()
};
localStorage.setItem('arFurnitureScene', JSON.stringify(sceneData));
// 显示保存成功提示
setTimeout(() => {
isSaving.value = false;
}, 1000);
} catch (error) {
console.error('保存场景失败:', error);
isSaving.value = false;
}
};
// 点击放置家具
const handleCanvasClick = (event) => {
if (interactionMode.value === 'place' && selectedFurniture.value) {
const rect = arCanvas.value.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const position = arTracker.estimatePlacementPosition(x, y);
placeFurniture(position);
}
};
onMounted(() => {
init();
arCanvas.value.addEventListener('click', handleCanvasClick);
});
onUnmounted(() => {
if (arTracker) {
arTracker.stop();
}
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
arCanvas.value.removeEventListener('click', handleCanvasClick);
});
return {
// 模板引用
videoElement,
arCanvas,
// 状态数据
isLoading,
loadingProgress,
trackingStatus,
interactionMode,
selectedFurniture,
selectedObject,
currentCategory,
isSaving,
showInstruction,
categories,
furnitureItems,
// 计算属性
currentFurniture,
statusText,
statusIcon,
// 方法
selectFurniture,
setInteractionMode,
uniformScale,
deleteSelected,
saveScene
};
}
};
</script>
应用特性
核心功能
- 实时AR跟踪- 基于标记或平面检测的稳定跟踪
- 家具模型库- 分类整理的3D家具模型
- 直观放置- 点击屏幕即可放置选中家具
- 精细控制- 协助位置、旋转、缩放的精确调整
- 场景持久化- 本地保存和恢复家具布局
交互体验
- 智能状态提示- 清晰呈现当前AR跟踪状态
- 视觉反馈- 选中高亮和操作确认
- 渐进式指引- 新用户友好引导
- 响应式布局- 适配不同屏幕尺寸
技术亮点
- 模块化架构- 清晰的职责分离
- 性能优化- 高效的渲染和资源管理
- 错误处理- 健壮的异常处理机制
- 扩展性强- 易于添加新家具和功能
本节通过完整的AR家具摆放应用,展示了如何将前面学习的AR技术、3D渲染和交互设计整合到实用的商业应用中。这种类型的应用在家具零售、室内设计等领域具有广泛的应用前景。

浙公网安备 33010602011771号