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

系列文章目录

  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 的强大功能发挥到极致,为网页应用增添更多的乐趣和惊喜。

程序预览

{正在筹备}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Rust跨平台编译

❝ 如果你感觉自己被困住了&#xff0c;焦虑并充满消极情绪&#xff0c;生命出现了停滞&#xff0c;那么治疗方法很简单&#xff1a;「做点什么」。 ❞ 大家好&#xff0c;我是「柒八九」。一个「专注于前端开发技术/Rust及AI应用知识分享」的Coder 前言 之前我们不是写了一篇R…

SpringBoot 微服务token 传递实现

1、前言 随着微服务的流行&#xff0c;微服务之间的安全以及业务需要&#xff0c;都需要穿递token &#xff0c;而token的传递一般通过header 头来传递。从架构的角度来讲 &#xff0c;一般的企业应用都由nginx、业务网关和各个微服务组成。这个nginx 传递header 我就不讲述。下…

Java | Leetcode Java题解之第25题K个一组翻转链表

题目&#xff1a; 题解&#xff1a; class Solution {public ListNode reverseKGroup(ListNode head, int k) {ListNode hair new ListNode(0);hair.next head;ListNode pre hair;while (head ! null) {ListNode tail pre;// 查看剩余部分长度是否大于等于 kfor (int i 0…

Slf4j+Log4j简单使用

Slf4jLog4j简单使用 文章目录 Slf4jLog4j简单使用一、引入依赖二、配置 log4j2.xml2.1 配置结构2.2 配置文件 三、使用四、使用MDC完成日志ID4.1 程序入口处4.2 配置文件配置打印4.3 多线程日志ID传递配置 五. 官网 一、引入依赖 <dependencies><dependency><g…

24年山东省三支一扶网上报名流程及照片要求

山东省终于公布了本年度的“三支一扶”招募计划&#xff0c;本次共计招募1350人 报名人员可登录《山东人事考试信息网》查看本次三支一扶报名公告以及报名。 报名时间&#xff1a;2024年4月16日9:00—4月20日16:00 查询时间&#xff1a;2024年4月16日11:00—4月21日16:00 在…

Django中的静态文件、路径、访问静态文件的方法

1.什么是静态文件 不能与服务器端做动态交互的文件都是静态文件 如:图片,css,js,音频,视频,html文件(部分) 2.静态文件配置 在 settings.py 中配置一下两项内容: 1.配置静态文件的访问路径 通过哪个url地址找静态文件 STATIC_URL ‘/static/’ 说…

OSCP靶场--Nukem

OSCP靶场–Nukem 考点(公共exp反弹shell密码复用ssh端口转发dosbox suid提权) 1.nmap扫描 ┌──(root㉿kali)-[~/Desktop] └─# nmap -sV -sC 192.168.158.105 -Pn --min-rate 2500 Starting Nmap 7.92 ( https://nmap.org ) at 2024-04-12 01:33 EDT RTTVAR has grown to…

【springboot开发】MVC和SSM

前言&#xff1a;关于MVC和SSM基本内容的梳理&#xff0c;以及两者之间的关系。 文章目录 1. 三层架构2. MVC3. SSM 1. 三层架构 三层架构是指&#xff1a; 视图层view&#xff08;表现层&#xff09;: 用于显示数据和接收用户输入的数据&#xff0c;为用户提供一种交互式操作…

ABAP ADBC_QUERY 测试代码

项目中使用的接口取数采用的是DBLink的方式&#xff0c;对方提供的表名太长&#xff0c;超过标准程序ADBC_QUERY的参数长度&#xff0c;于是写了一份简单的测试代码用来测试连接和取数。 DBCO配置&#xff1a; 测试程序&#xff1a; 程序源码&#xff1a; *&------------…

基于java+springboot+vue实现的居家养老健康管理系统(文末源码+Lw)23-313

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装智慧社区居家养老健康管理系统软件来发挥其高效地信息处理…

【opencv】示例-peopledetect.cpp HOG(方向梯度直方图)描述子和SVM(支持向量机)进行行人检测...

