1. Three.js:JavaScript 3D库

1.1 3D渲染原理 & OpenGL & Three.js

  • Three.js是基于WebGL开发的3D图形库,WebGL是OpenGL ES的Web端实现(浏览器原生规范)
OpenGL(桌面端标准)→ OpenGL ES(嵌入式/移动端精简版)→ WebGL(浏览器端规范)→ Three.js(WebGL的封装库)
特性OpenGLThree.js
层级/类型跨平台3D图形底层API标准基于WebGL的JavaScript 3D库
开发语言C/C++(桌面)、GLSL(着色器)JavaScript + GLSL(可选)
核心工作手动操作顶点、纹理、着色器、渲染管线,需要自己实现3D基础逻辑(如相机、模型、光照)内置相机、几何体、光照、材质、渲染器等所有3D基础组件,直接调用API即可
学习成本极高,需要深入理解计算机图形学(如矩阵变换、深度测试、光栅化)极低,无需精通图形学,入门即可做3D效果
运行环境桌面(Windows/Linux/Mac)、移动端、嵌入式(需结合对应语言框架)所有支持WebGL的浏览器(几乎所有现代浏览器),无需额外安装环境
渲染管线支持固定管线(旧版)和可编程管线(核心,需手动写顶点/片元着色器)封装了可编程管线,内置海量材质/着色器,也支持自定义GLSL拓展
  • 注:Three.js不仅封装了基础3D能力,还拥有海量生态(如加载各种3D模型格式glTF/FBX、物理引擎ammo.js、后期处理库postprocessing),能满足绝大多数Web 3D开发需求。

1.2 Three.js主要组件(完整可运行代码实现立方体,约20行)

  • 核心优势:无需写GLSL(除非自定义效果)、无需处理顶点数据、无需编译着色器、无需手动矩阵变换,零基础也能快速实现3D效果,这就是封装的价值。

  • Three.js封装了所有底层操作,内置场景、相机、渲染器、几何体、材质、网格等组件,直接调用即可,无需关注WebGL/OpenGL的底层细节:

// 1. 引入Three.js(CDN/模块化均可)
import * as THREE from 'three';

// 2. 创建核心三要素:场景、相机、渲染器(Three.js内置)
const scene = new THREE.Scene(); // 3D场景容器
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000); // 透视相机
const renderer = new THREE.WebGLRenderer(); // WebGL渲染器(自动封装WebGL)
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 3. 创建立方体:几何体+材质+网格(Three.js内置所有基础组件)
const geometry = new THREE.BoxGeometry(1, 1, 1); // 立方体几何体(自动生成顶点数据)
const material = new THREE.MeshBasicMaterial({ color: 0xff7f50 }); // 基础材质(自动封装着色器)
const cube = new THREE.Mesh(geometry, material); // 网格(几何体+材质结合)
scene.add(cube); // 添加到场景

// 4. 设置相机位置
camera.position.z = 5;

// 5. 渲染循环(自动处理重绘、矩阵变换)
function animate() {
  requestAnimationFrame(animate);
  cube.rotation.x += 0.01; // 立方体旋转
  cube.rotation.y += 0.01;
  renderer.render(scene, camera); // 自动渲染(底层调用WebGL/OpenGL)
}
animate();

2. Kalidokit:面部/身体姿态跟踪与表情驱动库

2.1 Kalidokit简介&整体架构流程

  • Kalidokit是基于JavaScript/TypeScript开发的轻量型姿态解析工具,核心作用是把摄像头捕捉的人体/面部关键点数据,转换成VRM模型可直接驱动的骨骼旋转、表情权重,是Web端开发VRM实时动捕数字人的核心依赖库之一。
  • 流程步骤
摄像头采集画面 → MediaPipe识别关键点 → Kalidokit解析转换 → 输出VRM骨骼/表情数据 → three-vrm驱动模型渲染
┌────────────────────┐
│  用户摄像头 Webcam │
└─────────┬──────────┘
          │
          │ 摄像头视频流
          ▼
┌────────────────────┐
│ <video> HTML 元素  │
│ input_video        │
└─────────┬──────────┘
          │
          │ 每一帧画面
          ▼
┌────────────────────────────┐
│ MediaPipe Holistic (AI)     │
│                            │
│  • 人脸关键点 Face          │
│  • 姿态骨骼 Pose            │
│  • 手部关键点 Hands         │
└─────────┬──────────────────┘
          │
          │ 原始关键点数据
          ▼
