React + three.js 实现人脸动捕与3D模型表情同步

news2024/11/27 9:51:04

系列文章目录

  1. React 使用 three.js 加载 gltf 3D模型 | three.js 入门
  2. React + three.js 3D模型骨骼绑定
  3. React + three.js 3D模型面部表情控制
  4. React + three.js 实现人脸动捕与3D模型表情同步

示例项目(github):https://github.com/couchette/simple-react-three-facial-expression-sync-demo
示例项目(gitcode):https://gitcode.com/qq_41456316/simple-react-three-facial-expression-sync-demo


文章目录

  • 系列文章目录
  • 前言
  • 一、实现步骤
    • 1、创建项目配置环境
    • 2. 创建组件
    • 3. 使用组件
    • 4. 运行项目
  • 总结
    • 程序预览


前言

在本系列的上一篇文章中,我们已经探讨了如何在 React 中利用 three.js 来操作模型面部表情,现在,我们将深入研究如何结合人脸特征点检测与模型表情控制实现人脸动作步骤并与3D模型表情同步。让我们一同探索如何赋予你的 3D 模型更加生动和丰富的表情吧!


一、实现步骤

1、创建项目配置环境

使用 create-reacte-app 创建项目

npx create-react-app simple-react-three-facial-expression-sync-demo
cd simple-react-three-facial-expression-sync-demo

安装three.js

npm i three
npm i @mediapipe/tasks-vision

将示例项目中的public中的内容复制到新创建的项目的public中(相关的模型文件)

2. 创建组件

src目录创建components文件夹,在components文件夹下面创建ThreeContainer.js文件。
首先创建组件,并获取return 元素的ref

import * as THREE from "three";
import { useRef, useEffect } from "react";

function ThreeContainer() {
  const containerRef = useRef(null);
  const isContainerRunning = useRef(false);
  return <div ref={containerRef} />;
}

export default ThreeContainer;

接着将three.js自动创建渲染元素添加到return组件中为子元素(可见container.appendChild(renderer.domElement);),相关逻辑代码在useEffect中执行,完整代码内容如下

import * as THREE from "three";

import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";

import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js";
import { MeshoptDecoder } from "three/addons/libs/meshopt_decoder.module.js";

import { GUI } from "three/addons/libs/lil-gui.module.min.js";
import { useRef, useEffect } from "react";

// Mediapipe

import { FaceLandmarker, FilesetResolver } from "@mediapipe/tasks-vision";

const blendshapesMap = {
  // '_neutral': '',
  browDownLeft: "browDown_L",
  browDownRight: "browDown_R",
  browInnerUp: "browInnerUp",
  browOuterUpLeft: "browOuterUp_L",
  browOuterUpRight: "browOuterUp_R",
  cheekPuff: "cheekPuff",
  cheekSquintLeft: "cheekSquint_L",
  cheekSquintRight: "cheekSquint_R",
  eyeBlinkLeft: "eyeBlink_L",
  eyeBlinkRight: "eyeBlink_R",
  eyeLookDownLeft: "eyeLookDown_L",
  eyeLookDownRight: "eyeLookDown_R",
  eyeLookInLeft: "eyeLookIn_L",
  eyeLookInRight: "eyeLookIn_R",
  eyeLookOutLeft: "eyeLookOut_L",
  eyeLookOutRight: "eyeLookOut_R",
  eyeLookUpLeft: "eyeLookUp_L",
  eyeLookUpRight: "eyeLookUp_R",
  eyeSquintLeft: "eyeSquint_L",
  eyeSquintRight: "eyeSquint_R",
  eyeWideLeft: "eyeWide_L",
  eyeWideRight: "eyeWide_R",
  jawForward: "jawForward",
  jawLeft: "jawLeft",
  jawOpen: "jawOpen",
  jawRight: "jawRight",
  mouthClose: "mouthClose",
  mouthDimpleLeft: "mouthDimple_L",
  mouthDimpleRight: "mouthDimple_R",
  mouthFrownLeft: "mouthFrown_L",
  mouthFrownRight: "mouthFrown_R",
  mouthFunnel: "mouthFunnel",
  mouthLeft: "mouthLeft",
  mouthLowerDownLeft: "mouthLowerDown_L",
  mouthLowerDownRight: "mouthLowerDown_R",
  mouthPressLeft: "mouthPress_L",
  mouthPressRight: "mouthPress_R",
  mouthPucker: "mouthPucker",
  mouthRight: "mouthRight",
  mouthRollLower: "mouthRollLower",
  mouthRollUpper: "mouthRollUpper",
  mouthShrugLower: "mouthShrugLower",
  mouthShrugUpper: "mouthShrugUpper",
  mouthSmileLeft: "mouthSmile_L",
  mouthSmileRight: "mouthSmile_R",
  mouthStretchLeft: "mouthStretch_L",
  mouthStretchRight: "mouthStretch_R",
  mouthUpperUpLeft: "mouthUpperUp_L",
  mouthUpperUpRight: "mouthUpperUp_R",
  noseSneerLeft: "noseSneer_L",
  noseSneerRight: "noseSneer_R",
  // '': 'tongueOut'
};