// 包含OpenCV项目所需的objdetect模块头文件 #include <opencv2/objdetect.hpp> // 包含OpenCV项目所需的highgui模块头文件&#xff0c;用于图像的显示和简单操作 #include <opencv2/highgui.hpp> // 包含OpenCV项目所需的imgproc模块头文件&#xff0c;用于图像…

streamlit 大模型前段界面

结合 langchain 一起使用的工具&#xff0c;可以显示 web 界面 pip install streamlit duckduckgo-search 运行命令 streamlit run D:\Python_project\NLP\大模型学习\test.py import os from dotenv import load_dotenv from langchain_community.llms import Tongyi load…

武汉星起航:跨境电商领域的佼佼者,专业团队引领行业新高度

在跨境电商这片充满机遇与挑战的广阔天地中&#xff0c;众多企业纷纷崭露头角&#xff0c;竞相追逐市场份额。然而&#xff0c;在这样一个充满竞争的环境中&#xff0c;武汉星起航电子商务有限公司凭借其坚定的战略眼光和专业的团队实力&#xff0c;稳健地立足于市场&#xff0…

杰发科技AC7840——CAN通信简介(3)_时间戳

0. 时间戳简介 时间戳表示的是收到该CAN消息的时刻&#xff0c;通过连续多帧的时间戳&#xff0c;可以计算出CAN消息的发送周期&#xff0c;也可以用于判断CAN消息是否被持续收到。 1. 使用步骤 注意分别是发送和接收的功能&#xff1a; 2. 现象分析 看下寄存器的情况&#x…

引领智能互联时代,紫光展锐赋能百业创新发展

随着5G技术的快速发展&#xff0c;各行各业对通信技术的需求也在不断升级。紫光展锐持续深耕5G垂直行业&#xff0c;不断推进5G标准演进&#xff0c;从R15到R16&#xff0c;再到R17&#xff0c;展锐携手生态合作伙伴&#xff0c;不断推出创新性解决方案&#xff0c;在5G RedCap…

二叉树之建树

树结构如下所示。 class TreeNode{int val;TreeNode left;TreeNode right;public TreeNode(){};public TreeNode(int val){this.val val;} }二叉树的建树逻辑一般可以采用后序遍历的逻辑&#xff0c;先创建父结点&#xff0c;然后通过递归的方式得到左右孩子结点&#xff0c;…

04-使用Docker镜像和仓库

回忆一下之前的创建镜像命令&#xff1a; [rootnode2 /]# docker run -i -t --name another_centos7 centos:7 /bin/bash这个命令从centos7的镜像创建一个名为another_centos7的容器&#xff0c;并且启动bash界面 什么是Docker镜像 Docker镜像是由文件系统叠加而成的。 底层…

Centos 7.9.2009 下 Gitlab 完全卸载

一、linux版本&#xff1a;lsb_release -a 二、GtiLab 版本 # 查看gitlab的版本号 cat /opt/gitlab/embedded/service/gitlab-rails/VERSION 三、开始卸载 3.1&#xff0c;停止Gitlab 相关服务 # 停止所有GitLab相关服务&#xff1a; sudo gitlab-ctl stop# 移除GitLab包…

c语言->贪吃蛇实战技巧结合EasyX简单实现页面管理(简单实现)

✅作者简介&#xff1a;大家好&#xff0c;我是橘橙黄又青&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;再无B&#xff5e;U&#xff5e;G-CSDN博客 1. 游戏背景 贪吃蛇是久负盛名的游戏&#xff0c;它也和俄罗斯⽅…

CSS基础(上)(如果想知道CSS的全部基础知识点,那么只看这一篇就足够了!)

前言&#xff1a;在我们学习完了html之后&#xff0c;我们就要开始学习三大件中的第二件—CSS&#xff0c;CSS 可以控制多重网页的样式和布局&#xff0c;也就是将我们写好的html代码加上一层华丽的衣裳&#xff0c;使网页变得更加精美。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨…