🐭

Babylon.jsでちいかわの3Dモデルを作る

に公開

Babylon.jsとは?

Webで3Dグラフィックスを扱うことができるJavaScriptフレームワークです
類似のものだとThree.jsのほうが有名でしょうか
https://www.babylonjs.com/

知らなかったのですがnativeもいけるようになってるみたいですね・・・!

公式サイトのほうでデモやプレイグラウンド等ありますので気になる方は是非触ってみてください!

ちいかわとは?

イラストレーター ナガノさんによる作品「ちいかわ なんか小さくてかわいいやつ」に登場するキャラクターです
https://www.anime-chiikawa.jp/

漫画、アニメのどちらもとても面白い作品なので、まだ見ていない方は是非・・・

自分が好きなので、今回Babylon.jsで作ってみようと思いました

3Dモデルの作成

今回は3Dモデルを作成するだけなので、プレイグラウンドで書いていきます
まずは最低限必要なシーン、カメラを用意します

var createScene = function () {
    const scene = new BABYLON.Scene(engine);

    const camera = new BABYLON.ArcRotateCamera("camera", Math.PI / 2, Math.PI / 2, 8, BABYLON.Vector3.Zero());
    camera.attachControl(canvas, true);
    camera.wheelDeltaPercentage = 0.1;

    const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0));
    
    return scene;
};

まだ空間にはなにもないので何も表示されません

早速モデルをつくっていきたいのですが、モデルの質感を表現するMaterialを用意しておきます

const bodyMaterial = new BABYLON.StandardMaterial('bodyMaterial', scene);
bodyMaterial.diffuseColor = new BABYLON.Color3(1, 1, 1);
bodyMaterial.specularColor = new BABYLON.Color3(0, 0, 0);

まずは頭を作成します

const head = BABYLON.MeshBuilder.CreateSphere("head", { diameterX: 0.6, diameterY: 0.5, diameterZ: 0.4 }, scene);

何もなかった空間に丸い物体が現れました
Babylon.jsではMeshBuilderを使用して様々な形状を表示することができます
https://doc.babylonjs.com/features/featuresDeepDive/mesh/
今回はSphereという球体の形状を用意し、直径を設定して頭の形にしています

これに先ほどのMaterialを適用してみます

head.material = bodyMaterial;

反射がなくなりマットな質感になりました

次は胴体を作ります

const torso = BABYLON.MeshBuilder.CreateSphere("torso", { diameterX: 0.5, diameterY: 0.6, diameterZ: 0.3, slice: 0.5 }, scene);
torso.material = bodyMaterial;
torso.rotation.x = Math.PI;
torso.position.y = -0.13;

今回はspliceという値を追加し、半分だけの形状にしています
また、rotationで向きを変え、positionで少し下にずらしています

この調子で他の部位も位置や向きを調整しつつ追加していきます

const rightEar = BABYLON.MeshBuilder.CreateSphere("rightEar", { diameterX: 0.12, diameterY: 0.12, diameterZ: 0.06, slice: 0.5 }, scene);
rightEar.material = bodyMaterial;
rightEar.rotation.z = -Math.PI / 6;
rightEar.position = new BABYLON.Vector3(0.18, 0.18, 0);

const leftEar = BABYLON.MeshBuilder.CreateSphere("leftEar", { diameterX: 0.12, diameterY: 0.12, diameterZ: 0.06, slice: 0.5 }, scene);
leftEar.material = bodyMaterial;
leftEar.rotation.z = Math.PI / 6;
leftEar.position = new BABYLON.Vector3(-0.18, 0.18, 0);

const rightArm = BABYLON.MeshBuilder.CreateCapsule("rightArm", { height: 0.2, radius: 0.03 }, scene);
rightArm.material = bodyMaterial;
rightArm.position = new BABYLON.Vector3(0.25, -0.2, 0);
rightArm.rotation.z = Math.PI / 5;

const leftArm = BABYLON.MeshBuilder.CreateCapsule("leftArm", { height: 0.2, radius: 0.03 }, scene);
leftArm.material = bodyMaterial;
leftArm.position = new BABYLON.Vector3(-0.25, -0.2, 0);
leftArm.rotation.z = -Math.PI / 5;

const rightLeg = BABYLON.MeshBuilder.CreateCapsule("rightLeg", { height: 0.15, radius: 0.03 }, scene);
rightLeg.material = bodyMaterial;
rightLeg.position = new BABYLON.Vector3(0.1, -0.4, 0);

const leftLeg = BABYLON.MeshBuilder.CreateCapsule("leftLeg", { height: 0.15, radius: 0.03 }, scene);
leftLeg.material = bodyMaterial;
leftLeg.position = new BABYLON.Vector3(-0.1, -0.4, 0);