function ThreeContainer() {
  const containerRef = useRef(null);
  const isContainerRunning = useRef(false);

  useEffect(() => {
    if (!isContainerRunning.current && containerRef.current) {
      isContainerRunning.current = true;
      init();
    }

    async function init() {
      const renderer = new THREE.WebGLRenderer({ antialias: true });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.toneMapping = THREE.ACESFilmicToneMapping;
      containerRef.current.appendChild(renderer.domElement);

      const camera = new THREE.PerspectiveCamera(
        60,
        window.innerWidth / window.innerHeight,
        1,
        100
      );
      camera.position.z = 5;

      const scene = new THREE.Scene();
      scene.scale.x = -1;

      const environment = new RoomEnvironment(renderer);
      const pmremGenerator = new THREE.PMREMGenerator(renderer);

      scene.background = new THREE.Color(0x666666);
      scene.environment = pmremGenerator.fromScene(environment).texture;

      const controls = new OrbitControls(camera, renderer.domElement);

      // Face

      let face, eyeL, eyeR;
      const eyeRotationLimit = THREE.MathUtils.degToRad(30);

      const ktx2Loader = new KTX2Loader()
        .setTranscoderPath("/basis/")
        .detectSupport(renderer);

      new GLTFLoader()
        .setKTX2Loader(ktx2Loader)
        .setMeshoptDecoder(MeshoptDecoder)
        .load("models/facecap.glb", (gltf) => {
          const mesh = gltf.scene.children[0];
          scene.add(mesh);

          const head = mesh.getObjectByName("mesh_2");
          head.material = new THREE.MeshNormalMaterial();

          face = mesh.getObjectByName("mesh_2");
          eyeL = mesh.getObjectByName("eyeLeft");
          eyeR = mesh.getObjectByName("eyeRight");

          // GUI

          const gui = new GUI();
          gui.close();

          const influences = head.morphTargetInfluences;

          for (const [key, value] of Object.entries(
            head.morphTargetDictionary
          )) {
            gui
              .add(influences, value, 0, 1, 0.01)
              .name(key.replace("blendShape1.", ""))
              .listen(influences);
          }

          renderer.setAnimationLoop(animation);
        });

      // Video Texture

      const video = document.createElement("video");

      // const texture = new THREE.VideoTexture(video);
      // texture.colorSpace = THREE.SRGBColorSpace;

      const geometry = new THREE.PlaneGeometry(1, 1);
      const material = new THREE.MeshBasicMaterial({
        // map: texture,
        depthWrite: false,
      });
      const videomesh = new THREE.Mesh(geometry, material);
      scene.add(videomesh);

      // MediaPipe

      const filesetResolver = await FilesetResolver.forVisionTasks(
        // "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm"
        "fileset_resolver/wasm"
      );

      const faceLandmarker = await FaceLandmarker.createFromOptions(
        filesetResolver,
        {
          baseOptions: {
            modelAssetPath:
              // "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task",
              "ai_models/face_landmarker.task",
            delegate: "GPU",
          },
          outputFaceBlendshapes: true,
          outputFacialTransformationMatrixes: true,
          runningMode: "VIDEO",
          numFaces: 1,
        }
      );

      if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices
          .getUserMedia({ video: { facingMode: "user" } })
          .then(function (stream) {
            video.srcObject = stream;
            video.play();
          })
          .catch(function (error) {
            console.error("Unable to access the camera/webcam.", error);
          });
      }

      const transform = new THREE.Object3D();

      function animation() {
        if (video.readyState >= HTMLMediaElement.HAVE_METADATA) {
          const results = faceLandmarker.detectForVideo(video, Date.now());
          console.log(results);

          if (results.facialTransformationMatrixes.length > 0) {
            const facialTransformationMatrixes =
              results.facialTransformationMatrixes[0].data;

            transform.matrix.fromArray(facialTransformationMatrixes);
            transform.matrix.decompose(
              transform.position,
              transform.quaternion,
              transform.scale
            );

            const object = scene.getObjectByName("grp_transform");

            object.position.x = transform.position.x;
            object.position.y = transform.position.z + 40;
            object.position.z = -transform.position.y;

            object.rotation.x = transform.rotation.x;
            object.rotation.y = transform.rotation.z;
            object.rotation.z = -transform.rotation.y;
          }

          if (results.faceBlendshapes.length > 0) {
            const faceBlendshapes = results.faceBlendshapes[0].categories;

            // Morph values does not exist on the eye meshes, so we map the eyes blendshape score into rotation values
            const eyeScore = {
              leftHorizontal: 0,
              rightHorizontal: 0,
              leftVertical: 0,
              rightVertical: 0,
            };

            for (const blendshape of faceBlendshapes) {
              const categoryName = blendshape.categoryName;
              const score = blendshape.score;

              const index =
                face.morphTargetDictionary[blendshapesMap[categoryName]];

              if (index !== undefined) {
                face.morphTargetInfluences[index] = score;
              }

              // There are two blendshape for movement on each axis (up/down , in/out)
              // Add one and subtract the other to get the final score in -1 to 1 range
              switch (categoryName) {
                case "eyeLookInLeft":
                  eyeScore.leftHorizontal += score;
                  break;
                case "eyeLookOutLeft":
                  eyeScore.leftHorizontal -= score;
                  break;
                case "eyeLookInRight":
                  eyeScore.rightHorizontal -= score;
                  break;
                case "eyeLookOutRight":
                  eyeScore.rightHorizontal += score;
                  break;
                case "eyeLookUpLeft":
                  eyeScore.leftVertical -= score;
                  break;
                case "eyeLookDownLeft":
                  eyeScore.leftVertical += score;
                  break;
                case "eyeLookUpRight":
                  eyeScore.rightVertical -= score;
                  break;
                case "eyeLookDownRight":
                  eyeScore.rightVertical += score;
                  break;
              }
            }

            eyeL.rotation.z = eyeScore.leftHorizontal * eyeRotationLimit;
            eyeR.rotation.z = eyeScore.rightHorizontal * eyeRotationLimit;
            eyeL.rotation.x = eyeScore.leftVertical * eyeRotationLimit;
            eyeR.rotation.x = eyeScore.rightVertical * eyeRotationLimit;
          }
        }

        videomesh.scale.x = video.videoWidth / 100;
        videomesh.scale.y = video.videoHeight / 100;

        renderer.render(scene, camera);

        controls.update();
      }

      window.addEventListener("resize", function () {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize(window.innerWidth, window.innerHeight);
      });
    }
  }, []);
  return <div ref={containerRef} />;
}

