import * as Kalidokit from "../dist"; //Import Helper Functions from Kalidokit const remap = Kalidokit.Utils.remap; const clamp = Kalidokit.Utils.clamp; const lerp = Kalidokit.Vector.lerp; /* THREEJS WORLD SETUP */ let currentVrm; // renderer const renderer = new THREE.WebGLRenderer({ alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); document.body.appendChild(renderer.domElement); // camera const orbitCamera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000); orbitCamera.position.set(0.0, 1.4, 0.7); // controls const orbitControls = new THREE.OrbitControls(orbitCamera, renderer.domElement); orbitControls.screenSpacePanning = true; orbitControls.target.set(0.0, 1.4, 0.0); orbitControls.update(); // scene const scene = new THREE.Scene(); // light const light = new THREE.DirectionalLight(0xffffff); light.position.set(1.0, 1.0, 1.0).normalize(); scene.add(light); // Main Render Loop const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); if (currentVrm) { // Update model to render physics currentVrm.update(clock.getDelta()); } renderer.render(scene, orbitCamera); } animate(); /* VRM CHARACTER SETUP */ // Import Character VRM const loader = new THREE.GLTFLoader(); loader.crossOrigin = "anonymous"; // Import model from URL, add your own model here loader.load( "https://cdn.glitch.com/29e07830-2317-4b15-a044-135e73c7f840%2FAshtra.vrm?v=1630342336981", (gltf) => { THREE.VRMUtils.removeUnnecessaryJoints(gltf.scene); THREE.VRM.from(gltf).then((vrm) => { scene.add(vrm.scene); currentVrm = vrm; currentVrm.scene.rotation.y = Math.PI; // Rotate model 180deg to face camera }); }, (progress) => console.log("Loading model...", 100.0 * (progress.loaded / progress.total), "%"), (error) => console.error(error) ); // Animate Rotation Helper function const rigRotation = (name, rotation = { x: 0, y: 0, z: 0 }, dampener = 1, lerpAmount = 0.3) => { if (!currentVrm) { return; } const Part = currentVrm.humanoid.getBoneNode(THREE.VRMSchema.HumanoidBoneName[name]); if (!Part) { return; } let euler = new THREE.Euler( rotation.x * dampener, rotation.y * dampener, rotation.z * dampener, rotation.rotationOrder || "XYZ" ); let quaternion = new THREE.Quaternion().setFromEuler(euler); Part.quaternion.slerp(quaternion, lerpAmount); // interpolate }; // Animate Position Helper Function const rigPosition = (name, position = { x: 0, y: 0, z: 0 }, dampener = 1, lerpAmount = 0.3) => { if (!currentVrm) { return; } const Part = currentVrm.humanoid.getBoneNode(THREE.VRMSchema.HumanoidBoneName[name]); if (!Part) { return; } let vector = new THREE.Vector3(position.x * dampener, position.y * dampener, position.z * dampener); Part.position.lerp(vector, lerpAmount); // interpolate }; let oldLookTarget = new THREE.Euler(); const rigFace = (riggedFace) => { if (!currentVrm) { return; } rigRotation("Neck", riggedFace.head, 0.7); // Blendshapes and Preset Name Schema const Blendshape = currentVrm.blendShapeProxy; const PresetName = THREE.VRMSchema.BlendShapePresetName; // Simple example without winking. Interpolate based on old blendshape, then stabilize blink with `Kalidokit` helper function. // for VRM, 1 is closed, 0 is open. riggedFace.eye.l = lerp(clamp(1 - riggedFace.eye.l, 0, 1), Blendshape.getValue(PresetName.Blink), 0.5); riggedFace.eye.r = lerp(clamp(1 - riggedFace.eye.r, 0, 1), Blendshape.getValue(PresetName.Blink), 0.5); riggedFace.eye = Kalidokit.Face.stabilizeBlink(riggedFace.eye, riggedFace.head.y); Blendshape.setValue(PresetName.Blink, riggedFace.eye.l); // Interpolate and set mouth blendshapes Blendshape.setValue(PresetName.I, lerp(riggedFace.mouth.shape.I, Blendshape.getValue(PresetName.I), 0.5)); Blendshape.setValue(PresetName.A, lerp(riggedFace.mouth.shape.A, Blendshape.getValue(PresetName.A), 0.5)); Blendshape.setValue(PresetName.E, lerp(riggedFace.mouth.shape.E, Blendshape.getValue(PresetName.E), 0.5)); Blendshape.setValue(PresetName.O, lerp(riggedFace.mouth.shape.O, Blendshape.getValue(PresetName.O), 0.5)); Blendshape.setValue(PresetName.U, lerp(riggedFace.mouth.shape.U, Blendshape.getValue(PresetName.U), 0.5)); //PUPILS //interpolate pupil and keep a copy of the value let lookTarget = new THREE.Euler( lerp(oldLookTarget.x, riggedFace.pupil.y, 0.4), lerp(oldLookTarget.y, riggedFace.pupil.x, 0.4), 0, "XYZ" ); oldLookTarget.copy(lookTarget); currentVrm.lookAt.applyer.lookAt(lookTarget); }; /* VRM Character Animator */ const animateVRM = (vrm, results) => { if (!vrm) { return; } // Take the results from `Holistic` and animate character based on its Face, Pose, and Hand Keypoints. let riggedPose, riggedLeftHand, riggedRightHand, riggedFace; const faceLandmarks = results.faceLandmarks; // Pose 3D Landmarks are with respect to Hip distance in meters const pose3DLandmarks = results.ea; // Pose 2D landmarks are with respect to videoWidth and videoHeight const pose2DLandmarks = results.poseLandmarks; // Be careful, hand landmarks may be reversed const leftHandLandmarks = results.rightHandLandmarks; const rightHandLandmarks = results.leftHandLandmarks; // Animate Face if (faceLandmarks) { riggedFace = Kalidokit.Face.solve(faceLandmarks, { runtime: "mediapipe", video: videoElement, }); rigFace(riggedFace); } // Animate Pose if (pose2DLandmarks && pose3DLandmarks) { riggedPose = Kalidokit.Pose.solve(pose3DLandmarks, pose2DLandmarks, { runtime: "mediapipe", video: videoElement, }); rigRotation("Hips", riggedPose.Hips.rotation, 0.7); rigPosition( "Hips", { x: riggedPose.Hips.position.x, // Reverse direction y: riggedPose.Hips.position.y + 1, // Add a bit of height z: -riggedPose.Hips.position.z, // Reverse direction }, 1, 0.07 ); rigRotation("Chest", riggedPose.Spine, 0.25, 0.3); rigRotation("Spine", riggedPose.Spine, 0.45, 0.3); rigRotation("RightUpperArm", riggedPose.RightUpperArm, 1, 0.3); rigRotation("RightLowerArm", riggedPose.RightLowerArm, 1, 0.3); rigRotation("LeftUpperArm", riggedPose.LeftUpperArm, 1, 0.3); rigRotation("LeftLowerArm", riggedPose.LeftLowerArm, 1, 0.3); rigRotation("LeftUpperLeg", riggedPose.LeftUpperLeg, 1, 0.3); rigRotation("LeftLowerLeg", riggedPose.LeftLowerLeg, 1, 0.3); rigRotation("RightUpperLeg", riggedPose.RightUpperLeg, 1, 0.3); rigRotation("RightLowerLeg", riggedPose.RightLowerLeg, 1, 0.3); } // Animate Hands if (leftHandLandmarks) { riggedLeftHand = Kalidokit.Hand.solve(leftHandLandmarks, "Left"); rigRotation("LeftHand", { // Combine pose rotation Z and hand rotation X Y z: riggedPose.LeftHand.z, y: riggedLeftHand.LeftWrist.y, x: riggedLeftHand.LeftWrist.x, }); rigRotation("LeftRingProximal", riggedLeftHand.LeftRingProximal); rigRotation("LeftRingIntermediate", riggedLeftHand.LeftRingIntermediate); rigRotation("LeftRingDistal", riggedLeftHand.LeftRingDistal); rigRotation("LeftIndexProximal", riggedLeftHand.LeftIndexProximal); rigRotation("LeftIndexIntermediate", riggedLeftHand.LeftIndexIntermediate); rigRotation("LeftIndexDistal", riggedLeftHand.LeftIndexDistal); rigRotation("LeftMiddleProximal", riggedLeftHand.LeftMiddleProximal); rigRotation("LeftMiddleIntermediate", riggedLeftHand.LeftMiddleIntermediate); rigRotation("LeftMiddleDistal", riggedLeftHand.LeftMiddleDistal); rigRotation("LeftThumbProximal", riggedLeftHand.LeftThumbProximal); rigRotation("LeftThumbIntermediate", riggedLeftHand.LeftThumbIntermediate); rigRotation("LeftThumbDistal", riggedLeftHand.LeftThumbDistal); rigRotation("LeftLittleProximal", riggedLeftHand.LeftLittleProximal); rigRotation("LeftLittleIntermediate", riggedLeftHand.LeftLittleIntermediate); rigRotation("LeftLittleDistal", riggedLeftHand.LeftLittleDistal); } if (rightHandLandmarks) { riggedRightHand = Kalidokit.Hand.solve(rightHandLandmarks, "Right"); rigRotation("RightHand", { // Combine Z axis from pose hand and X/Y axis from hand wrist rotation z: riggedPose.RightHand.z, y: riggedRightHand.RightWrist.y, x: riggedRightHand.RightWrist.x, }); rigRotation("RightRingProximal", riggedRightHand.RightRingProximal); rigRotation("RightRingIntermediate", riggedRightHand.RightRingIntermediate); rigRotation("RightRingDistal", riggedRightHand.RightRingDistal); rigRotation("RightIndexProximal", riggedRightHand.RightIndexProximal); rigRotation("RightIndexIntermediate", riggedRightHand.RightIndexIntermediate); rigRotation("RightIndexDistal", riggedRightHand.RightIndexDistal); rigRotation("RightMiddleProximal", riggedRightHand.RightMiddleProximal); rigRotation("RightMiddleIntermediate", riggedRightHand.RightMiddleIntermediate); rigRotation("RightMiddleDistal", riggedRightHand.RightMiddleDistal); rigRotation("RightThumbProximal", riggedRightHand.RightThumbProximal); rigRotation("RightThumbIntermediate", riggedRightHand.RightThumbIntermediate); rigRotation("RightThumbDistal", riggedRightHand.RightThumbDistal); rigRotation("RightLittleProximal", riggedRightHand.RightLittleProximal); rigRotation("RightLittleIntermediate", riggedRightHand.RightLittleIntermediate); rigRotation("RightLittleDistal", riggedRightHand.RightLittleDistal); } }; /* SETUP MEDIAPIPE HOLISTIC INSTANCE */ let videoElement = document.querySelector(".input_video"), guideCanvas = document.querySelector("canvas.guides"); const onResults = (results) => { // Draw landmark guides drawResults(results); // Animate model animateVRM(currentVrm, results); }; const holistic = new Holistic({ locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/holistic@0.5.1635989137/${file}`; }, }); holistic.setOptions({ modelComplexity: 1, smoothLandmarks: true, minDetectionConfidence: 0.7, minTrackingConfidence: 0.7, refineFaceLandmarks: true, }); // Pass holistic a callback function holistic.onResults(onResults); const drawResults = (results) => { guideCanvas.width = videoElement.videoWidth; guideCanvas.height = videoElement.videoHeight; let canvasCtx = guideCanvas.getContext("2d"); canvasCtx.save(); canvasCtx.clearRect(0, 0, guideCanvas.width, guideCanvas.height); // Use `Mediapipe` drawing functions drawConnectors(canvasCtx, results.poseLandmarks, POSE_CONNECTIONS, { color: "#00cff7", lineWidth: 4, }); drawLandmarks(canvasCtx, results.poseLandmarks, { color: "#ff0364", lineWidth: 2, }); drawConnectors(canvasCtx, results.faceLandmarks, FACEMESH_TESSELATION, { color: "#C0C0C070", lineWidth: 1, }); if (results.faceLandmarks && results.faceLandmarks.length === 478) { //draw pupils drawLandmarks(canvasCtx, [results.faceLandmarks[468], results.faceLandmarks[468 + 5]], { color: "#ffe603", lineWidth: 2, }); } drawConnectors(canvasCtx, results.leftHandLandmarks, HAND_CONNECTIONS, { color: "#eb1064", lineWidth: 5, }); drawLandmarks(canvasCtx, results.leftHandLandmarks, { color: "#00cff7", lineWidth: 2, }); drawConnectors(canvasCtx, results.rightHandLandmarks, HAND_CONNECTIONS, { color: "#22c3e3", lineWidth: 5, }); drawLandmarks(canvasCtx, results.rightHandLandmarks, { color: "#ff0364", lineWidth: 2, }); }; // Use `Mediapipe` utils to get camera - lower resolution = higher fps const camera = new Camera(videoElement, { onFrame: async () => { await holistic.send({ image: videoElement }); }, width: 640, height: 480, }); camera.start();