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";// Mediapipeimport { 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);// Facelet 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");// GUIconst 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 Textureconst 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);// MediaPipeconst 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 valuesconst 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 rangeswitch (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.mzph.cn/news/811871.shtml

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

相关文章

BI数据分析软件:行业趋势与功能特点剖析

随着数据量的爆炸性增长&#xff0c;企业对于数据的需求也日益迫切。BI数据分析软件作为帮助企业实现数据驱动决策的关键工具&#xff0c;在当前的商业环境中扮演着不可或缺的角色。本文将从行业趋势、功能特点以及适用场景等方面&#xff0c;深入剖析BI数据分析软件&#xff0…

IP证书申请流程

目录 域名与IP的关系 SSL证书绑定域名还是绑定IP&#xff1f; IP证书支持免费申请吗&#xff1f; 如何申请IP地址证书 IP类型的SSL证书&#xff0c;又称之为IP SSL&#xff0c;这种SSL证书是专门用于公网IP地址验证的一种数字证书。 主要功能就是解决IP地址明文传输的安全…

毅四捕Go设计模式笔记——代理模式

代理模式&#xff08;Proxy Pattern&#xff09; 为了解决什么问题&#xff1f; 代理模式用于在不直接访问实际对象的情况下&#xff0c;通过引入一个代理对象来控制和管理对该对象的访问。主要解决的问题是对原始对象的访问控制&#xff0c;以及在不改变原始对象接口的情况下…

bilibili PC客户端架构设计——基于Electron

众所周知&#xff0c;bilibili是个学习的网站&#xff0c;网页端和粉版移动端都非常的好用&#xff0c;不过&#xff0c;相对其它平台来说bilibili的PC客户端也算是大器晚成了。在有些场景PC客户端的优势也是显而易见的&#xff0c;比如&#xff0c;跓留电脑桌面的快捷、独立的…

C#在后台自动化截图指定网站并保存图片