export default ThreeContainer;


3. 使用组件

修改App.js的内容如下

import "./App.css";
import ThreeContainer from "./components/ThreeContainer";

function App() {
  return (
    <div>
      <ThreeContainer />
    </div>
  );
}

export default App;


4. 运行项目

运行项目 npm start最终效果如下,模型会随着相机拍摄的人脸表情而变化,拍摄的图像显示部分的代码我已经注释掉了,如果想结合实际图像对比,可以放开相关注释。
请添加图片描述


总结

通过本文的介绍,相信读者对于在 React 中实现人脸动捕和3D模型表情同步有了初步的了解。如果你对此感兴趣,不妨动手尝试一下,可能会有意想不到的收获。同时,也欢迎大家多多探索,将 React 和 Three.js 的强大功能发挥到极致,为网页应用增添更多的乐趣和惊喜。

程序预览

{正在筹备}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1590649.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Godot4自学手册】第三十六节圆形移动或扇形移动的铁球

在第三十四节我实现了来回无限滚动的伤害铁刺球&#xff0c;这一节我准备实现一个圆形移动或扇形移动&#xff0c;并带有链条的铁球。效果如下&#xff1a; 一、实现原理 绕一点做圆周运动&#xff0c;简单的说就是&#xff1a; 每一帧根据旋转的角度计算出下一个位置的坐标…

