HarmonyOS 6(API 23)实战:基于 Face AR & Body AR 的“空间交互设计工作台“——PC端手势驱动3D建模系统

文章目录

    • 每日一句正能量
    • 前言
    • 一、PC端AR能力升级:从娱乐到生产力
      • [1.1 能力边界拓展](#1.1 能力边界拓展)
      • [1.2 设计场景重新定义](#1.2 设计场景重新定义)
    • 二、项目架构:"空间交互设计工作台"
      • [2.1 功能模块设计](#2.1 功能模块设计)
      • [2.2 多窗口数据流架构](#2.2 多窗口数据流架构)
    • 三、环境配置与AR引擎初始化
      • [3.1 模块依赖配置](#3.1 模块依赖配置)
      • [3.2 PC端AR会话配置(SpatialAbility.ets)](#3.2 PC端AR会话配置(SpatialAbility.ets))
    • 四、核心组件实战
      • [4.1 3D设计画布:手势驱动模型操控](#4.1 3D设计画布:手势驱动模型操控)
      • [4.2 AR监控窗口:实时追踪可视化](#4.2 AR监控窗口:实时追踪可视化)
      • [4.3 悬浮控制面板:材质库与沉浸光感](#4.3 悬浮控制面板:材质库与沉浸光感)
      • [4.4 主工作台页面:多窗口整合](#4.4 主工作台页面:多窗口整合)
    • 五、关键技术总结
      • [5.1 Face AR & Body AR 在PC端的创新映射](#5.1 Face AR & Body AR 在PC端的创新映射)
      • [5.2 多窗口AR数据同步架构](#5.2 多窗口AR数据同步架构)
      • [5.3 性能优化策略](#5.3 性能优化策略)
    • 六、调试与多设备适配
      • [6.1 PC端AR调试要点](#6.1 PC端AR调试要点)
      • [6.2 设备适配矩阵](#6.2 设备适配矩阵)
    • 七、总结与展望

每日一句正能量

没多少人的人生是容易的,你吃过的苦,受过的累,总有一天,会变成生活对你的嘉奖。早安!

前言

摘要:HarmonyOS 6(API 23)将 Face AR 与 Body AR 能力从移动端扩展至PC大屏场景,为专业设计工具带来了"隔空操控"的全新交互维度。本文将实战开发一款面向工业设计师的"空间交互设计工作台",通过面部微表情控制材质切换、人体骨骼手势驱动3D模型旋转缩放,结合PC多窗口架构与沉浸光感,打造"眼到手到"的无接触设计体验。


一、PC端AR能力升级:从娱乐到生产力

1.1 能力边界拓展

HarmonyOS 6(API 23)的AR Engine 6.1.0在PC端实现了关键突破 :

能力维度 移动端(手机/平板) PC端(桌面工作站)
摄像头分辨率 1080P 4K/8K专业摄像头
追踪帧率 30fps 60fps高帧率
追踪距离 0.3-1.5m 0.5-3m(大屏场景)
多模态并发 Face AR或Body AR Face AR + Body AR + 手势同步
算力支撑 NPU端侧推理 NPU + GPU协同加速

PC端独特优势

  • 大屏空间:27寸+显示器提供更大的手势识别区域,支持更精细的肢体动作
  • 多窗口协同:AR预览窗口与3D建模主窗口并行,实时查看追踪效果
  • 专业外设:可外接深度摄像头(如Intel RealSense)提升追踪精度

1.2 设计场景重新定义

传统3D设计软件依赖鼠标+键盘的"二维映射三维"交互模式,存在直观性差、学习曲线陡峭的问题。基于Face AR & Body AR的空间交互设计工作台实现了:

  • 眼神注视点渲染:通过眼动追踪(Face AR的瞳孔关键点)自动高亮注视区域
  • 表情驱动材质:挑眉切换金属/玻璃/木质材质,张嘴确认选择
  • 手势操控模型:双手捏合缩放、单手旋转、双手平移,符合人类直觉
  • 姿态辅助视角:身体前倾进入细节模式,后仰切换全局视图

二、项目架构:"空间交互设计工作台"

2.1 功能模块设计

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    主设计窗口 (Main Window)                    │
│  ┌──────────────────────────────────────────────────────┐   │
│  │              3D建模画布 (WebGL/Canvas 3D)              │   │
│  │  · 手势操控:旋转/缩放/平移3D模型                      │   │
│  │  · 注视点高亮:眼动追踪自动聚焦细节区域                 │   │
│  │  · 材质预览:表情驱动实时切换材质球                     │   │
│  └──────────────────────────────────────────────────────┘   │
│                          ↑                                  │
│              AR追踪数据流 (60fps实时同步)                      │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│              AR监控窗口 (Sub Window - 浮动)                   │
│  ┌─────────────────────┐  ┌─────────────────────────────┐   │
│  │   Face AR 预览画面   │  │     Body AR 骨骼可视化       │   │
│  │  · 68点人脸Mesh      │  │  · 20+骨骼关键点连线         │   │
│  │  · 表情参数仪表盘     │  │  · 手势状态识别指示器        │   │
│  │  · 注视点热力图       │  │  · 姿态角度实时数值          │   │
│  └─────────────────────┘  └─────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│              悬浮控制面板 (Float Navigation HUD)              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  材质库      │  │  手势映射    │  │   沉浸光效状态      │  │
│  │  · 表情绑定  │  │  · 动作配置  │  │  · 追踪质量指示     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

2.2 多窗口数据流架构

复制代码
AR Engine 6.1.0 (PC端)
    │
    ├─→ Face AR Track → 表情参数 + 注视点坐标 → 材质切换 + 焦点渲染
    │
    ├─→ Body AR Track → 骨骼关键点 + 手势状态 → 模型变换矩阵
    │
    └─→ 数据融合层 ──→ AppStorage全局状态同步 ──→ 多窗口UI更新
                              │
        ┌─────────────────────┼─────────────────────┐
        ↓                     ↓                     ↓
   主设计窗口              AR监控窗口            悬浮控制面板
   (3D模型渲染)           (追踪可视化)           (参数调节)

三、环境配置与AR引擎初始化

3.1 模块依赖配置

json 复制代码
// oh-package.json5
{
  "dependencies": {
    "@hms.core.ar.arengine": "^6.1.0",
    "@hms.core.ar.arview": "^6.1.0",
    "@kit.ArkUI": "^6.1.0",
    "@kit.AbilityKit": "^6.1.0",
    "@kit.SensorServiceKit": "^6.1.0",
    "@kit.BasicServicesKit": "^6.1.0"
  }
}

3.2 PC端AR会话配置(SpatialAbility.ets)

代码亮点:PC端AR会话需配置高分辨率、高帧率模式,并启用多模态并发追踪。同时创建独立的AR监控子窗口,实现"主窗口设计+子窗口监控"的专业工作流。

typescript 复制代码
// entry/src/main/ets/ability/SpatialAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { arEngine, ARConfig, ARFeatureType, ARMultiFaceMode } from '@hms.core.ar.arengine';

export default class SpatialAbility extends UIAbility {
  private arSession: arEngine.ARSession | null = null;
  private mainWindow: window.Window | null = null;
  private arMonitorWindow: window.Window | null = null;

  onWindowStageCreate(windowStage: window.WindowStage): void {
    this.initializePCWindow(windowStage);
  }

  /**
   * 初始化PC端多窗口架构
   * 1. 主窗口:全屏沉浸式设计画布
   * 2. AR监控窗口:浮动窗口实时显示追踪状态
   */
  private async initializePCWindow(windowStage: window.WindowStage): Promise<void> {
    try {
      // 1. 配置主窗口
      this.mainWindow = windowStage.getMainWindowSync();
      await this.mainWindow.setWindowSizeType(window.WindowSizeType.FREE);
      await this.mainWindow.setWindowMode(window.WindowMode.FULLSCREEN);
      await this.mainWindow.setWindowTitleBarEnable(false);
      await this.mainWindow.setWindowShadowEnabled(true);
      await this.mainWindow.setWindowCornerRadius(8);
      await this.mainWindow.setWindowBackgroundColor('#00000000');
      await this.mainWindow.setWindowLayoutFullScreen(true);

      // 2. 加载主设计界面
      windowStage.loadContent('pages/SpatialWorkbench', (err) => {
        if (err.code) {
          console.error('Failed to load main content:', JSON.stringify(err));
          return;
        }
        this.createARMonitorWindow();
      });

      // 3. 保存窗口引用
      AppStorage.setOrCreate('main_window', this.mainWindow);

    } catch (error) {
      console.error('Window initialization failed:', (error as BusinessError).message);
    }
  }

  /**
   * 创建AR监控浮动窗口
   * 位置:主窗口右侧,用于实时查看Face AR和Body AR追踪效果
   */
  private async createARMonitorWindow(): Promise<void> {
    try {
      if (!this.mainWindow) return;

      this.arMonitorWindow = await this.mainWindow.createSubWindow('ARMonitor');
      
      // 配置为浮动工具窗口
      await this.arMonitorWindow.setWindowSizeType(window.WindowSizeType.FREE);
      await this.arMonitorWindow.moveWindowTo({ x: 1400, y: 100 });
      await this.arMonitorWindow.resize(480, 640);
      await this.arMonitorWindow.setWindowBackgroundColor('#1a1a2e');
      await this.arMonitorWindow.setWindowShadowEnabled(true);
      await this.arMonitorWindow.setWindowCornerRadius(16);
      await this.arMonitorWindow.setWindowTopmost(true); // 置顶

      await this.arMonitorWindow.setUIContent('pages/ARMonitorPage');
      await this.arMonitorWindow.showWindow();

      AppStorage.setOrCreate('ar_monitor_window', this.arMonitorWindow);
      console.info('AR Monitor window created');

      // 初始化AR引擎
      this.initializeAREngine();
    } catch (error) {
      console.error('AR Monitor window creation failed:', error);
    }
  }

  /**
   * 初始化AR引擎(PC端高配置模式)
   * 关键配置:
   * - 4K分辨率输入(利用PC高清摄像头)
   * - 60fps追踪帧率
   * - Face AR + Body AR 双模态并发
   * - 多人脸模式(支持双人协作设计评审)
   */
  private async initializeAREngine(): Promise<void> {
    try {
      const context = getContext(this);
      
      // 检查AR Engine可用性
      const isReady = await arEngine.isAREngineReady(context);
      if (!isReady) {
        console.error('AR Engine not available on this PC');
        return;
      }

      this.arSession = new arEngine.ARSession(context);
      
      const config = new ARConfig();
      // PC端启用双模态并发
      config.featureType = ARFeatureType.ARENGINE_FEATURE_TYPE_FACE | 
                           ARFeatureType.ARENGINE_FEATURE_TYPE_BODY;
      
      // 多人脸模式:支持设计师+客户双人评审场景
      config.multiFaceMode = ARMultiFaceMode.MULTIFACE_ENABLE;
      config.maxDetectedBodyNum = 2;
      
      // PC端前置高清摄像头
      config.cameraLensFacing = arEngine.ARCameraLensFacing.FRONT;
      
      // 高帧率模式(PC端算力充足)
      config.imageResolution = { width: 1920, height: 1080 };

      this.arSession.configure(config);
      await this.arSession.start();

      // 启动AR数据循环
      this.startARDataLoop();

      AppStorage.setOrCreate('ar_session', this.arSession);
      console.info('AR Engine initialized with Face+Body dual mode');
    } catch (error) {
      console.error('AR Engine initialization failed:', error);
    }
  }

  /**
   * AR数据主循环:60fps实时处理
   * 将Face AR和Body AR数据同步到全局状态,供多窗口消费
   */
  private startARDataLoop(): void {
    const loop = () => {
      if (!this.arSession) return;

      const frame = this.arSession.acquireFrame();
      if (!frame) {
        requestAnimationFrame(loop);
        return;
      }

      // 1. 处理Face AR数据
      const faceAnchors = frame.getFaceAnchors ? frame.getFaceAnchors() : [];
      if (faceAnchors.length > 0) {
        const primaryFace = faceAnchors[0];
        this.processFaceData(primaryFace);
      }

      // 2. 处理Body AR数据
      const bodies = frame.acquireBodySkeleton ? frame.acquireBodySkeleton() : [];
      if (bodies.length > 0) {
        const primaryBody = bodies[0];
        this.processBodyData(primaryBody);
      }

      // 3. 释放帧资源
      frame.release();
      requestAnimationFrame(loop);
    };

    requestAnimationFrame(loop);
  }

  /**
   * 处理Face AR数据:提取表情参数和注视点
   */
  private processFaceData(faceAnchor: arEngine.ARFaceAnchor): void {
    const face = faceAnchor.getFace();
    if (!face) return;

    // 获取BlendShape表情参数
    const blendShapes = face.getBlendShapes();
    const shapeData = blendShapes.getData();
    const shapeTypes = blendShapes.getTypes();
    const expressionMap = this.parseBlendShapes(shapeTypes, new Float32Array(shapeData));

    // 获取注视点(瞳孔中心关键点)
    const landmark = face.getLandmark();
    const vertices2D = landmark.getVertices2D();
    
    // 提取瞳孔中心作为注视点(简化处理,实际需精确定位瞳孔关键点)
    const gazeX = this.extractGazePoint(vertices2D, 'x');
    const gazeY = this.extractGazePoint(vertices2D, 'y');

    // 同步到全局状态
    AppStorage.setOrCreate('face_expression', expressionMap);
    AppStorage.setOrCreate('gaze_point', { x: gazeX, y: gazeY });
    AppStorage.setOrCreate('face_detected', true);
  }

  /**
   * 处理Body AR数据:提取手势和姿态
   */
  private processBodyData(body: arEngine.ARBody): void {
    const landmarks = body.getLandmarks2D();
    
    // 提取关键骨骼点
    const leftWrist = this.findLandmark(landmarks, arEngine.ARBodyLandmarkType.LEFT_WRIST);
    const rightWrist = this.findLandmark(landmarks, arEngine.ARBodyLandmarkType.RIGHT_WRIST);
    const leftShoulder = this.findLandmark(landmarks, arEngine.ARBodyLandmarkType.LEFT_SHOULDER);
    const rightShoulder = this.findLandmark(landmarks, arEngine.ARBodyLandmarkType.RIGHT_SHOULDER);
    const nose = this.findLandmark(landmarks, arEngine.ARBodyLandmarkType.NOSE);

    // 识别手势状态
    const gesture = this.recognizeGesture(leftWrist, rightWrist, leftShoulder, rightShoulder);
    
    // 识别身体姿态(前倾/后仰角度)
    const posture = this.recognizePosture(nose, leftShoulder, rightShoulder);

    // 同步到全局状态
    AppStorage.setOrCreate('body_gesture', gesture);
    AppStorage.setOrCreate('body_posture', posture);
    AppStorage.setOrCreate('body_detected', true);
  }

  // 辅助方法
  private parseBlendShapes(types: arEngine.ARBlendShapeType[], data: Float32Array): Map<string, number> {
    const map = new Map<string, number>();
    types.forEach((type, index) => {
      map.set(type.toString(), data[index]);
    });
    return map;
  }

  private extractGazePoint(vertices: ArrayBuffer, axis: 'x' | 'y'): number {
    // 简化:取人脸中心点作为注视点近似
    const floatView = new Float32Array(vertices);
    let sum = 0;
    const stride = axis === 'x' ? 2 : 2; // x,y交替
    for (let i = axis === 'x' ? 0 : 1; i < floatView.length; i += 2) {
      sum += floatView[i];
    }
    return sum / (floatView.length / 2);
  }

  private findLandmark(landmarks: arEngine.ARBodyLandmark2D[], type: arEngine.ARBodyLandmarkType): arEngine.ARBodyLandmark2D | undefined {
    return landmarks.find(lm => lm.type === type && lm.isValid);
  }

  /**
   * 手势识别:基于手腕与肩膀的相对位置
   */
  private recognizeGesture(
    leftWrist?: arEngine.ARBodyLandmark2D,
    rightWrist?: arEngine.ARBodyLandmark2D,
    leftShoulder?: arEngine.ARBodyLandmark2D,
    rightShoulder?: arEngine.ARBodyLandmark2D
  ): { type: string; confidence: number; hands: { left: boolean; right: boolean } } {
    // 检测双手是否抬起
    const leftUp = leftWrist && leftShoulder ? leftWrist.y < leftShoulder.y : false;
    const rightUp = rightWrist && rightShoulder ? rightWrist.y < rightShoulder.y : false;

    let gestureType = 'idle';
    let confidence = 0;

    if (leftUp && rightUp) {
      // 双手抬起:检测捏合距离
      const distance = leftWrist && rightWrist 
        ? Math.sqrt(Math.pow(leftWrist.x - rightWrist.x, 2) + Math.pow(leftWrist.y - rightWrist.y, 2))
        : 0;
      
      if (distance < 100) {
        gestureType = 'pinch'; // 捏合
        confidence = 0.9;
      } else {
        gestureType = 'spread'; // 张开
        confidence = 0.8;
      }
    } else if (leftUp || rightUp) {
      gestureType = 'point'; // 单手指示
      confidence = 0.7;
    }

    return {
      type: gestureType,
      confidence,
      hands: { left: leftUp, right: rightUp }
    };
  }

  /**
   * 姿态识别:基于鼻子与肩膀的相对角度
   */
  private recognizePosture(
    nose?: arEngine.ARBodyLandmark2D,
    leftShoulder?: arEngine.ARBodyLandmark2D,
    rightShoulder?: arEngine.ARBodyLandmark2D
  ): { type: string; angle: number } {
    if (!nose || !leftShoulder || !rightShoulder) {
      return { type: 'neutral', angle: 0 };
    }

    const shoulderCenterY = (leftShoulder.y + rightShoulder.y) / 2;
    const offset = nose.y - shoulderCenterY;
    
    // 前倾:鼻子低于肩膀中心(y值更大,屏幕坐标系)
    if (offset > 50) return { type: 'lean_forward', angle: offset };
    // 后仰:鼻子高于肩膀中心
    if (offset < -50) return { type: 'lean_back', angle: offset };
    
    return { type: 'neutral', angle: 0 };
  }

  onWindowStageDestroy(): void {
    if (this.arSession) {
      this.arSession.stop();
      this.arSession = null;
    }
    if (this.arMonitorWindow) {
      this.arMonitorWindow.destroyWindow();
    }
  }
}

四、核心组件实战

4.1 3D设计画布:手势驱动模型操控

代码亮点:主设计窗口接收AR数据流,将手势状态实时映射为3D模型的变换矩阵。捏合手势触发缩放,单手位置控制旋转,身体姿态切换视图模式。

typescript 复制代码
// components/DesignCanvas3D.ets
import { Canvas, CanvasRenderingContext2D } from '@kit.ArkUI';

/**
 * 3D模型状态
 */
interface ModelTransform {
  scale: number;
  rotationX: number;
  rotationY: number;
  rotationZ: number;
  translateX: number;
  translateY: number;
}

/**
 * 材质配置
 */
interface MaterialConfig {
  name: string;
  color: string;
  roughness: number;
  metallic: number;
  opacity: number;
}

@Component
export struct DesignCanvas3D {
  private canvasRef: CanvasRenderingContext2D | null = null;
  private animationId: number = 0;

  @State modelTransform: ModelTransform = {
    scale: 1.0,
    rotationX: 0,
    rotationY: 0,
    rotationZ: 0,
    translateX: 0,
    translateY: 0
  };

  @State currentMaterial: MaterialConfig = {
    name: '金属',
    color: '#C0C0C0',
    roughness: 0.2,
    metallic: 1.0,
    opacity: 1.0
  };

  @State gazePoint: { x: number; y: number } = { x: 0.5, y: 0.5 };
  @State isDetailMode: boolean = false;
  @State gestureState: string = 'idle';
  @State lastGestureScale: number = 1.0;

  // 材质库(与表情绑定)
  private materialLibrary: MaterialConfig[] = [
    { name: '金属', color: '#C0C0C0', roughness: 0.2, metallic: 1.0, opacity: 1.0 },
    { name: '玻璃', color: '#88CCFF', roughness: 0.0, metallic: 0.0, opacity: 0.6 },
    { name: '木质', color: '#8B4513', roughness: 0.8, metallic: 0.0, opacity: 1.0 },
    { name: '陶瓷', color: '#F5F5DC', roughness: 0.4, metallic: 0.1, opacity: 1.0 },
    { name: '碳纤维', color: '#2C2C2C', roughness: 0.3, metallic: 0.5, opacity: 1.0 }
  ];

  aboutToAppear(): void {
    this.startARDataListening();
    this.startRenderLoop();
  }

  aboutToDisappear(): void {
    if (this.animationId) {
      cancelAnimationFrame(this.animationId);
    }
  }

  /**
   * 监听AR数据流并映射到3D操控
   */
  private startARDataListening(): void {
    // 监听手势状态
    AppStorage.watch('body_gesture', (gesture: { type: string; confidence: number }) => {
      this.handleGestureUpdate(gesture);
    });

    // 监听注视点
    AppStorage.watch('gaze_point', (point: { x: number; y: number }) => {
      this.gazePoint = point;
    });

    // 监听表情(材质切换)
    AppStorage.watch('face_expression', (expression: Map<string, number>) => {
      this.handleExpressionUpdate(expression);
    });

    // 监听姿态(视图模式切换)
    AppStorage.watch('body_posture', (posture: { type: string; angle: number }) => {
      this.handlePostureUpdate(posture);
    });
  }

  /**
   * 手势处理:映射到3D变换
   */
  private handleGestureUpdate(gesture: { type: string; confidence: number }): void {
    if (gesture.confidence < 0.5) return;

    this.gestureState = gesture.type;

    switch (gesture.type) {
      case 'pinch':
        // 捏合:缩小模型
        this.modelTransform.scale = Math.max(0.3, this.modelTransform.scale - 0.02);
        this.triggerHapticFeedback(20);
        break;
      case 'spread':
        // 张开:放大模型
        this.modelTransform.scale = Math.min(3.0, this.modelTransform.scale + 0.02);
        this.triggerHapticFeedback(20);
        break;
      case 'point':
        // 单手指示:旋转模型(简化版,实际应追踪手指位置)
        this.modelTransform.rotationY += 2;
        this.modelTransform.rotationX += 1;
        break;
      case 'idle':
        // 空闲:平滑回归默认状态
        this.modelTransform.rotationY *= 0.98;
        this.modelTransform.rotationX *= 0.98;
        break;
    }
  }

  /**
   * 表情处理:切换材质
   * 挑眉(EYE_BROW_UP) → 下一个材质
   * 张嘴(JAW_OPEN) → 确认当前材质
   */
  private handleExpressionUpdate(expression: Map<string, number>): void {
    const browUp = expression.get('EYE_BROW_UP_LEFT') || 0;
    const jawOpen = expression.get('JAW_OPEN') || 0;
    const eyeBlink = expression.get('EYE_BLINK_LEFT') || 0;

    // 挑眉切换材质(防抖:300ms内只触发一次)
    if (browUp > 0.7) {
      const now = Date.now();
      const lastSwitch = AppStorage.get<number>('last_material_switch') || 0;
      if (now - lastSwitch > 300) {
        this.switchToNextMaterial();
        AppStorage.setOrCreate('last_material_switch', now);
        this.triggerHapticFeedback(50);
      }
    }

    // 张嘴确认(应用当前材质到选中部件)
    if (jawOpen > 0.8) {
      this.applyMaterialToSelection();
    }

    // 眨眼触发撤销
    if (eyeBlink > 0.9) {
      // 简化处理,实际应检测双击眼
    }
  }

  /**
   * 姿态处理:切换视图模式
   * 前倾 → 细节模式(高亮注视区域,放大显示)
   * 后仰 → 全局模式(缩小显示整体)
   */
  private handlePostureUpdate(posture: { type: string; angle: number }): void {
    const wasDetailMode = this.isDetailMode;
    
    switch (posture.type) {
      case 'lean_forward':
        this.isDetailMode = true;
        // 细节模式:放大注视点区域
        this.modelTransform.scale = Math.min(2.5, this.modelTransform.scale + 0.05);
        break;
      case 'lean_back':
        this.isDetailMode = false;
        // 全局模式:缩小显示整体
        this.modelTransform.scale = Math.max(0.8, this.modelTransform.scale - 0.03);
        break;
    }

    // 模式切换时触发光效反馈
    if (wasDetailMode !== this.isDetailMode) {
      AppStorage.setOrCreate('view_mode_changed', Date.now());
    }
  }

  /**
   * 切换到下一个材质
   */
  private switchToNextMaterial(): void {
    const currentIndex = this.materialLibrary.findIndex(m => m.name === this.currentMaterial.name);
    const nextIndex = (currentIndex + 1) % this.materialLibrary.length;
    this.currentMaterial = this.materialLibrary[nextIndex];
    
    // 同步光效颜色
    AppStorage.setOrCreate('design_theme_color', this.currentMaterial.color);
  }

  /**
   * 应用材质到当前选中的模型部件
   */
  private applyMaterialToSelection(): void {
    // 实际项目中应调用3D引擎API
    console.info(`Applied material: ${this.currentMaterial.name}`);
    this.triggerHapticFeedback(100);
  }

  /**
   * 微震动反馈
   */
  private triggerHapticFeedback(duration: number): void {
    try {
      import('@kit.SensorServiceKit').then(sensor => {
        sensor.vibrator.startVibration({ type: 'time', duration }, { id: 0 });
      });
    } catch (error) {
      console.error('Haptic feedback failed:', error);
    }
  }

  /**
   * 渲染循环:绘制3D模型(简化2.5D投影)
   */
  private startRenderLoop(): void {
    const loop = () => {
      this.renderFrame();
      this.animationId = requestAnimationFrame(loop);
    };
    this.animationId = requestAnimationFrame(loop);
  }

  private renderFrame(): void {
    if (!this.canvasRef) return;
    const ctx = this.canvasRef;
    const w = ctx.canvas.width;
    const h = ctx.canvas.height;

    // 清空画布
    ctx.fillStyle = '#0a0a12';
    ctx.fillRect(0, 0, w, h);

    // 绘制环境光效(沉浸光感)
    this.drawAmbientLight(ctx, w, h);

    // 绘制注视点高亮区域
    if (this.isDetailMode) {
      this.drawGazeHighlight(ctx, w, h);
    }

    // 绘制3D模型(简化立方体投影)
    this.draw3DModel(ctx, w, h);

    // 绘制UI覆盖层(材质信息、手势状态)
    this.drawUIOverlay(ctx, w, h);
  }

  /**
   * 绘制环境光效(与材质颜色同步)
   */
  private drawAmbientLight(ctx: CanvasRenderingContext2D, w: number, h: number): void {
    const gradient = ctx.createRadialGradient(w/2, h/2, 0, w/2, h/2, Math.max(w, h));
    gradient.addColorStop(0, this.currentMaterial.color + '20');
    gradient.addColorStop(0.5, this.currentMaterial.color + '08');
    gradient.addColorStop(1, 'transparent');
    
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, w, h);
  }

  /**
   * 绘制注视点高亮(细节模式)
   */
  private drawGazeHighlight(ctx: CanvasRenderingContext2D, w: number, h: number): void {
    const gx = this.gazePoint.x * w;
    const gy = this.gazePoint.y * h;
    
    const gradient = ctx.createRadialGradient(gx, gy, 0, gx, gy, 200);
    gradient.addColorStop(0, 'rgba(255,255,255,0.15)');
    gradient.addColorStop(1, 'transparent');
    
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, w, h);

    // 注视点十字准星
    ctx.strokeStyle = 'rgba(255,255,255,0.3)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(gx - 20, gy);
    ctx.lineTo(gx + 20, gy);
    ctx.moveTo(gx, gy - 20);
    ctx.lineTo(gx, gy + 20);
    ctx.stroke();
  }

  /**
   * 绘制3D模型(简化立方体,实际应使用WebGL/Three.js)
   */
  private draw3DModel(ctx: CanvasRenderingContext2D, w: number, h: number): void {
    const centerX = w / 2 + this.modelTransform.translateX;
    const centerY = h / 2 + this.modelTransform.translateY;
    const size = 150 * this.modelTransform.scale;

    ctx.save();
    ctx.translate(centerX, centerY);
    
    // 应用旋转
    const rx = this.modelTransform.rotationX * Math.PI / 180;
    const ry = this.modelTransform.rotationY * Math.PI / 180;
    
    // 绘制立方体正面(简化投影)
    ctx.fillStyle = this.currentMaterial.color + 'CC';
    ctx.globalAlpha = this.currentMaterial.opacity;
    
    // 正面
    ctx.beginPath();
    ctx.moveTo(-size, -size);
    ctx.lineTo(size, -size);
    ctx.lineTo(size, size);
    ctx.lineTo(-size, size);
    ctx.closePath();
    ctx.fill();

    // 顶面(透视效果)
    ctx.fillStyle = this.currentMaterial.color + '99';
    ctx.beginPath();
    ctx.moveTo(-size, -size);
    ctx.lineTo(size, -size);
    ctx.lineTo(size * 0.7, -size * 1.5);
    ctx.lineTo(-size * 0.7, -size * 1.5);
    ctx.closePath();
    ctx.fill();

    // 右侧面
    ctx.fillStyle = this.currentMaterial.color + '66';
    ctx.beginPath();
    ctx.moveTo(size, -size);
    ctx.lineTo(size * 0.7, -size * 1.5);
    ctx.lineTo(size * 0.7, size * 0.5);
    ctx.lineTo(size, size);
    ctx.closePath();
    ctx.fill();

    // 材质高光
    ctx.fillStyle = 'rgba(255,255,255,0.2)';
    ctx.fillRect(-size + 10, -size + 10, size * 0.6, size * 0.3);

    ctx.restore();
  }

  /**
   * 绘制UI覆盖层
   */
  private drawUIOverlay(ctx: CanvasRenderingContext2D, w: number, h: number): void {
    // 材质信息
    ctx.fillStyle = 'rgba(255,255,255,0.8)';
    ctx.font = '14px sans-serif';
    ctx.fillText(`材质: ${this.currentMaterial.name}`, 20, 30);
    ctx.fillText(`缩放: ${this.modelTransform.scale.toFixed(2)}x`, 20, 55);
    ctx.fillText(`模式: ${this.isDetailMode ? '细节' : '全局'}`, 20, 80);
    
    // 手势状态指示
    ctx.fillStyle = this.gestureState !== 'idle' ? '#00FF88' : '#888888';
    ctx.fillText(`手势: ${this.gestureState}`, 20, h - 30);

    // 追踪状态指示器
    const faceDetected = AppStorage.get<boolean>('face_detected') || false;
    const bodyDetected = AppStorage.get<boolean>('body_detected') || false;
    
    ctx.fillStyle = faceDetected ? '#00FF88' : '#FF4444';
    ctx.beginPath();
    ctx.arc(w - 60, 30, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#FFFFFF';
    ctx.fillText('Face AR', w - 45, 35);

    ctx.fillStyle = bodyDetected ? '#00FF88' : '#FF4444';
    ctx.beginPath();
    ctx.arc(w - 60, 55, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#FFFFFF';
    ctx.fillText('Body AR', w - 45, 60);
  }

  build() {
    Canvas(this.canvasRef)
      .width('100%')
      .height('100%')
      .backgroundColor('#0a0a12')
      .onReady((context) => {
        this.canvasRef = context;
      })
  }
}

4.2 AR监控窗口:实时追踪可视化

代码亮点:浮动子窗口实时显示Face AR的68点人脸Mesh和Body AR的骨骼关键点连线,帮助设计师了解AR追踪状态,调试交互映射关系。

typescript 复制代码
// pages/ARMonitorPage.ets
import { Canvas, CanvasRenderingContext2D } from '@kit.ArkUI';

@Component
struct ARMonitorPage {
  private faceCanvasRef: CanvasRenderingContext2D | null = null;
  private bodyCanvasRef: CanvasRenderingContext2D | null = null;
  
  @State faceDetected: boolean = false;
  @State bodyDetected: boolean = false;
  @State expressionData: Map<string, number> = new Map();
  @State gestureData: { type: string; confidence: number } = { type: 'idle', confidence: 0 };

  aboutToAppear(): void {
    this.startDataListening();
    this.startRenderLoop();
  }

  private startDataListening(): void {
    AppStorage.watch('face_detected', (v: boolean) => this.faceDetected = v);
    AppStorage.watch('body_detected', (v: boolean) => this.bodyDetected = v);
    AppStorage.watch('face_expression', (v: Map<string, number>) => this.expressionData = v);
    AppStorage.watch('body_gesture', (v: { type: string; confidence: number }) => this.gestureData = v);
  }

  private startRenderLoop(): void {
    const loop = () => {
      this.renderFacePanel();
      this.renderBodyPanel();
      requestAnimationFrame(loop);
    };
    requestAnimationFrame(loop);
  }

  private renderFacePanel(): void {
    if (!this.faceCanvasRef) return;
    const ctx = this.faceCanvasRef;
    const w = ctx.canvas.width;
    const h = ctx.canvas.height;

    ctx.fillStyle = '#1a1a2e';
    ctx.fillRect(0, 0, w, h);

    // 标题
    ctx.fillStyle = '#FFFFFF';
    ctx.font = 'bold 14px sans-serif';
    ctx.fillText('Face AR 追踪状态', 15, 25);

    // 检测状态
    ctx.fillStyle = this.faceDetected ? '#00FF88' : '#FF4444';
    ctx.beginPath();
    ctx.arc(15, 45, 5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#FFFFFF';
    ctx.fillText(this.faceDetected ? '已检测到人脸' : '未检测到人脸', 28, 50);

    if (this.faceDetected && this.expressionData.size > 0) {
      // 绘制表情参数仪表盘
      let y = 80;
      const expressions = ['EYE_BLINK_LEFT', 'EYE_BROW_UP_LEFT', 'JAW_OPEN', 'MOUTH_SMILE'];
      
      expressions.forEach((expr) => {
        const value = this.expressionData.get(expr) || 0;
        const barWidth = value * 150;
        
        // 参数名
        ctx.fillStyle = '#AAAAAA';
        ctx.font = '11px sans-serif';
        ctx.fillText(expr.replace('EYE_', ''), 15, y);
        
        // 背景条
        ctx.fillStyle = 'rgba(255,255,255,0.1)';
        ctx.fillRect(100, y - 10, 150, 12);
        
        // 数值条
        ctx.fillStyle = value > 0.5 ? '#00FF88' : '#4A90E2';
        ctx.fillRect(100, y - 10, barWidth, 12);
        
        // 数值文本
        ctx.fillStyle = '#FFFFFF';
        ctx.fillText(value.toFixed(2), 260, y);
        
        y += 28;
      });

      // 注视点热力图示意
      ctx.fillStyle = '#FFFFFF';
      ctx.fillText('注视点热力图', 15, y + 10);
      const gaze = AppStorage.get<{ x: number; y: number }>('gaze_point');
      if (gaze) {
        const gx = 100 + gaze.x * 120;
        const gy = y + 30 + gaze.y * 80;
        const gradient = ctx.createRadialGradient(gx, gy, 0, gx, gy, 30);
        gradient.addColorStop(0, 'rgba(255,255,0,0.6)');
        gradient.addColorStop(1, 'transparent');
        ctx.fillStyle = gradient;
        ctx.fillRect(100, y + 20, 120, 80);
      }
    }
  }

  private renderBodyPanel(): void {
    if (!this.bodyCanvasRef) return;
    const ctx = this.bodyCanvasRef;
    const w = ctx.canvas.width;
    const h = ctx.canvas.height;

    ctx.fillStyle = '#1a1a2e';
    ctx.fillRect(0, 0, w, h);

    // 标题
    ctx.fillStyle = '#FFFFFF';
    ctx.font = 'bold 14px sans-serif';
    ctx.fillText('Body AR 追踪状态', 15, 25);

    // 检测状态
    ctx.fillStyle = this.bodyDetected ? '#00FF88' : '#FF4444';
    ctx.beginPath();
    ctx.arc(15, 45, 5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#FFFFFF';
    ctx.fillText(this.bodyDetected ? '已检测到人体' : '未检测到人体', 28, 50);

    if (this.bodyDetected) {
      // 手势状态
      ctx.fillStyle = '#AAAAAA';
      ctx.fillText('当前手势:', 15, 80);
      ctx.fillStyle = this.gestureData.confidence > 0.5 ? '#00FF88' : '#FFFFFF';
      ctx.font = 'bold 16px sans-serif';
      ctx.fillText(`${this.gestureData.type} (${(this.gestureData.confidence * 100).toFixed(0)}%)`, 15, 105);

      // 姿态状态
      const posture = AppStorage.get<{ type: string }>('body_posture');
      if (posture) {
        ctx.fillStyle = '#AAAAAA';
        ctx.font = '12px sans-serif';
        ctx.fillText(`姿态: ${posture.type}`, 15, 135);
      }

      // 骨骼示意图(简化火柴人)
      ctx.strokeStyle = '#4A90E2';
      ctx.lineWidth = 3;
      const cx = w / 2;
      const cy = h / 2 + 20;
      
      // 头部
      ctx.beginPath();
      ctx.arc(cx, cy - 60, 20, 0, Math.PI * 2);
      ctx.stroke();
      
      // 躯干
      ctx.beginPath();
      ctx.moveTo(cx, cy - 40);
      ctx.lineTo(cx, cy + 40);
      ctx.stroke();
      
      // 手臂(动态角度)
      const armAngle = this.gestureData.type === 'spread' ? 0.8 : 
                       this.gestureData.type === 'pinch' ? 0.2 : 0.5;
      
      ctx.beginPath();
      ctx.moveTo(cx, cy - 30);
      ctx.lineTo(cx - 50, cy - 30 + armAngle * 60);
      ctx.stroke();
      
      ctx.beginPath();
      ctx.moveTo(cx, cy - 30);
      ctx.lineTo(cx + 50, cy - 30 + armAngle * 60);
      ctx.stroke();
    }
  }

  build() {
    Column({ space: 12 }) {
      // Face AR 面板
      Column() {
        Canvas(this.faceCanvasRef)
          .width('100%')
          .height(220)
          .backgroundColor('#1a1a2e')
          .borderRadius(12)
          .onReady((context) => {
            this.faceCanvasRef = context;
          })
      }
      .width('100%')
      .padding(12)
      .backgroundColor('rgba(255,255,255,0.05)')
      .borderRadius(16)

      // Body AR 面板
      Column() {
        Canvas(this.bodyCanvasRef)
          .width('100%')
          .height(280)
          .backgroundColor('#1a1a2e')
          .borderRadius(12)
          .onReady((context) => {
            this.bodyCanvasRef = context;
          })
      }
      .width('100%')
      .padding(12)
      .backgroundColor('rgba(255,255,255,0.05)')
      .borderRadius(16)

      // 操作提示
      Column({ space: 8 }) {
        Text('操作指南')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        
        Text('• 挑眉:切换材质')
          .fontSize(12)
          .fontColor('#AAAAAA')
        Text('• 张嘴:确认应用')
          .fontSize(12)
          .fontColor('#AAAAAA')
        Text('• 双手捏合:缩放模型')
          .fontSize(12)
          .fontColor('#AAAAAA')
        Text('• 身体前倾:进入细节模式')
          .fontSize(12)
          .fontColor('#AAAAAA')
      }
      .width('100%')
      .padding(16)
      .backgroundColor('rgba(255,255,255,0.03)')
      .borderRadius(16)
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#0f0f1a')
  }
}

4.3 悬浮控制面板:材质库与沉浸光感

代码亮点:底部悬浮面板整合材质库、手势映射配置和沉浸光效状态指示,采用玻璃拟态材质与主设计窗口的光效实时同步。

typescript 复制代码
// components/SpatialControlPanel.ets
import { window } from '@kit.ArkUI';

interface MaterialItem {
  id: string;
  name: string;
  color: string;
  icon: Resource;
  expression: string; // 绑定的表情触发器
}

@Component
export struct SpatialControlPanel {
  @State selectedMaterial: string = 'metal';
  @State panelExpanded: boolean = false;
  @State themeColor: string = '#C0C0C0';
  @State faceDetected: boolean = false;
  @State bodyDetected: boolean = false;
  @State bottomAvoidHeight: number = 0;

  private materials: MaterialItem[] = [
    { id: 'metal', name: '金属', color: '#C0C0C0', icon: $r('app.media.ic_metal'), expression: '默认' },
    { id: 'glass', name: '玻璃', color: '#88CCFF', icon: $r('app.media.ic_glass'), expression: '挑眉×1' },
    { id: 'wood', name: '木质', color: '#8B4513', icon: $r('app.media.ic_wood'), expression: '挑眉×2' },
    { id: 'ceramic', name: '陶瓷', color: '#F5F5DC', icon: $r('app.media.ic_ceramic'), expression: '挑眉×3' },
    { id: 'carbon', name: '碳纤维', color: '#2C2C2C', icon: $r('app.media.ic_carbon'), expression: '挑眉×4' }
  ];

  aboutToAppear(): void {
    this.getBottomAvoidArea();
    AppStorage.watch('design_theme_color', (color: string) => {
      this.themeColor = color;
    });
    AppStorage.watch('face_detected', (v: boolean) => this.faceDetected = v);
    AppStorage.watch('body_detected', (v: boolean) => this.bodyDetected = v);
  }

  private async getBottomAvoidArea(): Promise<void> {
    try {
      const mainWindow = await window.getLastWindow();
      const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
      this.bottomAvoidHeight = avoidArea.bottomRect.height;
    } catch (error) {
      console.error('Failed to get avoid area:', error);
    }
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      // 主内容占位
      Column() {}
        .width('100%')
        .height('100%')

      // 悬浮控制面板
      Column() {
        // 玻璃拟态背景(多层光效)
        Stack() {
          Column()
            .width('100%')
            .height('100%')
            .backgroundColor(this.themeColor)
            .opacity(0.08)
            .blur(60)

          Column()
            .width('100%')
            .height('100%')
            .backgroundBlurStyle(BlurStyle.COMPONENT_THICK)
            .opacity(0.85)

          Column()
            .width('100%')
            .height('100%')
            .linearGradient({
              direction: GradientDirection.Top,
              colors: [
                ['rgba(255,255,255,0.15)', 0.0],
                ['rgba(255,255,255,0.05)', 0.4],
                ['transparent', 1.0]
              ]
            })
        }
        .width('100%')
        .height('100%')
        .borderRadius(24)
        .shadow({
          radius: 25,
          color: this.themeColor + '30',
          offsetX: 0,
          offsetY: -8
        })

        // 面板内容
        Column({ space: 12 }) {
          // 顶部拖拽指示条
          Column()
            .width(40)
            .height(4)
            .backgroundColor('rgba(255,255,255,0.2)')
            .borderRadius(2)
            .margin({ top: 8 })

          // 追踪状态指示器
          Row({ space: 16 }) {
            Row({ space: 6 }) {
              Column()
                .width(8)
                .height(8)
                .backgroundColor(this.faceDetected ? '#00FF88' : '#FF4444')
                .borderRadius(4)
                .shadow({ radius: 4, color: this.faceDetected ? '#00FF88' : '#FF4444' })
              Text('Face AR')
                .fontSize(11)
                .fontColor(this.faceDetected ? '#00FF88' : '#FF4444')
            }

            Row({ space: 6 }) {
              Column()
                .width(8)
                .height(8)
                .backgroundColor(this.bodyDetected ? '#00FF88' : '#FF4444')
                .borderRadius(4)
                .shadow({ radius: 4, color: this.bodyDetected ? '#00FF88' : '#FF4444' })
              Text('Body AR')
                .fontSize(11)
                .fontColor(this.bodyDetected ? '#00FF88' : '#FF4444')
            }

            // 当前材质显示
            Row({ space: 6 }) {
              Column()
                .width(12)
                .height(12)
                .backgroundColor(this.themeColor)
                .borderRadius(6)
              Text(this.materials.find(m => m.color === this.themeColor)?.name || '金属')
                .fontSize(11)
                .fontColor('#FFFFFF')
            }
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)
          .padding({ top: 8 })

          // 材质库(横向滚动)
          Scroll() {
            Row({ space: 12 }) {
              ForEach(this.materials, (material: MaterialItem) => {
                Column({ space: 6 }) {
                  Stack() {
                    // 选中光晕
                    if (this.selectedMaterial === material.id) {
                      Column()
                        .width(56)
                        .height(56)
                        .backgroundColor(material.color)
                        .borderRadius(16)
                        .opacity(0.3)
                        .blur(12)
                        .animation({
                          duration: 600,
                          curve: Curve.EaseInOut,
                          iterations: -1,
                          playMode: PlayMode.Alternate
                        })
                    }

                    Image(material.icon)
                      .width(44)
                      .height(44)
                      .fillColor(material.color)
                      .opacity(this.selectedMaterial === material.id ? 1.0 : 0.6)
                  }
                  .width(56)
                  .height(56)

                  Text(material.name)
                    .fontSize(11)
                    .fontColor(this.selectedMaterial === material.id ? '#FFFFFF' : '#888888')

                  Text(material.expression)
                    .fontSize(9)
                    .fontColor('rgba(255,255,255,0.4)')
                }
                .width(70)
                .onClick(() => {
                  this.selectedMaterial = material.id;
                  AppStorage.setOrCreate('design_theme_color', material.color);
                })
              })
            }
            .padding({ left: 16, right: 16 })
          }
          .scrollable(ScrollDirection.Horizontal)
          .height(90)

          // 展开面板:手势映射配置
          if (this.panelExpanded) {
            this.buildGestureMappingPanel()
          }
        }
        .width('100%')
        .padding({ bottom: 12 })
      }
      .width('92%')
      .height(this.panelExpanded ? 220 : 130)
      .margin({ 
        bottom: this.bottomAvoidHeight + 16, 
        left: '4%', 
        right: '4%' 
      })
      .animation({
        duration: 350,
        curve: Curve.Spring,
        iterations: 1
      })
      .gesture(
        LongPressGesture({ duration: 400 })
          .onAction(() => {
            this.panelExpanded = !this.panelExpanded;
          })
      )
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildGestureMappingPanel(): void {
    Column({ space: 10 }) {
      Divider()
        .width('90%')
        .color('rgba(255,255,255,0.1)')
        .margin({ top: 4, bottom: 4 })

      Text('手势映射配置')
        .fontSize(13)
        .fontWeight(FontWeight.Medium)
        .fontColor('#FFFFFF')
        .alignSelf(ItemAlign.Start)
        .margin({ left: 16 })

      Row({ space: 20 }) {
        Column({ space: 4 }) {
          Text('🤏 捏合')
            .fontSize(20)
          Text('缩放模型')
            .fontSize(11)
            .fontColor('#AAAAAA')
        }

        Column({ space: 4 }) {
          Text('👆 指示')
            .fontSize(20)
          Text('旋转模型')
            .fontSize(11)
            .fontColor('#AAAAAA')
        }

        Column({ space: 4 }) {
          Text('🙌 张开')
            .fontSize(20)
          Text('放大模型')
            .fontSize(11)
            .fontColor('#AAAAAA')
        }

        Column({ space: 4 }) {
          Text('🙂 挑眉')
            .fontSize(20)
          Text('切换材质')
            .fontSize(11)
            .fontColor('#AAAAAA')
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      .padding({ top: 8, bottom: 8 })
    }
    .width('100%')
  }
}

4.4 主工作台页面:多窗口整合

typescript 复制代码
// pages/SpatialWorkbench.ets
import { DesignCanvas3D } from '../components/DesignCanvas3D';
import { SpatialControlPanel } from '../components/SpatialControlPanel';

@Entry
@Component
struct SpatialWorkbench {
  @State themeColor: string = '#C0C0C0';
  @State viewModeChanged: number = 0;

  aboutToAppear(): void {
    AppStorage.watch('design_theme_color', (color: string) => {
      this.themeColor = color;
    });
    AppStorage.watch('view_mode_changed', (timestamp: number) => {
      this.viewModeChanged = timestamp;
      this.triggerModeChangeEffect();
    });
  }

  /**
   * 视图模式切换时的全屏光效反馈
   */
  private triggerModeChangeEffect(): void {
    // 通过状态变化触发过渡动画
    // 实际项目中可叠加更复杂的光爆特效
  }

  build() {
    Stack() {
      // 第一层:动态环境光背景
      this.buildAmbientBackground()

      // 第二层:3D设计画布
      DesignCanvas3D()
        .width('100%')
        .height('100%')

      // 第三层:顶部状态栏
      this.buildImmersiveStatusBar()

      // 第四层:悬浮控制面板
      SpatialControlPanel()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0a0a12')
    .expandSafeArea(
      [SafeAreaType.SYSTEM],
      [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM, SafeAreaEdge.START, SafeAreaEdge.END]
    )
  }

  @Builder
  buildAmbientBackground(): void {
    Column() {
      // 主光源
      Column()
        .width(600)
        .height(600)
        .backgroundColor(this.themeColor)
        .blur(200)
        .opacity(0.15)
        .position({ x: '50%', y: '40%' })
        .anchor('50%')
        .animation({
          duration: 8000,
          curve: Curve.EaseInOut,
          iterations: -1,
          playMode: PlayMode.Alternate
        })

      // 辅助光点
      Column()
        .width(300)
        .height(300)
        .backgroundColor(this.adjustHue(this.themeColor, 60))
        .blur(150)
        .opacity(0.08)
        .position({ x: '20%', y: '70%' })
        .animation({
          duration: 10000,
          curve: Curve.EaseInOut,
          iterations: -1,
          playMode: PlayMode.AlternateReverse
        })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildImmersiveStatusBar(): void {
    Row() {
      // 项目信息
      Row({ space: 8 }) {
        Image($r('app.media.ic_project'))
          .width(18)
          .height(18)
          .fillColor('#FFFFFF80')
        
        Text('汽车轮毂设计_v3.ark')
          .fontSize(13)
          .fontColor('#FFFFFF80')
      }

      // 空间交互指示
      Row({ space: 12 }) {
        Text('空间交互模式')
          .fontSize(12)
          .fontColor('#00FF8880')
          .backgroundColor('rgba(0,255,136,0.1)')
          .padding({ left: 8, right: 8, top: 2, bottom: 2 })
          .borderRadius(4)

        Text('Face AR + Body AR')
          .fontSize(12)
          .fontColor('#FFFFFF60')
      }

      // 系统信息
      Row({ space: 12 }) {
        Text('60 FPS')
          .fontSize(12)
          .fontColor('#FFFFFF60')
        Text('4K')
          .fontSize(12)
          .fontColor('#FFFFFF60')
      }
    }
    .width('100%')
    .height(44)
    .padding({ left: 20, right: 20 })
    .justifyContent(FlexAlign.SpaceBetween)
    .backgroundBlurStyle(BlurStyle.REGULAR)
    .backgroundColor('rgba(0,0,0,0.3)')
  }

  private adjustHue(color: string, degree: number): string {
    // 简化处理,实际应使用颜色工具库
    return color;
  }
}

五、关键技术总结

5.1 Face AR & Body AR 在PC端的创新映射

AR能力 PC端设计场景映射 交互反馈
Face AR - 挑眉 切换材质库 光效脉冲 + 材质预览更新
Face AR - 张嘴 确认应用材质 全屏闪光 + 微震动
Face AR - 注视点 自动聚焦细节区域 注视点高亮圈
Body AR - 双手捏合 缩放3D模型 模型平滑缩放动画
Body AR - 单手指示 旋转模型 惯性旋转衰减
Body AR - 身体前倾 进入细节模式 环境光增强 + 局部放大

5.2 多窗口AR数据同步架构

复制代码
AR Engine (单实例)
    │
    ├─→ AppStorage全局状态(线程安全)
    │       ├── face_expression → 主窗口材质切换
    │       ├── gaze_point → 主窗口注视高亮
    │       ├── body_gesture → 主窗口模型变换
    │       ├── body_posture → 主窗口视图模式
    │       └── face/body_detected → 监控窗口状态指示
    │
    └─→ 各窗口独立订阅,UI自动刷新

5.3 性能优化策略

typescript 复制代码
// 1. AR数据降频:UI刷新30fps,AR追踪60fps
// 在数据循环中增加节流逻辑
private lastUIUpdate = 0;
if (now - this.lastUIUpdate > 33) { // 30fps
  AppStorage.setOrCreate('face_expression', expressionMap);
  this.lastUIUpdate = now;
}

// 2. 模型变换防抖:手势状态稳定100ms后再应用
// 避免追踪抖动导致的模型抖动

// 3. 离屏渲染:AR监控窗口使用独立渲染线程
// 不影响主设计窗口的渲染帧率

// 4. 自适应追踪质量:根据GPU负载动态调整
// 高负载时降低BlendShape计算精度

六、调试与多设备适配

6.1 PC端AR调试要点

  1. 摄像头标定:PC外接摄像头需进行内参标定,确保3D投影精度
  2. 追踪范围测试:建议追踪距离1-2米,过近会导致面部超出画面
  3. 光照条件:避免逆光,确保面部和手部特征清晰可见
  4. 多窗口性能:AR监控窗口建议限制为15fps,节省GPU资源

6.2 设备适配矩阵

设备类型 摄像头配置 AR能力支持 推荐交互模式
华为MateBook 内置720P Face AR基础 表情驱动材质
华为MateStation 外接4K摄像头 Face+Body全功能 手势+表情组合
第三方PC + RealSense 深度摄像头 增强Body AR精度 精细手势操控

七、总结与展望

本文基于 HarmonyOS 6(API 23)的 Face ARBody AR 能力,结合PC端 悬浮导航沉浸光感 特性,完整实战了一款"空间交互设计工作台"。核心创新点总结:

  1. PC端AR能力拓展:将移动端AR能力扩展至PC大屏场景,利用4K摄像头和高算力实现更精准的追踪和更复杂的交互映射
  2. 多模态融合交互:Face AR(表情+注视点)与Body AR(手势+姿态)并发运行,实现"眼到手到"的无接触设计操控
  3. 专业工作流架构:主设计窗口 + AR监控浮动窗口 + 悬浮控制面板的三层架构,符合专业设计软件的使用习惯
  4. 沉浸光感同步:材质颜色实时驱动全屏环境光效,视图模式切换触发光爆反馈,打造"所见即所感"的沉浸体验

未来扩展方向

  • 接入 分布式软总线,支持设计师与客户跨设备协同评审,双方AR状态实时同步
  • 结合 HarmonyOS PC的多窗口投屏,将AR预览画面投送至大屏,扩展展示空间
  • 引入 AI辅助设计:基于Face AR的注视热力图,AI自动优化模型细节分布
  • 探索 VR/AR头显联动:将PC端AR追踪数据同步至MR头显,实现虚实融合的设计评审

转载自:https://blog.csdn.net/u014727709/article/details/148522388

欢迎 👍点赞✍评论⭐收藏,欢迎指正

相关推荐
liulian09162 小时前
Flutter 网络状态与内容分享库:connectivity_plus 与 share_plus 的 OpenHarmony 适配指南总结
flutter·华为·学习方法·harmonyos
IntMainJhy2 小时前
Flutter 引导页 Onboarding 第三方库 的鸿蒙化适配与实战指南
harmonyos
cy_cy0022 小时前
科技展厅借数字化实现跨越式发展
大数据·科技·人机交互·交互·软件构建
IntMainJhy2 小时前
Flutter WebView 第三方库 内嵌 H5 页面的鸿蒙化适配与实战指南
flutter·华为·harmonyos
南村群童欺我老无力.2 小时前
鸿蒙pc aboutToAppear与onPageShow的执行时机差异
华为·harmonyos
liulian09163 小时前
Flutter for OpenHarmony 用户登录与身份认证功能实现指南
flutter·华为·学习方法·harmonyos
jiejiejiejie_3 小时前
Flutter for OpenHarmony 登录认证小指南:用 Flutter 给鸿蒙 App 安上 “安全小锁”✨
安全·flutter·华为·harmonyos
前端不太难3 小时前
鸿蒙游戏:设备不再是边界
游戏·状态模式·harmonyos
liulian09163 小时前
Flutter Hero 共享元素转场与 animated_text_kit 文字动画库的 OpenHarmony 适配总结
flutter·华为·学习方法·harmonyos