Webブラウザで玉転がしを作成してみる（3）


## はじめに

この記事は　[Babylon.js Advent Calendar 2024](https://qiita.com/advent-calendar/2024/babylonjs)
の、24日目の記事です。

何度かに分けて、スマホのセンサーにアクセスして玉転がしをするページを作成しています。

## 今回の内容
[前回](https://qiita.com/Ykoba791/items/947da14586a46d84fb28)は、ジャイロセンサーにアクセスしてデバイスの傾きを取得、傾きを物理エンジンの重力方向に反映させて画面上の玉を転がすWebページを作成しました。
PlaygourndのURLと、実行した際の画像は以下の通りです。
https://playground.babylonjs.com/#2S9H94#24

![MNUH5839.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/b0353105-ee73-af16-71a8-677aa50d17f4.gif)

前回はフラットな土台しか用意しなかったので、今回は枠などを追加していきます。

### 土台の端に枠を作る
四隅や障害物となる壁を作るロジックを作成します。
作る数が多少あるので、壁の四隅の座標（x1,z1,x2,z2）を指定したらCreateBoxで壁を作成する処理を用意しました。
Boxを作成した後は、PhysicsImpostorで土台と同じように重力の影響を受けるか（mass）、衝突時の跳ね返りの強さ（restitution）、移動の際の摩擦（friction）も指定しています。

``` JavaScript
    //指定座標に四角い障害物を作成する
    const createBox=(x1,z1,x2,z2,mate) => {
        if(x1>x2){[x1, x2] = [x2, x1];}
        if(z1>z2){[z1, z2] = [z2, z1];}
        const wall = BABYLON.MeshBuilder.CreateBox("box", {width: Math.abs(x1-x2), height: 1, depth: Math.abs(z1-z2)})   
        wall.position.x = x1 + Math.abs(x1-x2)/2;
        wall.position.y = 0.5;
        wall.position.z = z1 + Math.abs(z1-z2)/2;
        wall.material=mate;

        wall.physicsImpostor = new BABYLON.PhysicsImpostor(
            wall,
            BABYLON.PhysicsImpostor.BoxImpostor,
            { mass: 0, restitution: 0.5, friction: 0.1 },
            scene
        );
        return wall;
    }
```

上記の処理で、土台の淵に壁を作成します。
土台の大きさが10×10なので、-5～5の範囲で壁を4つ作成しています。
``` JavaScript
    //外壁
    const outLinewalls = [
        createBox(4.8,-5,5,5,wallMaterial),
        createBox(-4.8,-5,-5,5,wallMaterial),
        createBox(-5,-4.8,5,-5,wallMaterial),
        createBox(-5,4.8,5,5,wallMaterial),
    ];
```
実行結果は以下のようになります。
壁に衝突判定が発生して外に落ちないようになっていますね。
![JHYW2784.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/28a2bda5-0b1a-9e05-4ea2-5c41c218fd1d.gif)


### 簡単な迷路を作成
外枠が出来たので、同じように迷路にするための壁のデータを追加します。
``` JavaScript
    //障害物
    const walls = [
        createBox( 0.8,-1.0 , 1.0, 2.0,wallMaterial),
        createBox( 0.8,-1.0 ,-2.0,-0.8,wallMaterial),
        createBox(-2.0, 0.4 ,-1.8,-5.0,wallMaterial),
        createBox( 1.0, 1.8 ,-3.2, 2.0,wallMaterial),
        createBox(-3.4, 2.0 ,-3.2,-3.3,wallMaterial),
        createBox(-3.4, 5.0 ,-3.2, 3.5,wallMaterial),
        createBox(-2.0, 3.5 ,-1.8, 2.0,wallMaterial),
        createBox(-0.6, 5.0 ,-0.4, 3.5,wallMaterial),
        createBox( 1.0, 3.5 , 0.8, 2.0,wallMaterial),
        createBox( 2.1, 2.0 , 3.6, 3.5,wallMaterial),
        createBox( 0.8, 0.8 , 2.3, 1.0,wallMaterial),
        createBox( 3.5, 0.8 , 5.0, 1.0,wallMaterial),
        createBox( 2.1, 0.8 , 2.3, -2.3,wallMaterial),
        createBox( 3.5, 0.8 , 3.7, -2.3,wallMaterial),
        createBox( 0.8,-3.5 , 2.3, -3.7,wallMaterial),
        createBox( 3.5,-3.5 , 5.0, -3.7,wallMaterial),
        createBox( 0.8,-5.0 , 1.0, -2.5,wallMaterial),
        createBox( 0.8,-2.5 ,-0.5, -2.7,wallMaterial),
    ];
```

上記のデータで壁を作成した画面は以下のようになります。
![HRHM4228.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/d301c876-9f2c-e095-a475-a7811bb43d32.gif)

## 最後に

物理演算のおかげで、衝突判定を持つオブジェクトを配置するだけで、玉を転がす玩具の再現が簡単にできました。
センサーへのアクセスも容易なので色々と機能を追加していくと面白そうです。


最期にPlayGroundのURLと、ソースコードを載せておきます。
https://playground.babylonjs.com/#2S9H94#27
<img width="100" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/60f2c2c1-2f63-ad51-3a88-54354c5ba76e.png">


``` JavaScript
const createScene = function () {
    const scene = new BABYLON.Scene(engine);
    const camera = new BABYLON.FreeCamera("camera", new BABYLON.Vector3(0, 30, -1), scene);
    camera.setTarget(BABYLON.Vector3.Zero());
    new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 1), scene);

    // GUI設定
    const advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");
    // 開始ボタンを作成
    const btnStart = BABYLON.GUI.Button.CreateSimpleButton("btnStart", "センサー読み取り開始");
    btnStart.width = "600px"; // ボタンの幅
    btnStart.height = "80px"; // ボタンの高さ
    btnStart.color = "white"; // テキストの色
    btnStart.fontSize = "24px"; // フォントサイズ
    btnStart.background = "green"; // ボタンの背景色
    btnStart.cornerRadius = 10; // 角の丸み
    btnStart.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
    btnStart.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
    btnStart.onPointerClickObservable.add(() => {
        // ボタンクリック センサーイベント登録
        init();
    });
    advancedTexture.addControl(btnStart);   //ボタンを追加
   
    // 表示用パネルの作成
    const pnlInfo = new BABYLON.GUI.StackPanel();
    pnlInfo.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
    pnlInfo.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
    pnlInfo.background = "rgba(0, 0, 0, 1.0)";
    pnlInfo.width = "300px";
    advancedTexture.addControl(pnlInfo);
    const createTextBlock = (name, text) => {
        const textBlock = new BABYLON.GUI.TextBlock();
        textBlock.name = name;
        textBlock.text = text;
        textBlock.color = "white";
        textBlock.fontSize = 24;
        textBlock.height = "32px";
        pnlInfo.addControl(textBlock);
        return textBlock;
    };
    //表示用のText領域を生成
    const alphaText = createTextBlock("alpha", "Alpha: 0°");
    const betaText = createTextBlock("beta", "Beta: 0°");
    const gammaText = createTextBlock("gamma", "Gamma: 0°");

    // Cannon.js を使った物理エンジンを有効化
    const gravityVector = new BABYLON.Vector3(0, -9.81, 0); // 初期重力
    const physicsPlugin = new BABYLON.CannonJSPlugin();
    scene.enablePhysics(gravityVector, physicsPlugin);

    // 地面を作成
    const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 10, height: 10 }, scene);
    const groundMaterial = new BABYLON.StandardMaterial("groundMat", scene);
    groundMaterial.diffuseColor = new BABYLON.Color3(0.5, 0.8, 0.5);
    ground.material = groundMaterial;

    // 地面に物理特性を追加
    ground.physicsImpostor = new BABYLON.PhysicsImpostor(
        ground,
        BABYLON.PhysicsImpostor.BoxImpostor,
        { mass: 0, restitution: 0.9 }, // 地面は固定（mass: 0）
        scene
    );

    // ボールを作成
    const ball = BABYLON.MeshBuilder.CreateSphere("ball", { diameter: 1 }, scene);
    ball.position.y = 5; // ボールを初期位置に配置

    const ballMaterial = new BABYLON.StandardMaterial("ballMat", scene);
    ballMaterial.diffuseColor = new BABYLON.Color3(1, 0.2, 0.5);
    ball.material = ballMaterial;

    // ボールに物理特性を追加
    ball.physicsImpostor = new BABYLON.PhysicsImpostor(
        ball,
        BABYLON.PhysicsImpostor.SphereImpostor,
        { mass: 1, restitution: 0.33 }, // 質量1、反発係数0.9
        scene
    );

    // 障害物の作成
    // 指定座標にBoxを作成する
    var wallMaterial = new BABYLON.StandardMaterial("wallMaterial", scene);
    wallMaterial.emissiveColor = new BABYLON.Color3(0.4, 1, 0.8);
    
    //指定座標に四角い障害物を作成する
    const createBox=(x1,z1,x2,z2,mate) => {
        if(x1>x2){[x1, x2] = [x2, x1];}
        if(z1>z2){[z1, z2] = [z2, z1];}
        const wall = BABYLON.MeshBuilder.CreateBox("box", {width: Math.abs(x1-x2), height: 1, depth: Math.abs(z1-z2)})   
        wall.position.x = x1 + Math.abs(x1-x2)/2;
        wall.position.y = 0.5;
        wall.position.z = z1 + Math.abs(z1-z2)/2;
        wall.material=mate;

        wall.physicsImpostor = new BABYLON.PhysicsImpostor(
            wall,
            BABYLON.PhysicsImpostor.BoxImpostor,
            { mass: 0, restitution: 0.5, friction: 0.1 },
            scene
        );
        return wall;
    }

    //外壁
    const outLinewalls = [
        createBox(4.8,-5,5,5,wallMaterial),
        createBox(-4.8,-5,-5,5,wallMaterial),
        createBox(-5,-4.8,5,-5,wallMaterial),
        createBox(-5,4.8,5,5,wallMaterial),
    ];

    //障害物
    const walls = [
        createBox( 0.8,-1.0 , 1.0, 2.0,wallMaterial),
        createBox( 0.8,-1.0 ,-2.0,-0.8,wallMaterial),
        createBox(-2.0, 0.4 ,-1.8,-5.0,wallMaterial),
        createBox( 1.0, 1.8 ,-3.2, 2.0,wallMaterial),
        createBox(-3.4, 2.0 ,-3.2,-3.3,wallMaterial),
        createBox(-3.4, 5.0 ,-3.2, 3.5,wallMaterial),
        createBox(-2.0, 3.5 ,-1.8, 2.0,wallMaterial),
        createBox(-0.6, 5.0 ,-0.4, 3.5,wallMaterial),
        createBox( 1.0, 3.5 , 0.8, 2.0,wallMaterial),
        createBox( 2.1, 2.0 , 3.6, 3.5,wallMaterial),
        createBox( 0.8, 0.8 , 2.3, 1.0,wallMaterial),
        createBox( 3.5, 0.8 , 5.0, 1.0,wallMaterial),
        createBox( 2.1, 0.8 , 2.3, -2.3,wallMaterial),
        createBox( 3.5, 0.8 , 3.7, -2.3,wallMaterial),
        createBox( 0.8,-3.5 , 2.3, -3.7,wallMaterial),
        createBox( 3.5,-3.5 , 5.0, -3.7,wallMaterial),
        createBox( 0.8,-5.0 , 1.0, -2.5,wallMaterial),
        createBox( 0.8,-2.5 ,-0.5, -2.7,wallMaterial),
    ];


    const init = async () => {
        //DeviceOrientationEventを登録
        try {
            if (typeof DeviceOrientationEvent.requestPermission === 'function') {
                await DeviceOrientationEvent.requestPermission();
            }
            window.addEventListener('deviceorientation', handleOrientation);
        } catch (error) {
            console.log(' permission denied');
        }
    };

    //センサーの値を表示する
    const handleOrientation = (event) => {
        const alpha = (event.alpha || 0);
        const beta = (event.beta || 0) ;
        const gamma = (event.gamma || 0) ;

        alphaText.text = `alpha: ${alpha.toFixed(2)}°`;
        betaText.text = `beta: ${beta.toFixed(2)}°`;
        gammaText.text = `gamma: ${gamma.toFixed(2)}°`;

        // 重力ベクトルを更新（物理エンジンへ反映）
        const gravity = new BABYLON.Vector3(gamma , -9.81, -beta );
        scene.getPhysicsEngine().setGravity(gravity);

    };

    window.addEventListener("resize", () => {
        engine.resize();
        shaderMaterial.setVector2("resolution", new BABYLON.Vector2(window.innerWidth, window.innerHeight));
    });

    return scene;
};
```