R语言绘图:绘制横向柱状图

代码主要实现&#xff1a; 对数据进行排序&#xff0c;并且相同分组的数据会有相同的颜色。最后&#xff0c;绘制横向柱状图。 # 加载ggplot2包 library(ggplot2)# 示例数据&#xff0c;假设有三列&#xff1a;Group, Variable, Value data <- data.frame(Group factor(c(…

植被参数光学遥感反演方法(Python)及遥感与生态模型数据同化算法

传统的地面实测方法能够得到比较准确的植被参数&#xff08;如叶面积指数、覆盖度、生物量、叶绿素、干物质、叶片含水量、FPAR等&#xff09;&#xff0c;但其获取信息有限&#xff0c;难以满足大范围提取植被参数的需求&#xff0c;尤其在异质地表区域。遥感技术的发展为植被…

【PythonCode】力扣Leetcode11~15题Python版

【PythonCode】力扣Leetcode11~15题Python版 前言 力扣Leetcode是一个集学习、刷题、竞赛等功能于一体的编程学习平台&#xff0c;很多计算机相关专业的学生、编程自学者、IT从业者在上面学习和刷题。 在Leetcode上刷题&#xff0c;可以选择各种主流的编程语言&#xff0c;如C…

316_C++_xml文件解析成map,可以放到表格上 + xml、xlsx文件互相解析

xml文件例如&#xff1a; <?xml version"1.0" encoding"UTF-8" standalone"yes"?> <TrTable> <tr id"0" label"TR_PB_CH" text"CH%2"/> <tr id"4" label"TR_PB_CHN"…

[BT]BUUCTF刷题第16天(4.12)

第16天 Web [MRCTF2020]Ezpop 打开网站就是一段泄露的源代码&#xff1a; <?php //flag is in flag.php //WTF IS THIS? //Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95 //And Crack…

Ollama、FastGPT大模型RAG结合使用案例

参考: https://ollama.com/download/linux https://doc.fastai.site/docs/intro/ https://blog.csdn.net/m0_71142057/article/details/136738997 https://doc.fastgpt.run/docs/development/custom-models/m3e/ Ollama作为后端大模型加载运行 FastGPT作为前端页面聊天集成RA…

Linux函数学习 select

1、Linux select 函数 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); nfds 最大文件fd 1 readfds 监听可读文件集合fd writefds 监听可写文件集合fd exceptfd 监听异常文件集…

[蓝桥杯] 岛屿个数(C语言)

提示&#xff1a; 橙色字体为需要注意部分&#xff0c;红色字体为难点部分&#xff0c;会在文章“重难点解答”部分精讲。 题目链接 蓝桥杯2023年第十四届省赛真题-岛屿个数 - C语言网 题目理解 这道题让我们求岛屿个数&#xff0c;那么我们就应该先弄懂&#xff0c;对于一…

R: 支持向量机(Support Vector Machine,简称SVM)

