+
95
-

回答

可以通过face-api通过摄像头捕获人脸 表情数据控制vrm三维人物表情,效果如下:

800_auto

完整代码如下:

点击查看全文

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <style>
        body { margin: 0; }
        canvas { display: block; }
        #video { position: absolute; top: 10px; right: 10px; width: 320px; height: 240px; }
    </style>
</head>
<body>
    <video id="video" width="320" height="240" autoplay muted></video>

<script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/face-api.1.7.13.js"></script>
    <script type="importmap">
    {
        "imports": {
            "three": "//repo.bfw.wiki/bfwrepo/js/module/three/build/164/three.module.js",
            "three/addons/": "//repo.bfw.wiki/bfwrepo/js/module/three/examples/164/jsm/",
            "@pixiv/three-vrm": "//repo.bfw.wiki/bfwrepo/js/three-vrm.module.3.0.0.js"
        }
    }
    </script>

    <script type="module">
        import * as THREE from 'three';
        import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';

        let currentVrm, faceDetection;
const modelPath = '../models/'; // path to model folder that will be loaded using http

const minScore = 0.2; // minimum score
const maxResults = 5; // maximum number of results to return
let optionsSSDMobileNet;
  // 尝试获取第三个摄像头(如果存在的话)
            const devices = await navigator.mediaDevices.enumerateDevices();
            const videoDevices = devices.filter(device => device.kind === 'videoinput');
            const deviceId = videoDevices.length >= 1 ? videoDevices[0].deviceId : undefined;

            const stream = await navigator.mediaDevices.getUserMedia({
                video: deviceId ? { deviceId: { exact: deviceId } } : true
            });
            video.srcObject = stream;
async function detectVideo() {
 
  const t0 = performance.now();
  faceapi
    .detectAllFaces(video, optionsSSDMobileNet)
    .withFaceLandmarks()
    .withFaceExpressions()
    // .withFaceDescriptors()
    .withAgeAndGender()
    .then((result) => {
      const fps = 1000 / (performance.now() - t0);
     console.log(result[0]);
      
     
    updateVRMExpressions(result[0]);
     requestAnimationFrame(() => detectVideo());
      return true;
    })
    .catch((err) => {
      console.log(err);
       requestAnimationFrame(() => detectVideo());
      return true;
    });
  return false;
}