┌────────────────────────────┐
│ Kalidokit                   │
│ (运动学解算器)               │
│                            │
│  • 头部旋转 Head Rotation   │
│  • 表情参数 Face Blendshape │
│  • 身体骨骼 Pose Rotation   │
│  • 手指关节 Hand Rotation   │
└─────────┬──────────────────┘
          │
          │ VRM 可用的数据
          ▼
┌────────────────────────────┐
│ Three.js + three-vrm        │
│                            │
│  • 加载 VRM 模型            │
│  • 设置骨骼旋转             │
│  • 设置表情权重             │
└─────────┬──────────────────┘
          │
          │ 实时动画
          ▼
┌────────────────────┐
│ WebGL Canvas       │
│ 浏览器 3D 画面     │
└────────────────────┘

2.2 Kalidokit运行依赖

  • VRM 深度适配:内置了VRM 1.0/0.0规范的骨骼映射规则表情规范(VRM BlendShape),无需手动定义骨骼与姿态的映射关系,解析后的数据可直接对接three-vrm、@pixiv/three-vrm等VRM渲染库;

  • 运行环境:纯前端浏览器运行(基于WebGL/Canvas),无需后端服务,支持Chrome/Firefox等现代浏览器,是Web端VRM数字人实时动捕的首选工具。

  • 核心依赖:Kalidokit本身不做摄像头识别,而是解析MediaPipe(Google开源的视觉识别框架)输出的关键点数据(面部468个关键点、身体33个姿态点、手部21个关键点),做二次处理和坐标/角度转换;

模块类别组件/库名称作用
3D 渲染基础Three.js基于 WebGL 的 3D 渲染引擎,用于在浏览器中创建和显示 3D 场景与模型。
3D 模型加载GLTFLoader加载 .gltf.glb 格式的 3D 模型;VRM 模型基于 GLTF 格式,因此也适用。
相机交互控制OrbitControls实现鼠标拖拽旋转、滚轮缩放、平移等操作,提升用户与 3D 场景的交互体验。
VRM 虚拟角色支持three-vrm (@pixiv/three-vrm)专门用于加载和控制 VRM 格式 的虚拟人物模型,支持骨骼动画、BlendShape 表情等。
人体/人脸 AI 捕捉MediaPipe HolisticGoogle 提供的 AI 模型,通过摄像头实时检测:
• 人脸关键点(表情、眼嘴)
• 全身姿态(骨骼)
• 双手关节(21 点/手)
  • VRM = GLTF + 扩展:VRM 是基于 GLTF 2.0 的扩展格式,专为虚拟人设计,因此需 GLTFLoader + three-vrm 协同工作。
  • MediaPipe Holistic 运行在浏览器端,无需服务器,直接调用摄像头即可实现实时动作捕捉。

2.3 Kalidokit 核心功能模块

Kalidokit将动捕解析分为4大核心模块,完全覆盖VRM数字人的头部、面部、身体、手部驱动需求,每个模块都能输出VRM适配的标准化数据:

2.3.1. 面部表情解析(Face)
  • 解析MediaPipe Face Mesh的468个面部关键点,计算出VRM标准BlendShape表情权重(0~1之间);
  • 内置支持VRM规范的基础表情:开心(Joy)、生气(Anger)、悲伤(Sorrow)、惊讶(Fun)、闭眼(Blink)、张嘴(Aah)、挑眉、嘴角拉伸等,也支持自定义表情映射;
import * as Kalidokit from 'kalidokit';
import { VRM } from '@pixiv/three-vrm';

// 假设已通过MediaPipe获取面部关键点数据faceMeshResults
// 假设已加载并实例化VRM模型:const vrm: VRM;
// Kalidokit解析面部数据(输出头部姿态+表情权重)
const faceResult = Kalidokit.Face.solve(faceMeshResults, {
  runtime: 'mediapipe', // 识别引擎:mediapipe/tfjs
  video: videoElement,  // 摄像头视频元素(用于计算相对坐标)
  faceDetectConfidence: 0.8, // 面部检测置信度阈值
  meshConfidence: 0.8,       // 关键点识别置信度阈值
});
// 驱动VRM表情(BlendShape权重赋值,0~1)
vrm.blendShapeProxy.setValue('Joy', faceResult.expressions.Joy);    // 开心
vrm.blendShapeProxy.setValue('Aah', faceResult.expressions.Aah);    // 张嘴
vrm.blendShapeProxy.setValue('Blink_L', faceResult.expressions.Blink); // 左眼闭眼
vrm.blendShapeProxy.setValue('Blink_R', faceResult.expressions.Blink); // 右眼闭眼
vrm.blendShapeProxy.update(); // 更新表情
  • 额外支持头部姿态跟踪:解析头部的偏航(左右转)、俯仰(上下抬)、侧倾(左右歪)角度,转换成VRM头部骨骼的旋转数据。