在数据科学和机器学习领域中&#xff0c;支持向量机&#xff08;Support Vector Machine&#xff0c;简称SVM&#xff09;是一种强大的监督学习算法&#xff0c;常用于分类和回归分析。它的优点之一是可以适用于复杂的数据集&#xff0c;并且在高维空间中表现良好。在本文中&am…

SpringBoot3 + Vue3 + Uniapp + uView + Elenment 实现动态二级分类以及二级分类的管理

SpringBoot3 Vue3 Uniapp uView Elenment 实现动态二级分类以及二级分类的管理 1. 效果展示1.1 前端显示效果1.2 后台管理一级分类1.3 后台管理二级分类 2. 后端代码2.1 GoodsCategoryController.java2.2.1 GoodsCategoryMapper.java2.2.2 GoodsCategorySonMapper.java2.3.…

蓝桥杯备赛(C/C++组)

README&#xff1a; 本笔记是自己的备考笔记&#xff0c;按照官网提纲进行复习&#xff01;适合有基础&#xff0c;复习用。 一、总考点 试题考查选手解决实际问题的能力&#xff0c;对于结果填空题&#xff0c;选手可以使用手算、软件、编程等方法解决&#xff0c;对于编程大…

Laravel 11入门:使用ServBay打造高效开发环境

Laravel 11发布&#xff0c;改进了不少功能。 它引入了更加流畅的应用结构、每秒限速、健康路由等特性。 此外&#xff0c;Laravel还推出了第一方可扩展的WebSocket服务器Laravel Reverb&#xff0c;为你的应用提供强大的实时功能。 在今天的指南中&#xff0c;我将设置一个…

OSPF中配置VLAN通信(单臂路由)

OSPF中配置VLAN通信&#xff08;单臂路由&#xff09; 单臂路由&#xff08;One-Arm Routing&#xff09;是一种网络路由配置方式&#xff0c;常用于解决网络中的特定问题。在传统的网络架构中&#xff0c;路由器通常需要连接到多个子网或网络段&#xff0c;每个子网都需要一个…

项目管理工具——使用甘特图制定项目计划的详细步骤

甘特图是一种直观的项目管理工具&#xff0c;它有助于我们清晰地展示任务安排、时间管理和项目的进度。以下是使用甘特图制定项目计划的详细步骤&#xff1a; 1、创建项目&#xff1a;首先&#xff0c;在进度猫中创建新的项目&#xff0c;并设置项目的时间、工作日等参数。根据…

test4132

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

14届蓝桥杯 C/C++ B组 T6 岛屿个数 (BFS,FloodFill,填色)

首先拿到这道题不要想着去直接判断环里面的岛屿&#xff0c;这样太困难了&#xff0c;我们可以使用之前做过的题的经验&#xff0c;在输入加入一圈海水&#xff0c;然后从(0,0)点开始BFS&#xff0c;这里进行八向搜索&#xff0c;搜到的0全部都染色成2&#xff0c;假如2能够蔓延…

GEE数据集——巴基斯坦国家级土壤侵蚀数据集(2005 年和 2015 年)

简介 巴基斯坦国家级土壤侵蚀数据集&#xff08;2005 年和 2015 年&#xff09; 该数据集采用修订的通用土壤流失方程 (RUSLE)&#xff0c;并考虑了六个关键影响因素&#xff1a;降雨侵蚀率 (R)、土壤可侵蚀性 (K)、坡长 (L)、坡陡 (S)、覆盖管理 (C) 和保护措施 (P)&#xff…

机器人瓶胚检测工作站(H3U脉冲轴控制)

1、变量定义 2、程序监控1 2、 程序监控2 3、程序监控3 机器人输送料和机构的动作安全尤为重要&#xff0c;下面我们讨论下安全联锁控制逻辑

C++设计模式|0.前言

1.什么是设计模式&#xff1f; 简答来说&#xff0c;设计模式就是一套好用的代码经验总结&#xff0c;也就是怎么写好代码的方法论。使用设计模式是为了可重用代码、让代码更容易被他人理解、提高代码的可靠性。 2.设计模式的分类 设计模式可以分为三类&#xff1a;创建型、…