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

应用架构概览:
用户界面 AR核心系统 家具模型库 标记识别 姿态跟踪 场景渲染 模型加载 材质管理 虚拟物体锚定 交互系统 放置/移动/旋转 撤销/重做 场景保存
核心实现
AR家具摆放应用
vue
<template>
<div class="ar-furniture-app">
<!-- AR视图 -->
<div class="ar-viewport">
<video ref="videoElement" class="camera-feed" autoplay playsinline></video>
<canvas ref="arCanvas" class="ar-overlay"></canvas>
<!-- UI叠加层 -->
<div class="ar-ui">
<div class="status-indicator" :class="trackingStatus">
<span class="status-icon">{{ statusIcon }}</span>
<span class="status-text">{{ statusText }}</span>
</div>
<div class="instruction" v-if="showInstruction">
📱 扫描平面或标记来放置家具
</div>
</div>
</div>
<!-- 底部控制栏 -->
<div class="control-bar">
<!-- 家具选择 -->
<div class="furniture-picker">
<div class="category-tabs">
<button v-for="category in categories"
:key="category.id"
:class="{ active: currentCategory === category.id }"
@click="currentCategory = category.id"
class="tab-button">
{{ category.name }}
</button>
</div>
<div class="furniture-grid">
<div v-for="item in currentFurniture"
:key="item.id"
:class="{ selected: selectedFurniture === item.id }"
@click="selectFurniture(item)"
class="furniture-item">
<img :src="item.thumbnail" :alt="item.name" class="item-thumb">
<span class="item-name">{{ item.name }}</span>
</div>
</div>
</div>
<!-- 操作控制 -->
<div class="action-controls">
<button @click="setInteractionMode('place')"
:class="{ active: interactionMode === 'place' }"
class="action-button">
📍 放置
</button>
<button @click="setInteractionMode('move')"
:class="{ active: interactionMode === 'move' }"
class="action-button">
🚀 移动
</button>
<button @click="setInteractionMode('rotate')"
:class="{ active: interactionMode === 'rotate' }"
class="action-button">
🔄 旋转
</button>
<button @click="deleteSelected" class="action-button delete">
🗑️ 删除
</button>
<button @click="saveScene" class="action-button save">
<span v-if="!isSaving">💾 保存</span>
<span v-else>⏳ 保存中...</span>
</button>
</div>
</div>
<!-- 变换控制面板 -->
<div v-if="selectedObject" class="transform-panel">
<div class="transform-controls">
<div class="control-group">
<label>位置</label>
<div class="vector-controls">
<input type="number" v-model.number="selectedObject.position.x" step="0.1" placeholder="X">
<input type="number" v-model.number="selectedObject.position.y" step="0.1" placeholder="Y">
<input type="number" v-model.number="selectedObject.position.z" step="0.1" placeholder="Z">
</div>
</div>
<div class="control-group">
<label>旋转</label>
<div class="vector-controls">
<input type="number" v-model.number="selectedObject.rotation.x" step="0.1" placeholder="X">
<input type="number" v-model.number="selectedObject.rotation.y" step="0.1" placeholder="Y">
<input type="number" v-model.number="selectedObject.rotation.z" step="0.1" placeholder="Z">
</div>
</div>
<div class="control-group">
<label>缩放: {{ selectedObject.scale.x.toFixed(1) }}</label>
<input type="range" v-model.number="selectedObject.scale.x" min="0.1" max="3" step="0.1"
@input="uniformScale">
</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-overlay">
<div class="loading-content">
<div class="spinner"></div>
<h3>初始化AR场景...</h3>
<p>{{ loadingProgress }}%</p>
</div>
</div>
</div>
</template>
<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>
<style scoped>
.ar-furniture-app {
width: 100%;
height: 100vh;
background: #000;
overflow: hidden;
position: relative;
}
.ar-viewport {
width: 100%;
height: 100%;
position: relative;
}
.camera-feed {
width: 100%;
height: 100%;
object-fit: cover;
}
.ar-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: auto;
}
.ar-ui {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.status-indicator {
position: absolute;
top: 20px;
left: 20px;
padding: 12px 20px;
background: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 25px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
pointer-events: none;
}
.status-indicator.searching {
background: rgba(255, 165, 0, 0.7);
}
.status-indicator.tracking {
background: rgba(0, 255, 0, 0.7);
}
.status-indicator.placing {
background: rgba(0, 150, 255, 0.7);
}
.instruction {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px 30px;
border-radius: 15px;
font-size: 16px;
text-align: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
pointer-events: none;
}
.control-bar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background: rgba(45, 45, 45, 0.95);
backdrop-filter: blur(20px);
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding: 15px;
}
.furniture-picker {
margin-bottom: 15px;
}
.category-tabs {
display: flex;
gap: 5px;
margin-bottom: 15px;
overflow-x: auto;
}
.tab-button {
padding: 8px 16px;
background: #444;
color: #ccc;
border: none;
border-radius: 20px;
font-size: 14px;
white-space: nowrap;
cursor: pointer;
transition: all 0.3s ease;
}
.tab-button.active {
background: #00a8ff;
color: white;
}
.furniture-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 10px;
max-height: 120px;
overflow-y: auto;
}
.furniture-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
background: #333;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.furniture-item:hover {
background: #444;
}
.furniture-item.selected {
background: #00a8ff;
border-color: #00ffff;
}
.item-thumb {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 5px;
margin-bottom: 5px;
}
.item-name {
color: white;
font-size: 12px;
text-align: center;
}
.action-controls {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.action-button {
padding: 12px 8px;
background: #444;
color: white;
border: none;
border-radius: 8px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.action-button:hover {
background: #555;
}
.action-button.active {
background: #00a8ff;
}
.action-button.delete {
background: #ff4757;
}
.action-button.save {
background: #2ed573;
}
.transform-panel {
position: absolute;
top: 80px;
right: 20px;
background: rgba(45, 45, 45, 0.9);
padding: 15px;
border-radius: 10px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
min-width: 200px;
}
.transform-controls {
display: flex;
flex-direction: column;
gap: 15px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.control-group label {
color: #00ffff;
font-size: 14px;
font-weight: bold;
}
.vector-controls {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 5px;
}
.vector-controls input {
padding: 6px;
background: #333;
border: 1px solid #555;
border-radius: 4px;
color: white;
font-size: 12px;
text-align: center;
}
.vector-controls input::placeholder {
color: #888;
}
.control-group input[type="range"] {
width: 100%;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-content {
text-align: center;
color: white;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #00ffff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
.loading-content h3 {
margin: 0 0 10px 0;
color: white;
}
.loading-content p {
margin: 0;
color: #00ffff;
font-size: 18px;
font-weight: bold;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 768px) {
.action-controls {
grid-template-columns: repeat(3, 1fr);
}
.action-button.save {
grid-column: 1 / -1;
}
.transform-panel {
position: relative;
top: auto;
right: auto;
margin: 10px;
width: calc(100% - 20px);
}
.furniture-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 480px) {
.category-tabs {
font-size: 12px;
}
.tab-button {
padding: 6px 12px;
}
.action-button {
font-size: 11px;
padding: 10px 6px;
}
.furniture-grid {
grid-template-columns: repeat(3, 1fr);
}
}
</style>
应用特性
核心功能
- 实时AR跟踪 - 基于标记或平面检测的稳定跟踪
- 家具模型库 - 分类整理的3D家具模型
- 直观放置 - 点击屏幕即可放置选中家具
- 精细控制 - 支持位置、旋转、缩放的精确调整
- 场景持久化 - 本地保存和恢复家具布局
交互体验
- 智能状态提示 - 清晰显示当前AR跟踪状态
- 视觉反馈 - 选中高亮和操作确认
- 渐进式指引 - 新用户友好引导
- 响应式布局 - 适配不同屏幕尺寸
技术亮点
- 模块化架构 - 清晰的职责分离
- 性能优化 - 高效的渲染和资源管理
- 错误处理 - 健壮的异常处理机制
- 扩展性强 - 易于添加新家具和功能
本节通过完整的AR家具摆放应用,展示了如何将前面学习的AR技术、3D渲染和交互设计整合到实用的商业应用中。这种类型的应用在家具零售、室内设计等领域具有广泛的应用前景。