const tail = BABYLON.MeshBuilder.CreateSphere("tail", { diameterX: 0.08, diameterY: 0.08, diameterZ: 0.08 }, scene);
tail.material = bodyMaterial;
tail.position = new BABYLON.Vector3(0, -0.35, -0.13);
tail.rotation.x = -2 * Math.PI / 3;

あっという間にそれらしい体のモデルができあがりました!

顔を描く

ボディはできたので、次は顔を描いていきます
顔を描くのにはDecalを使用しました

const eyeTexture = new BABYLON.DynamicTexture("eyeTexture", { width: 100, height: 100 }, scene);
eyeTexture.hasAlpha = true;

const eyeCtx = eyeTexture.getContext();
// 目
eyeCtx.fillStyle = '#000000';
eyeCtx.beginPath();
eyeCtx.arc(100 / 2, 100 / 2, 100 / 3, 0, Math.PI * 2);
eyeCtx.fill();

// ハイライト
eyeCtx.fillStyle = '#ffffff';
eyeCtx.beginPath();
eyeCtx.arc(100 / 2, 100 / 2 - 100 / 8, 100 / 6, 0, Math.PI * 2);
eyeCtx.fill();

// ハイライト
eyeCtx.strokeStyle = '#ffffff';
eyeCtx.lineWidth = 8;
eyeCtx.beginPath();
eyeCtx.arc(100 / 2, 100 / 2, 100 / 4, Math.PI / 4, 3 * Math.PI / 4);
eyeCtx.stroke();

eyeTexture.update();

const eyeMaterial = new BABYLON.StandardMaterial("eyeMat", scene);
eyeMaterial.diffuseTexture = eyeTexture;
eyeMaterial.zOffset = -1;

const rightEye = BABYLON.DecalBuilder.CreateDecal("rightEye", head, {
    position: new BABYLON.Vector3(0.1, 0.05, 0.1),
    normal: new BABYLON.Vector3(0, 0, 1),
    size: new BABYLON.Vector3(0.1, 0.1, 0.5)
});
rightEye.material = eyeMaterial;

const leftEye = BABYLON.DecalBuilder.CreateDecal("leftEye", head, {
    position: new BABYLON.Vector3(-0.1, 0.05, 0.1),
    normal: new BABYLON.Vector3(0, 0, 1),
    size: new BABYLON.Vector3(0.1, 0.1, 0.5)
});
leftEye.material = eyeMaterial;

htmlのcanvasで図形を描き、それをモデルに貼り付ける感じですね
canvasのプレイグラウンドで図形を作成してから貼り付けるのがやりやすいかなと思いました
画像なども貼り付けることができます

デカールを貼り付ける際は.showBoundingBoxをtrueにすると位置が分かりやすくなります

rightEye.showBoundingBox = true;


あとは.zOffsetを設定しないとチラつくので、設定したほうがよさげです

この調子で他の部位も書いていきます

const rightEyebrowTexture = new BABYLON.DynamicTexture("rightEyebrowTexture", { width: 100, height: 100 }, scene);
rightEyebrowTexture.hasAlpha = true;

const rightEyebrowCtx = rightEyebrowTexture.getContext();
rightEyebrowCtx.strokeStyle = '#000000';
rightEyebrowCtx.lineWidth = 8;
rightEyebrowCtx.beginPath();
rightEyebrowCtx.moveTo(0, 50);
rightEyebrowCtx.quadraticCurveTo(10, 40, 50, 40);
rightEyebrowCtx.stroke();

rightEyebrowTexture.update();

const rightEyebrowMaterial = new BABYLON.StandardMaterial("rightEyebrowMat", scene);
rightEyebrowMaterial.diffuseTexture = rightEyebrowTexture;
rightEyebrowMaterial.zOffset = -1;

const rightEyebrow = BABYLON.DecalBuilder.CreateDecal("rightEyebrow", head, {
    position: new BABYLON.Vector3(0.08, 0.12, 0.1),
    normal: new BABYLON.Vector3(0, 0, 1),
    size: new BABYLON.Vector3(0.1, 0.1, 0.5)
});
rightEyebrow.material = rightEyebrowMaterial;

const leftEyebrowTexture = new BABYLON.DynamicTexture("leftEyebrowTexture", { width: 100, height: 100 }, scene);
leftEyebrowTexture.hasAlpha = true;

const leftEyebrowCtx = leftEyebrowTexture.getContext();
leftEyebrowCtx.strokeStyle = '#000000';
leftEyebrowCtx.lineWidth = 8;
leftEyebrowCtx.beginPath();
leftEyebrowCtx.moveTo(100, 50);
leftEyebrowCtx.quadraticCurveTo(90, 40, 50, 40);
leftEyebrowCtx.stroke();

