モーションキャプチャを自作せよ
なぜか業務で、モーションキャプチャを自作せよとの指令が。
最終学歴商業高校卒業のわたしに、なんとまぁ無理難題を。
検索力だけは自負がある。早速調査開始である。
先人の知恵
ずぶの素人がやってみよう!
なんて思うことは、たいてい先人がすでに成し遂げていることなのですよね。
まさに、これという代物を発掘した。
KalidoKitである。
使えるモデルはVRMのみ。
だが、モデリング技術なんて無いので、VRoidstudioでサクッと作れるVRMなら問題なし。
早速こちらの記事を参考に試してみた。
自作モデルを配置し、動いた時の感動よ。
あぁでも、このままじゃただ便利ツールを使っただけである。
カスタムしようぜ!
自作モデルを動かすことには成功した。
次はそうだな…
背景を変えよう!
自作モデルは、弊社の製品を着用している。
想定される使用環境に合った背景に変えてみよう。
ちなみに、自作モデルはこんな感じ。
背景を変えよう
Kalidokitの中身は、three.js。
この中に、キューブマップをぶちこむことにした。
ちなみにキューブマップとやらも、今回初めて学んだ。
画像は手持ちがないので、借りてきた。
できた!!!
なんとなくアンマッチ感のある背景だが、とりあえずテストだし。
これで完成、といきたいところだが、創作意欲が高まってきた。
BGMをつけるべし
無音ってなんか寂しいよね?
てことで、BGMをつけることにした。
こちらを参考に別スクリプトで組み込んでみたが、jsがケンカしているのか、うまく鳴らない。
うーん…
困ったときは、これしかない!
#助けて!ChatGPT
正直、存在を忘れていた。笑
なかなかいい仕事してくれるので、早速聞いてみた。
Q.three.jsで、BGMを流したい
A.
Three.jsを使用してBGMを組み込むために、まず音声ファイルを読み込むコードを追加し、次にアニメーションループ内で音声を再生するコードを書きます。以下は、提供されたThree.jsのコードにBGMを追加する方法です。
-
BGM用の音声ファイルを用意します。この例では、"bgm.mp3"というファイルを用意することとします。
-
Three.jsの初期化コードの前に、BGMを再生するためのオーディオ要素を追加します。
const audioElement = new Audio('path/to/your/bgm.mp3');
audioElement.loop = true; // ループ再生を有効にする
audioElement.volume = 0.5; // 音量を設定する(0から1の範囲)
- Three.jsのアニメーションループ内で音声を再生します。
animate
関数内にBGM再生を追加します。
function animate() {
requestAnimationFrame(animate);
if (currentVrm) {
// Update model to render physics
currentVrm.update(clock.getDelta());
}
// BGMを再生する
if (audioElement.paused) {
audioElement.play();
}
renderer.render(scene, orbitCamera);
}
// アニメーションを開始
animate();
これで、提供されたThree.jsプロジェクトにBGMが組み込まれました。BGMはアニメーションループ内で再生され、必要に応じてループ再生されます。音量やファイルパスなどを適切に調整してください。
さすが、ChatGPT。
このコードを組み込んでみたら、流れましたよBGM。
というわけで、今回のミッションはひとまず成功ということにしよう。
ネットの力は偉大である。
おまけ
素人コードなので、改造した部分が整っていないかもなのはご勘弁。
精進します。
KalidoKit、ありがとう。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- HTML Meta Tags -->
<title>test</title>
<meta
name="description"
content="Track face and body rigs just your browser webcam!"
/>
<!-- Basic Three.js -->
<script src="https://unpkg.com/three@0.133.0/build/three.js"></script>
<!-- GLTF Loader for Three.js -->
<script src="https://unpkg.com/three@0.133.0/examples/js/loaders/GLTFLoader.js"></script>
<!-- Orbit Controls for Three.js -->
<script src="https://unpkg.com/three@0.133.0/examples/js/controls/OrbitControls.js"></script>
<!-- VRM Loader for Three.js -->
<script src="https://unpkg.com/@pixiv/three-vrm@0.6.7/lib/three-vrm.js"></script>
<!-- Mediapipe or Tensorflow.js -->
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/holistic@0.5.1635989137/holistic.js"
crossorigin="anonymous"
></script>
<!-- Mediapipe Drawing Tools -->
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"
crossorigin="anonymous"
></script>
<!-- Mediapipe Camera Tools -->
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"
crossorigin="anonymous"
></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="preview">
<video class="input_video" width="1280px" height="720px"></video>
<canvas class="guides"></canvas>
</div>
<script src="./script.js" type="module" defer></script>
</body>
</html>
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;
const audioElement = new Audio('mnk.mp3');
audioElement.loop = true; // ループ再生を有効にする
audioElement.volume = 0.5; // 音量を設定する(0から1の範囲)
/* 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();
const cubeMap = {
path: './Park/',
urls: [ 'posx.jpg', 'negx.jpg','posy.jpg', 'negy.jpg','posz.jpg', 'negz.jpg' ]
}
// scene
const scene = new THREE.Scene();
scene.background = new THREE.CubeTextureLoader().setPath(cubeMap.path).load(cubeMap.urls);
// 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());
}
// BGMを再生する
if (audioElement.paused) {
audioElement.play();
}
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(
"Actiboots.vrm",
(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();