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

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

程序预览

{正在筹备}

这篇关于React + three.js 实现人脸动捕与3D模型表情同步的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/899275

相关文章

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python脚本实现自动删除C盘临时文件夹

《Python脚本实现自动删除C盘临时文件夹》在日常使用电脑的过程中,临时文件夹往往会积累大量的无用数据,占用宝贵的磁盘空间,下面我们就来看看Python如何通过脚本实现自动删除C盘临时文件夹吧... 目录一、准备工作二、python脚本编写三、脚本解析四、运行脚本五、案例演示六、注意事项七、总结在日常使用

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

Java中Springboot集成Kafka实现消息发送和接收功能

《Java中Springboot集成Kafka实现消息发送和接收功能》Kafka是一个高吞吐量的分布式发布-订阅消息系统,主要用于处理大规模数据流,它由生产者、消费者、主题、分区和代理等组件构成,Ka... 目录一、Kafka 简介二、Kafka 功能三、POM依赖四、配置文件五、生产者六、消费者一、Kaf

使用Python实现在Word中添加或删除超链接

《使用Python实现在Word中添加或删除超链接》在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能,本文将为大家介绍一下Python如何实现在Word中添加或... 在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能。通过添加超

windos server2022里的DFS配置的实现

《windosserver2022里的DFS配置的实现》DFS是WindowsServer操作系统提供的一种功能,用于在多台服务器上集中管理共享文件夹和文件的分布式存储解决方案,本文就来介绍一下wi... 目录什么是DFS?优势:应用场景:DFS配置步骤什么是DFS?DFS指的是分布式文件系统(Distr

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服

Golang的CSP模型简介(最新推荐)

《Golang的CSP模型简介(最新推荐)》Golang采用了CSP(CommunicatingSequentialProcesses,通信顺序进程)并发模型,通过goroutine和channe... 目录前言一、介绍1. 什么是 CSP 模型2. Goroutine3. Channel4. Channe

C#使用yield关键字实现提升迭代性能与效率

《C#使用yield关键字实现提升迭代性能与效率》yield关键字在C#中简化了数据迭代的方式,实现了按需生成数据,自动维护迭代状态,本文主要来聊聊如何使用yield关键字实现提升迭代性能与效率,感兴... 目录前言传统迭代和yield迭代方式对比yield延迟加载按需获取数据yield break显式示迭

Python实现高效地读写大型文件

《Python实现高效地读写大型文件》Python如何读写的是大型文件,有没有什么方法来提高效率呢,这篇文章就来和大家聊聊如何在Python中高效地读写大型文件,需要的可以了解下... 目录一、逐行读取大型文件二、分块读取大型文件三、使用 mmap 模块进行内存映射文件操作(适用于大文件)四、使用 pand