leftEyebrowTexture.update();

const leftEyebrowMaterial = new BABYLON.StandardMaterial("leftEyebrowMat", scene);
leftEyebrowMaterial.diffuseTexture = leftEyebrowTexture;
leftEyebrowMaterial.zOffset = -1;

const leftEyebrow = BABYLON.DecalBuilder.CreateDecal("leftEyebrow", head, {
    position: new BABYLON.Vector3(-0.08, 0.12, 0.1),
    normal: new BABYLON.Vector3(0, 0, 1),
    size: new BABYLON.Vector3(0.1, 0.1, 0.5)
});
leftEyebrow.material = leftEyebrowMaterial;

const mouthTexture = new BABYLON.DynamicTexture("mouthTexture", { width: 100, height: 100 }, scene);
mouthTexture.hasAlpha = true;

const mouthCtx = mouthTexture.getContext();
mouthCtx.strokeStyle = '#000000';
mouthCtx.lineWidth = 8;
mouthCtx.beginPath();
mouthCtx.moveTo(0, 75);
mouthCtx.bezierCurveTo(20, 100, 40, 100, 50, 60);
mouthCtx.bezierCurveTo(60, 100, 80, 100, 100, 75);
mouthCtx.stroke();

mouthTexture.update();

const mouthMaterial = new BABYLON.StandardMaterial("mouthMat", scene);
mouthMaterial.diffuseTexture = mouthTexture;
mouthMaterial.zOffset = -1;

const mouth = BABYLON.DecalBuilder.CreateDecal("mouth", head, {
    position: new BABYLON.Vector3(0, 0.0, 0.1),
    normal: new BABYLON.Vector3(0, 0, 1),
    size: new BABYLON.Vector3(0.1, 0.1, 0.5)
});
mouth.material = mouthMaterial;

const jawTexture = new BABYLON.DynamicTexture("jawTexture", { width: 100, height: 100 }, scene);
jawTexture.hasAlpha = true;

const jawCtx = jawTexture.getContext();
jawCtx.strokeStyle = '#000000';
jawCtx.lineWidth = 8;
jawCtx.beginPath();
jawCtx.moveTo(40, 50);
jawCtx.bezierCurveTo(45, 60, 55, 60, 60, 50);
jawCtx.stroke();

jawTexture.update();

const jawMaterial = new BABYLON.StandardMaterial("jawMat", scene);
jawMaterial.diffuseTexture = jawTexture;
jawMaterial.zOffset = -1;

const jaw = BABYLON.DecalBuilder.CreateDecal("jaw", head, {
    position: new BABYLON.Vector3(0, -0.06, 0.1),
    normal: new BABYLON.Vector3(0, 0, 1),
    size: new BABYLON.Vector3(0.1, 0.1, 0.5)
});
jaw.material = jawMaterial;

const cheekTexture = new BABYLON.DynamicTexture("cheekTexture", { width: 100, height: 100 }, scene);
cheekTexture.hasAlpha = true;

const cheekCtx = cheekTexture.getContext();
cheekCtx.fillStyle = '#FFC0CB';
cheekCtx.beginPath();
cheekCtx.ellipse(50, 50, 40, 30, 0, 0, 2 * Math.PI);
cheekCtx.fill();
cheekCtx.beginPath();
cheekCtx.font = "bold 50px Arial";
cheekCtx.fillStyle = '#000000';
cheekCtx.fillText("////", 22, 67);

cheekTexture.update();

const cheekMaterial = new BABYLON.StandardMaterial("cheekMat", scene);
cheekMaterial.diffuseTexture = cheekTexture;
cheekMaterial.zOffset = -1;

const rightCheek = BABYLON.DecalBuilder.CreateDecal("rightCheek", head, {
    position: new BABYLON.Vector3(0.15, -0.03, 0.1),
    normal: new BABYLON.Vector3(0, 0, 1),
    size: new BABYLON.Vector3(0.1, 0.1, 0.5)
});
rightCheek.material = cheekMaterial;

const leftCheek = BABYLON.DecalBuilder.CreateDecal("leftCheek", head, {
    position: new BABYLON.Vector3(-0.15, -0.03, 0.1),
    normal: new BABYLON.Vector3(0, 0, 1),
    size: new BABYLON.Vector3(0.1, 0.1, 0.5)
});
leftCheek.material = cheekMaterial;

ちいかわができました、かわいい!

実際にこのようなフレームワークで3Dモデルを使用する際はBlender等で作ったものを使用するのだとは思いますが
Babylon.jsだけでも作れるのでとても楽しいですね

※ちいかわ作るのまずかったら削除します・・・

Discussion