先安装PuppeteerSharp的库 然后调用如下方法 private async Task ScreenShotAsync(string url){//using var browserFetcher new BrowserFetcher();//await browserFetcher.DownloadAsync();await using var browser await Puppeteer.LaunchAsync(new LaunchOptions { Headle…

gitlab 搭建

cat etc/initial_root_password Password: ipGg5y7GJPp/YmVHf3c3ViMKzCWYJSjU4JzUktrw8cY ###### 可修改&#xff0c;可不修改&#xff0c;并###并#并 #初始密码 #本次未修改 vim /data/gitlab/etc/gitlab.rb external_url http://ip/gitlab #访问网址及端口 #ssh远程地址 gi…

时钟周期检测标志信号

在某些情况下需要对系统时钟分频后的时钟进行周期检测&#xff0c;引出周期标志信号以便在后续其他情况的使用。虽然在大多数情况下我们能够知道分频后的时钟是系统时钟的几倍分频&#xff0c;但为增强在分频时钟改变情况下周期标志信号的复用性或对未知时钟的周期检测&#xf…

FFmpeg: 简易ijkplayer播放器实现--06封装打开和关闭stream

文章目录 流程图stream openstream close 流程图 stream open 初始化SDL以允许⾳频输出&#xff1b;初始化帧Frame队列初始化包Packet队列初始化时钟Clock初始化音量创建解复用读取线程read_thread创建视频刷新线程video_refresh_thread int FFPlayer::stream_open(const cha…

如何解决Uniapp更新数据不重新渲染组件

办法就是在修改数据的函数里面&#xff0c;用let thatthis&#xff0c;再给that用赋值。 原因是给数据赋值的函数没用箭头函数&#xff0c;this是函数自己的this。比如success&#xff08;res&#xff09;{} 或者用箭头函数&#xff0c;比如success&#xff08;res&#xff0…

在Ubuntu服务器上快速安装一个redis并提供远程服务

一、快速安装一个Redis 第一步&#xff1a;更新apt源 sudo apt update第二步&#xff1a;下载Redis sudo apt install redis第三步&#xff1a;查看Redis是否已自启动 systemctl status redis二、配置Redis提供远程服务 第一步&#xff1a;先确保6379端口正常开放 如果是…

STM32F427+RTthread——USB虚拟串口

书接上回说到&#xff0c;RT-Thread完整版移植完毕&#xff0c;接下来做USB虚拟串口的功能 打开MX工程文件&#xff0c;配置USB CDC 先在ENV上选好USB CDC选项 在CubeMX_Config文件夹下就有生成的usb相关文件&#xff0c;添加到Project工程里 然后引入驱动drv_usbd.c&#xff…

【Android Surface】从Activity的创建到Surface的创建,源码分析1

文章目录 activity的创建performLaunchActivityhandleResumeActivitysetContentViewmInstrumentation.newActivitynew出phonewindowWindowManager的创建 回到setContextViewfindViewById addViewViewRootAndroid在哪里“画画” 我们知道Android绘制图形依靠的是surface和surfac…

PostgreSQL数据库基础--简易版

数据库 其中runoobdb为数据库名 查看已经存在的数据库 \l进入数据库 \c runoobdb创建数据库 CREATE DATABASE runoobdb;删除数据库 DROP DATABASE runoobdb;表 其中COMPANY为表名 创建表格 CREATE TABLE COMPANY(ID INT PRIMARY KEY NOT NULL,NAME TEXT…

Harmony鸿蒙南向驱动开发-UART接口使用

功能简介 UART指异步收发传输器&#xff08;Universal Asynchronous Receiver/Transmitter&#xff09;&#xff0c;是通用串行数据总线&#xff0c;用于异步通信。该总线双向通信&#xff0c;可以实现全双工传输。 两个UART设备的连接示意图如下&#xff0c;UART与其他模块一…

外观模式:简化复杂系统的统一接口

在面向对象的软件开发中&#xff0c;外观模式是一种常用的结构型设计模式&#xff0c;旨在为复杂的系统提供一个简化的接口。通过创建一个统一的高级接口&#xff0c;这个模式帮助客户端通过一个简单的方式与复杂的子系统交互。本文将详细介绍外观模式的定义、实现、应用场景以…

【Hadoop大数据技术】——Flume日志采集系统(学习笔记)

&#x1f4d6; 前言&#xff1a;在大数据系统的开发中&#xff0c;数据收集工作无疑是开发者首要解决的一个难题&#xff0c;但由于生产数据的源头丰富多样&#xff0c;其中包含网站日志数据、后台监控数据、用户浏览网页数据等&#xff0c;数据工程师要想将它们分门别类的采集…

什么是RMVB视频?如何把视频转成RMVB格式?视频格式转换的方法

一&#xff0c;什么是RMVB视频格式 RMVB是一种视频文件格式&#xff0c;它基于RealNetworks公司开发的RealMedia编解码器&#xff0c;被广泛应用于互联网上的视频流媒体传输和下载。RMVB文件通常具有较小的文件大小&#xff0c;同时保持较高的视频质量&#xff0c;因此在网络传…

python之堆的实现

堆本质是一个完全二叉树&#xff0c;分为大根堆和小根堆&#xff0c;大根堆每个结点的值都大于它的孩子的值&#xff0c;小根堆相反&#xff0c;每个结点的值都小于它的孩子的值 heapq是python的标准库&#xff0c;用于维护堆&#xff0c;非常方便 heapq库常用的几个函数 he…

React添加到现有项目

1.检查现有项目的根目录下是否有package.json文件 如果没有&#xff0c;则在项目的根目录下初始化一个package.json配置文件 2.在根目录下安装react和react-dom依赖 npm install --save react react-dom react-scripts安装成功后&#xff0c;react、react-dom以及react-scr…

上位机图像处理和嵌入式模块部署(qmacvisual缺失的光源控制)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 有些场景下面&#xff0c;是不需要光源和光源控制的&#xff0c;比如说利用摄像头识别对应区域的库位&#xff0c;这部分直接利用红外光采集对应的…