// 驱动VRM头部姿态(四元数赋值)
if (vrm.humanoid.getBoneNode('head')) {
  vrm.humanoid.getBoneNode('head').quaternion.set(
    faceResult.head.x, // 侧倾
    faceResult.head.y, // 偏航
    faceResult.head.z, // 俯仰
    faceResult.head.w  // 四元数w分量
  );
}
2.3.2. 身体姿态解析(Pose)
  • 解析MediaPipe Pose的33个人体姿态关键点,计算出全身骨骼的旋转角度(胯部、腰部、胸部、肩膀、手臂、腿部等);
  • 针对VRM骨骼做了反向运动学(IK)简化,无需复杂的IK解算,直接输出欧拉角/四元数,可直接赋值给VRM骨骼;
  • 支持下半身根节点锁定(避免数字人因姿态偏移原地移动),适配VRM数字人“站定动捕”的常见需求。
2.3.3. 手部姿态解析(Hand)
  • 解析MediaPipe Hands的左右手各21个关键点,计算出手指各关节、手腕的旋转角度
  • 适配VRM的手部骨骼规范,支持五指的独立弯曲、张开、旋转驱动,还原精细的手部动作(如比心、点赞、握拳)。
2.3.4. 数据平滑与优化
  • 内置插值平滑算法,解决摄像头识别的“抖动问题”(如面部关键点轻微跳动导致数字人表情僵硬);

  • 支持角度限制,可自定义骨骼旋转的最大范围,避免VRM模型出现骨骼穿模、姿态扭曲的情况;

  • 输出数据支持欧拉角(Euler)、四元数(Quaternion),兼容three.js等3D引擎的骨骼旋转赋值规范。

  • Kalidokit 工具函数(数学辅助):

// 从 Kalidokit 中导入工具函数
const remap = Kalidokit.Utils.remap;   // 数值区间映射
const clamp = Kalidokit.Utils.clamp;   // 限制数值范围
const lerp = Kalidokit.Vector.lerp;    // 线性插值(平滑过渡)
  • remap:把一个范围的值映射到另一个范围
  • clamp:防止数值超出合理范围
  • lerp:让动画“慢慢过渡”,避免抖动

3. 官方示例

  • three-vrm集成示例:https://github.com/pixiv/three-vrm/tree/main/examples(含Kalidokit联动示例);
3.1. HTML部分
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <!-- HTML Meta Tags -->
    <title>KalidoKit - Face and Body Rig Solver</title>
    <meta
      name="description"
      content="Track face and body rigs just your browser webcam!"
    />

    <!-- Twitter Meta Tags -->
    <meta name="twitter:card" content="summary_large_image" />
    <meta property="twitter:domain" content="kit.kalidoface.com" />
    <meta property="twitter:url" content="https://kit.kalidoface.com/" />
    <meta name="twitter:title" content="KalidoKit - Face and Body Rig Solver" />
    <meta
      name="twitter:description"
      content="Track face and body rigs just your browser webcam!"
    />
    <meta
      name="twitter:image"
      content="https://cdn.glitch.com/239c5934-4d83-4c5c-bef6-44dcdb04c8fb%2Fkalidoface-meta.jpg?v=1630110302224"
    />
    <meta
      name="viewport"
      content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"
    />
    <link
      rel="shortcut icon"
      href="https://yeemachine.github.io/kalidoface-live2d-models/Icons/icon-circle.svg"
    />

    <!--     Basic Three.js -->
    <script src="https://unpkg.com/three@0.133.0/build/three.js"></script>
    <!--     GLTF Loader for Three.js -->
    <script src="https://unpkg.com/three@0.133.0/examples/js/loaders/GLTFLoader.js"></script>
    <!--     Orbit Controls for Three.js -->
    <script src="https://unpkg.com/three@0.133.0/examples/js/controls/OrbitControls.js"></script>
    <!--     VRM Loader for Three.js -->
    <script src="https://unpkg.com/@pixiv/three-vrm@0.6.7/lib/three-vrm.js"></script>
    <!--     Mediapipe or Tensorflow.js -->
    <script
      src="https://cdn.jsdelivr.net/npm/@mediapipe/holistic@0.5.1635989137/holistic.js"
      crossorigin="anonymous"
    ></script>

    <!--     Mediapipe Drawing Tools -->
    <script
      src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"
      crossorigin="anonymous"
    ></script>
    <!--     Mediapipe Camera Tools -->
    <script
      src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"
      crossorigin="anonymous"
    ></script>
    <!--     Kalidokit Kinematic Solver -->
    <script src="https://cdn.jsdelivr.net/npm/kalidokit@1.1/dist/kalidokit.umd.js"></script>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div class="preview">
      <video class="input_video" width="1280px" height="720px"  autoplay muted playsinline></video>
      <canvas class="guides"></canvas>
      <section>
        <a href="https://kalidokit-live2d.glitch.me"><p>Live2D</p></a>
        <a class="current" href="https://glitch.com/edit/#!/kalidokit"
          ><p>VRM</p></a
        >
      </section>
    </div>
    <h1 class="notranslate">
      <a href="https://3d.kalidoface.com">kalidoface</a>
    </h1>
    <nav>
      <a href="https://www.npmjs.com/package/kalidokit">
        <img
          src="https://cdn.glitch.me/447b6603-7eae-4da6-957d-73ee30c8e731%2Fnpm.png?v=1635133318451"
        />
      </a>
      <a href="https://github.com/yeemachine/kalidokit">
        <img
          src="https://cdn.glitch.me/447b6603-7eae-4da6-957d-73ee30c8e731%2Fgithub.png?v=1635133310517"
        />
      </a>
      <a href="https://twitter.com/yeemachine">
        <img
          src="https://cdn.glitch.me/447b6603-7eae-4da6-957d-73ee30c8e731%2Ftwitter.png?v=1635133322561"
        />
      </a>
    </nav>
    <p class="linkOut">
      Visit
      <a id="full" href="https://kalidoface.com">the full Vtuber App</a>!
    </p>
    <script src="script.js"></script>
  </body>
</html>
3.1.1 画布&容器
    <div class="preview">
      <video class="input_video" width="1280px" height="720px"  autoplay muted playsinline></video> //`muted`:防止自动播放被浏览器拦截,`playsinline`:iOS 不全屏
      <canvas class="guides"></canvas>
    </div>
3.1.2 video 和 canvas 的关系(简短说明):
  • video.input_video:作为媒体源,播放摄像头流(原始图像帧)。

  • canvas.guides:覆盖在 video 之上,用来绘制检测到的关键点/骨架/调试指示(不会改变视频本身)。

  • 脚本中把 video 作为输入送给 Mediapipe/kalidokit,拿到 landmark 后在 canvas 上用 2D context 绘制(每帧清除 canvas,然后绘制新的 guides,3D类似且更简单)。保证两者尺寸一致并通过 CSS 用 position/z-index 叠放。

  • 简短说明:

    • videoElement (.input_video) 作为摄像头媒体源,传给 Mediapipe(Camera + holistic.send)。
    • guideCanvas (canvas.guides) 用于在每帧上绘制关键点/连接,尺寸通过 videoElement.videoWidth/Height 同步。
  • 每一帧更新数据,实现实时动捕驱动并在2d中就是绘制关键点连接:

// 获取元素
const video = document.querySelector('.input_video');
const canvas = document.querySelector('.guides');
const ctx = canvas.getContext('2d');

// 每帧渲染(回调示例)
function onFrame(landmarks) {
  // 保证 canvas 大小与 video 一致
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;

  // 清除并绘制视频(可选,如果 video 在页面下层则不必画)
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // ctx.drawImage(video, 0, 0, canvas.width, canvas.height);// 将 <video> 元素当前播放的画面绘制到画布上

  // 用 landmarks 绘制点/线
  ctx.fillStyle = 'red';
  landmarks.forEach(pt => {
    ctx.beginPath();
    ctx.arc(pt.x * canvas.width, pt.y * canvas.height, 3, 0, Math.PI * 2);
    ctx.fill();
  });
}

// 在 Mediapipe / camera 回调中调用 onFrame(landmarks)
3.2 脚本部分
3.2.1 加载脚本
  • 所有脚本需按顺序加载(如先加载 three.js,再加载 GLTFLoaderOrbitControls)。
  • 辅助工具
    • drawing_utils:画人脸/骨骼点到 Canvas
    • camera_utils:简化摄像头调用
流程步骤对应代码 / 库
摄像头<video class="input_video">
摄像头控制camera_utils.js
人体识别MediaPipe Holistic
画关键点drawing_utils.js
数据转换Kalidokit
3D 渲染Three.js
VRM 支持three-vrm
主逻辑script.js
3.2.2 完整版运行流程
  • 3DVRM的驱动代码(2D类似):
/**************************************************
 * 一、从 Kalidokit 中导入常用工具函数
 **************************************************/

// Kalidokit控制函数
const remap = Kalidokit.Utils.remap;// remap:把一个数值从一个区间映射到另一个区间
const clamp = Kalidokit.Utils.clamp;// clamp:限制数值在最小值和最大值之间
const lerp = Kalidokit.Vector.lerp;// lerp:线性插值,用于让动画平滑过渡


/**************************************************
 * 二、Three.js 世界初始化
 **************************************************/

// 当前加载并正在使用的 VRM 模型
let currentVrm;

/* ========== 1. 创建渲染器 Renderer ========== */

// 创建 WebGL 渲染器,alpha=true 表示背景透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);// 设置渲染尺寸为窗口大小
renderer.setPixelRatio(window.devicePixelRatio);// 设置像素比,避免高清屏模糊
document.body.appendChild(renderer.domElement);// 把渲染画布插入到页面 body 中;创建渲染器时,Three.js 在内存里生成<canvas> 标签(即renderer.domElement)。

/* ========== 2. 创建相机 Camera ========== */