async function setupFaceAPI() {
  // load face-api models
  // log('Models loading');
  // await faceapi.nets.tinyFaceDetector.load(modelPath); // using ssdMobilenetv1
  await faceapi.nets.ssdMobilenetv1.load(modelPath);
  await faceapi.nets.ageGenderNet.load(modelPath);
  await faceapi.nets.faceLandmark68Net.load(modelPath);
  await faceapi.nets.faceRecognitionNet.load(modelPath);
  await faceapi.nets.faceExpressionNet.load(modelPath);
  optionsSSDMobileNet = new faceapi.SsdMobilenetv1Options({ minConfidence: minScore, maxResults });
  // check tf engine state

}

        // 初始化face-api
        async function initVrm() {
    	// renderer
			const renderer = new THREE.WebGLRenderer();
			renderer.setSize( window.innerWidth, window.innerHeight );
			renderer.setPixelRatio( window.devicePixelRatio );
			document.body.appendChild( renderer.domElement );

			// camera
			const camera = new THREE.PerspectiveCamera( 30.0, window.innerWidth / window.innerHeight, 0.1, 20.0 );
			camera.position.set( 0.0, 1.0, 5.0 );

			// camera controls
			const controls = new OrbitControls( camera, renderer.domElement );
			controls.screenSpacePanning = true;
			controls.target.set( 0.0, 1.0, 0.0 );
			controls.update();

			// scene
			const scene = new THREE.Scene();

			// light
			const light = new THREE.DirectionalLight( 0xffffff, Math.PI );
			light.position.set( 1.0, 1.0, 1.0 ).normalize();
			scene.add( light );

			// gltf and vrm
			const loader = new GLTFLoader();
			loader.crossOrigin = 'anonymous';
			loader.register( ( parser ) => new VRMLoaderPlugin( parser ) );

			loader.load(
				'//repo.bfw.wiki/bfwrepo/threemodel/girl.vrm',
				( gltf ) => {
					const vrm = gltf.userData.vrm;
					VRMUtils.removeUnnecessaryVertices( gltf.scene );
					VRMUtils.removeUnnecessaryJoints( gltf.scene );
					vrm.scene.traverse( ( obj ) => {
						obj.frustumCulled = false;
					} );
					scene.add( vrm.scene );
					currentVrm = vrm;
					console.log( vrm );
				},
				( progress ) => console.log( 'Loading model...', 100.0 * ( progress.loaded / progress.total ), '%' ),
				( error ) => console.error( error )
			);

			// helpers
			const gridHelper = new THREE.GridHelper( 10, 10 );
			scene.add( gridHelper );

			const axesHelper = new THREE.AxesHelper( 5 );
			scene.add( axesHelper );

			// animate
			const clock = new THREE.Clock();

			function animate() {
				requestAnimationFrame( animate );
				const deltaTime = clock.getDelta();
				if ( currentVrm ) {
					currentVrm.update( deltaTime );
				}
				renderer.render( scene, camera );
			}

			animate();

			// Expression control functions
			window.setExpression = function(expression) {
				if (currentVrm) {
resetAllExpressions();


					switch(expression) {
						case 'angry':
							currentVrm.expressionManager.setValue('angry', 1.0);
							break;
						case 'happy':
							currentVrm.expressionManager.setValue('happy', 1.0);
							break;
						case 'sad':
							currentVrm.expressionManager.setValue('sad', 1.0);
							break;
						case 'neutral':
							currentVrm.expressionManager.setValue('relaxed', 1.0);
							break;
								case 'fearful':
							currentVrm.expressionManager.setValue('fearful', 1.0);
							break;
						case 'crying':
							currentVrm.expressionManager.setValue('sad', 1.0);
							currentVrm.expressionManager.setValue('aa', 0.8); // Open mouth
							break;
							case 'surprised':
							currentVrm.expressionManager.setValue('surprised', 1.0);
							currentVrm.expressionManager.setValue('aa', 0.8); // Open mouth
							break;
					}
				}
        }
        }

        	function resetAllExpressions() {
				const expressions = ['angry', 'happy', 'sad', 'disgusted', 'relaxed',"surprised","fearful"];
				expressions.forEach(exp => {
					currentVrm.expressionManager.setValue(exp, 0);
				});
			}
		
         // 更新VRM表情
        function updateVRMExpressions(exp) {
                   
            let expressions=exp.expressions;
            console.log(expressions)
     

        if (!currentVrm) return;
 
        setExpression(findMaxProperty(expressions))
       
 

           
        }
function findMaxProperty(obj) {
  let maxProp = null;
  let maxValue = -Infinity;

  for (const prop in obj) {
    if (obj[prop] > maxValue) {
      maxValue = obj[prop];
      maxProp = prop;
    }
  }

  return maxProp;
}

        // 初始化并开始动画
        async function init() {
            
             
              await faceapi.tf.setBackend('webgl');
              await faceapi.tf.ready();
            
              // tfjs optimizations
              if (faceapi.tf?.env().flagRegistry.CANVAS2D_WILL_READ_FREQUENTLY) faceapi.tf.env().set('CANVAS2D_WILL_READ_FREQUENTLY', true);
              if (faceapi.tf?.env().flagRegistry.WEBGL_EXP_CONV) faceapi.tf.env().set('WEBGL_EXP_CONV', true);
              if (faceapi.tf?.env().flagRegistry.WEBGL_EXP_CONV) faceapi.tf.env().set('WEBGL_EXP_CONV', true);
       
            
              await setupFaceAPI();
    

           await detectVideo();
           await initVrm();
            //animate();
        }

        init();
    </script>
</body>
</html>

VRM模型中的表情通常使用blendshapes(也称为morph targets)来实现。VRM 规范定义了一套标准的表情blendshapes,但并不是所有的VRM模型都会包含所有这些表情。以下是VRM标准中定义的表情blendshapes及其对应的face-api.js表情:

neutral - 对应 face-api.js 的 "neutral"a - 张嘴(可用于 "surprised" 或 "happy")e - 微笑(可用于 "happy")i - 皱眉(可用于 "angry" 或 "disgusted")o - 圆嘴(可用于 "surprised")u - 撅嘴(可用于 "sad" 或 "disgusted")blink - 眨眼blink_l - 左眼眨眼blink_r - 右眼眨眼joy - 开心(对应 "happy")angry - 生气(对应 "angry")sorrow - 悲伤(对应 "sad")fun - 有趣(可用于 "happy" 或 "surprised")lookUp - 向上看lookDown - 向下看lookLeft - 向左看lookRight - 向右看

网友回复

我知道答案,我要回答