// 创建透视相机
// 参数:视野角度、宽高比、近裁剪面、远裁剪面
const orbitCamera = new THREE.PerspectiveCamera(
  35,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
// 设置相机位置(前方 + 稍微高一点)
orbitCamera.position.set(0.0, 1.4, 0.7);


/* ========== 3. 轨道控制器 OrbitControls ========== */

// 用鼠标控制相机旋转 / 缩放
const orbitControls = new THREE.OrbitControls(
  orbitCamera,
  renderer.domElement
);

orbitControls.screenSpacePanning = true;// 允许屏幕空间平移
orbitControls.target.set(0.0, 1.4, 0.0);// 相机始终看向角色头部附近
orbitControls.update();// 更新控制器状态


/* ========== 4. 场景 Scene ========== */

// Three.js 的场景容器
const scene = new THREE.Scene();


/* ========== 5. 光源 Light ========== */

// 创建平行光
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(1.0, 1.0, 1.0).normalize();// 设置光源方向并归一化
scene.add(light);// 把光源加入场景


/* ========== 6. 主渲染循环 ========== */

// 用于获取每一帧的时间差(deltaTime)
const clock = new THREE.Clock();

// 主动画循环函数
function animate() {
  // 请求浏览器下一帧
  requestAnimationFrame(animate);

  // 如果 VRM 已加载,更新 VRM 物理(头发、裙子等)
  if (currentVrm) {
    currentVrm.update(clock.getDelta());
  }

  // 渲染场景
  renderer.render(scene, orbitCamera);
}

// 启动渲染循环
animate();


/**************************************************
 * 三、加载 VRM 角色模型
 **************************************************/

// 创建 GLTF 加载器(VRM 本质是 GLTF)
const loader = new THREE.GLTFLoader();

// 允许跨域加载模型
loader.crossOrigin = "anonymous";

// 从 URL 加载 VRM 模型
loader.load(
  "https://cdn.glitch.com/29e07830-2317-4b15-a044-135e73c7f840%2FAshtra.vrm?v=1630342336981",

  // 加载成功回调
  gltf => {
    // 移除 VRM 中不必要的骨骼,提高性能
    THREE.VRMUtils.removeUnnecessaryJoints(gltf.scene);

    // 将 GLTF 转换为 VRM 对象
    THREE.VRM.from(gltf).then(vrm => {
      // 把模型加入场景
      scene.add(vrm.scene);

      // 保存当前 VRM 引用
      currentVrm = vrm;

      // 旋转 180 度,让模型面向摄像头
      currentVrm.scene.rotation.y = Math.PI;
    });
  },

  // 加载进度回调
  progress => {
    console.log(
      "Loading model...",
      100.0 * (progress.loaded / progress.total),
      "%"
    );
  },

  // 加载错误回调
  error => console.error(error)
);


/**************************************************
 * 四、骨骼动画辅助函数
 **************************************************/

/* ========== 1. 骨骼旋转函数 ========== */

// name:骨骼名称(VRM 标准名)
// rotation:旋转角度(x y z)
// dampener:动作幅度缩放
// lerpAmount:插值平滑程度
const rigRotation = (
  name,
  rotation = { x: 0, y: 0, z: 0 },
  dampener = 1,
  lerpAmount = 0.3
) => {
  // 如果模型还没加载,直接返回
  if (!currentVrm) return;

  // 根据骨骼名获取骨骼节点
  const Part = currentVrm.humanoid.getBoneNode(
    THREE.VRMSchema.HumanoidBoneName[name]
  );

  // 如果该骨骼不存在,返回
  if (!Part) return;

  // 创建欧拉角
  let euler = new THREE.Euler(
    rotation.x * dampener,
    rotation.y * dampener,
    rotation.z * dampener
  );

  // 欧拉角转四元数
  let quaternion = new THREE.Quaternion().setFromEuler(euler);

  // 使用 slerp 进行平滑旋转
  Part.quaternion.slerp(quaternion, lerpAmount);
};


/* ========== 2. 骨骼位移函数 ========== */

const rigPosition = (
  name,
  position = { x: 0, y: 0, z: 0 },
  dampener = 1,
  lerpAmount = 0.3
) => {
  if (!currentVrm) return;

  const Part = currentVrm.humanoid.getBoneNode(
    THREE.VRMSchema.HumanoidBoneName[name]
  );

  if (!Part) return;

  // 创建位移向量
  let vector = new THREE.Vector3(
    position.x * dampener,
    position.y * dampener,
    position.z * dampener
  );

  // 平滑插值移动
  Part.position.lerp(vector, lerpAmount);
};


/**************************************************
 * 五、面部驱动(头部 / 表情 / 眼睛)
 **************************************************/

// 保存上一帧的眼球方向,防止抖动
let oldLookTarget = new THREE.Euler();

const rigFace = (riggedFace) => {
  if (!currentVrm) return;

  /* ========== 1. 头部旋转 ========== */
  rigRotation("Neck", riggedFace.head, 0.7);

  /* ========== 2. 表情(BlendShape) ========== */

  const Blendshape = currentVrm.blendShapeProxy;
  const PresetName = THREE.VRMSchema.BlendShapePresetName;

  // 计算左右眼眨眼(VRM:1=闭眼,0=睁眼)
  riggedFace.eye.l = lerp(
    clamp(1 - riggedFace.eye.l, 0, 1),
    Blendshape.getValue(PresetName.Blink),
    0.5
  );
  riggedFace.eye.r = lerp(
    clamp(1 - riggedFace.eye.r, 0, 1),
    Blendshape.getValue(PresetName.Blink),
    0.5
  );

  // 使用 Kalidokit 稳定眨眼
  riggedFace.eye = Kalidokit.Face.stabilizeBlink(
    riggedFace.eye,
    riggedFace.head.y
  );

  // 设置眨眼表情
  Blendshape.setValue(PresetName.Blink, riggedFace.eye.l);

  /* ========== 3. 嘴型(A I U E O) ========== */

  Blendshape.setValue(PresetName.I,
    lerp(riggedFace.mouth.shape.I, Blendshape.getValue(PresetName.I), 0.5)
  );
  Blendshape.setValue(PresetName.A,
    lerp(riggedFace.mouth.shape.A, Blendshape.getValue(PresetName.A), 0.5)
  );
  Blendshape.setValue(PresetName.E,
    lerp(riggedFace.mouth.shape.E, Blendshape.getValue(PresetName.E), 0.5)
  );
  Blendshape.setValue(PresetName.O,
    lerp(riggedFace.mouth.shape.O, Blendshape.getValue(PresetName.O), 0.5)
  );
  Blendshape.setValue(PresetName.U,
    lerp(riggedFace.mouth.shape.U, Blendshape.getValue(PresetName.U), 0.5)
  );

  /* ========== 4. 眼球注视方向 ========== */

  let lookTarget = new THREE.Euler(
    lerp(oldLookTarget.x, riggedFace.pupil.y, 0.4),
    lerp(oldLookTarget.y, riggedFace.pupil.x, 0.4),
    0,
    "XYZ"
  );

  oldLookTarget.copy(lookTarget);

  // 应用眼球朝向
  currentVrm.lookAt.applyer.lookAt(lookTarget);
};


/**************************************************
 * 六、VRM 总动画入口(核心)
 **************************************************/

const animateVRM = (vrm, results) => {
  if (!vrm) return;

  let riggedPose, riggedLeftHand, riggedRightHand, riggedFace;

  // MediaPipe 输出数据
  const faceLandmarks = results.faceLandmarks;
  const pose3DLandmarks = results.ea;
  const pose2DLandmarks = results.poseLandmarks;
  const leftHandLandmarks = results.rightHandLandmarks;
  const rightHandLandmarks = results.leftHandLandmarks;

  /* ========== 面部动画 ========== */
  if (faceLandmarks) {
    riggedFace = Kalidokit.Face.solve(faceLandmarks, {
      runtime: "mediapipe",
      video: videoElement
    });
    rigFace(riggedFace);
  }

  /* ========== 身体动画 ========== */
  if (pose2DLandmarks && pose3DLandmarks) {
    riggedPose = Kalidokit.Pose.solve(
      pose3DLandmarks,
      pose2DLandmarks,
      {
        runtime: "mediapipe",
        video: videoElement
      }
    );

    // 髋部旋转 + 位移
    rigRotation("Hips", riggedPose.Hips.rotation, 0.7);
    rigPosition(
      "Hips",
      {
        x: -riggedPose.Hips.position.x,
        y: riggedPose.Hips.position.y + 1,
        z: -riggedPose.Hips.position.z
      },
      1,
      0.07
    );

    // 脊柱
    rigRotation("Chest", riggedPose.Spine, 0.25, 0.3);
    rigRotation("Spine", riggedPose.Spine, 0.45, 0.3);

    // 四肢
    rigRotation("RightUpperArm", riggedPose.RightUpperArm);
    rigRotation("RightLowerArm", riggedPose.RightLowerArm);
    rigRotation("LeftUpperArm", riggedPose.LeftUpperArm);
    rigRotation("LeftLowerArm", riggedPose.LeftLowerArm);

    rigRotation("LeftUpperLeg", riggedPose.LeftUpperLeg);
    rigRotation("LeftLowerLeg", riggedPose.LeftLowerLeg);
    rigRotation("RightUpperLeg", riggedPose.RightUpperLeg);
    rigRotation("RightLowerLeg", riggedPose.RightLowerLeg);
  }

  /* ========== 手部动画(左右手) ========== */
  if (leftHandLandmarks) {
    riggedLeftHand = Kalidokit.Hand.solve(leftHandLandmarks, "Left");
    rigRotation("LeftHand", {
      z: riggedPose.LeftHand.z,
      y: riggedLeftHand.LeftWrist.y,
      x: riggedLeftHand.LeftWrist.x
    });
  }

  if (rightHandLandmarks) {
    riggedRightHand = Kalidokit.Hand.solve(rightHandLandmarks, "Right");
    rigRotation("RightHand", {
      z: riggedPose.RightHand.z,
      y: riggedRightHand.RightWrist.y,
      x: riggedRightHand.RightWrist.x
    });
  }
};


/**************************************************
 * 七、MediaPipe Holistic 初始化
 **************************************************/

let videoElement = document.querySelector(".input_video");
let guideCanvas = document.querySelector("canvas.guides");

// MediaPipe 每帧回调
const onResults = (results) => {
  drawResults(results);          // 画调试点
  animateVRM(currentVrm, results); // 驱动 VRM
};

// 创建 Holistic 实例
const holistic = new Holistic({
  locateFile: file =>
    `https://cdn.jsdelivr.net/npm/@mediapipe/holistic@0.5.1635989137/${file}`
});

// 设置识别参数
holistic.setOptions({
  modelComplexity: 1,
  smoothLandmarks: true,
  minDetectionConfidence: 0.7,
  minTrackingConfidence: 0.7,
  refineFaceLandmarks: true
});

// 注册回调
holistic.onResults(onResults);


/**************************************************
 * 八、启动摄像头
 **************************************************/

const camera = new Camera(videoElement, {
  onFrame: async () => {
    // 把当前视频帧送入 MediaPipe
    await holistic.send({ image: videoElement });
  },
  width: 640,
  height: 480
});

// 启动摄像头
camera.start();
3.2.3 测试效果

在这里插入图片描述

CG

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