# ブラウザから操作可能なラジコンを作る【９-２】

## 【９-２】センサー情報のブレを取り除き、スムーズに動かす。

前回に続き、スマホVRゴーグル用に現在１画面表示になっている要素を
２画面で表示を行うように修正していきます。


<br></br>
## 目次
* [【１】Raspberry pi の、GPIOをTypescriptから操作](https://qiita.com/Yasukosan/items/1bf9480c10de7d69c2a4)
* [【２】DCモーターをPWMで速度制御](https://qiita.com/Yasukosan/items/291c76c10be135fd7595)
* [【３】サーボモーターを制御](https://qiita.com/Yasukosan/items/e81c9c9d0159b32e5893)
* [【４】DualSenseをブラウザに接続](https://qiita.com/Yasukosan/items/bf3a085145db84a48823)
* [【５】DualSenseの情報をRaspberryPiに飛ばす](https://qiita.com/Yasukosan/items/400883be94751966221a)
* [【６】DualSenseのチャタリング？問題対応](https://qiita.com/Yasukosan/items/7e52609699b4b71196ca)
* [【７-１】RaspberryPiから低遅延で映像を飛ばす](https://qiita.com/Yasukosan/items/98426f1ba4dea4c9ec5f)
* [【７-２】RaspberryPiから低遅延で映像を飛ばす](https://qiita.com/Yasukosan/items/2246c8a32602ca77ace9)
* [【８】ThreeJSでVRもどきを作成](https://qiita.com/Yasukosan/items/e15e8fd002cf49e3d04d)
* [【９-１】iPhoneの加速度から頭の向きをVRに反映](https://qiita.com/Yasukosan/items/9b4333770baea4110361)
* [【９-２】スマホVRゴーグル向けデザインに変える](https://qiita.com/Yasukosan/items/7f1bb7a9d95cb76f8117)
* [【１０】ラジコン本体の製作](https://qiita.com/Yasukosan/items/0d37c1df5b5d6c02257b)
* 【１１】パワーアップとバッテリー問題の解決




<br></br>
## コード修正

理想を言うと、iPhoneにWebXRのWebVRAPIが残っていれば良かったのですが
消えてしまったので、VR画面ぽいデザインで乗り越えます。

映像を出力する画面が２つあり、VRゴーグルの左右の目の部分にフィットするように収めます。
CSSの調整だけの問題ですので何とかなると思います。

デザイン以外にも、画面を2つ出力する場合
ThreeJSのレンダラーを２つ用意する必要があります。

出力先も２つになりますので、設定情報他細かいところが変わります。

設定
```typescript
const Spec = {
    ．．．．．．
    Renderer: {
        videoTarget: 'remote-video',
        xrViewTarget1: 'xr_view1',
        xrViewTarget2: 'xr_view2',
    }
}
```

レンダラー
```typescript
    renderer1 = new WebGLRenderer({antialias: true})
    renderer1.setPixelRatio(window.devicePixelRatio)
    renderer1.setSize(Spec.Screen.width * 0.48, Spec.Screen.height * 0.8)

    renderer2 = new WebGLRenderer({antialias: true})
    renderer2.setPixelRatio(window.devicePixelRatio)
    renderer2.setSize(Spec.Screen.width * 0.48, Spec.Screen.height * 0.8)

    xrViewTarget1 = document.getElementById(Spec.Renderer.xrViewTarget1) as HTMLElement
    xrViewTarget2 = document.getElementById(Spec.Renderer.xrViewTarget2) as HTMLElement
    xrViewTarget1.appendChild(renderer1.domElement)
    xrViewTarget2.appendChild(renderer2.domElement)
```

レンダリングアニメーション
```typescript
    // アニメーションループ
    const animate = () => {
        if (!scene || !camera || !renderer1 || !renderer2 || !videoTexture) return
        animateId = requestAnimationFrame(animate)
        videoTexture.needsUpdate = true;
        renderer1.render(scene, camera)
        renderer2.render(scene, camera)
    }
```

変更内容を含めた全文になります。

<details><summary>panorama.service.ts</summary>

```typescript

'use client'
import {
    Scene, PerspectiveCamera, WebGLRenderer,
    Mesh, MeshBasicMaterial,
    SphereGeometry,
    Texture, MathUtils
} from "three" 

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

import { KalmanFilter } from './filter.helper'


const Spec = {
    Screen: {
        width: 990,
        height: 510,
    },
    Camera: {
        fov: 72,
        near: 0.1,
        far: 1000,
        x: 0,
        y: 1.6,
        z: 1.2,
    },
    Sphere: {
        radius: 400,
        widthSegments: 60,
        heightSegments: 40,
    },
    Controls: {
        enableZoom: false,
        enablePan: false,
        enableRotate: true,
        rotateSpeed: -0.2,
    },
    Renderer: {
        videoTarget: 'remote-video',
        xrViewTarget1: 'xr_view1',
        xrViewTarget2: 'xr_view2',
    }
}

type SpecType = typeof Spec

/**
 * 
 * @param spec Partial<SpecType["Screen"]>
 * @returns
 * 
 * @argument width: number  // スクリーンの幅
 * @argument height: number // スクリーンの高さ
 */
export const setScreenSize = (spec: Partial<SpecType["Screen"]>) => {
    Object.assign(Spec.Screen, spec)
}

/**
 * 
 * @param spec Partial<SpecType["Camera"]>
 * @returns
 * 
 * @argument fov: number  // 視野角
 * @argument near: number // カメラの最近距離
 * @argument far: number  // カメラの最遠距離
 * @argument x: number    // カメラのx座標
 * @argument y: number    // カメラのy座標
 * @argument z: number    // カメラのz座標
 */
export const setCameraSpec = (spec: Partial<SpecType["Camera"]>) => {
    Object.assign(Spec.Camera, spec)
}

/**
 * 
 * @param spec Partial<SpecType["Sphere"]>
 * @returns
 * 
 * @argument radius: number        // 球体の半径
 * @argument widthSegments: number // 球体の横分割数
 * @argument heightSegments: number // 球体の縦分割数
 */
export const setCameraPosition = (spec: Partial<SpecType["Camera"]>) => {
    Object.assign(Spec.Camera, spec)
}

/**
 * 
 * @param spec Partial<SpecType["Sphere"]>
 * @returns
 * 
 * @argument radius: number        // 球体の半径
 * @argument widthSegments: number // 球体の横分割数
 * @argument heightSegments: number // 球体の縦分割数
 */
export const setSphereSpec = (spec: Partial<SpecType["Sphere"]>) => {
    Object.assign(Spec.Sphere, spec)
}

/**
 * 
 * @param spec Partial<SpecType["Controls"]>
 * @returns
 * 
 * @argument enableZoom: boolean   // ズームを有効にするか
 * @argument enablePan: boolean    // パンを有効にするか
 * @argument enableRotate: boolean // 回転を有効にするか
 * @argument rotateSpeed: number   // 回転速度
 */
export const setControlsSpec = (spec: Partial<SpecType["Controls"]>) => {
    Object.assign(Spec.Controls, spec)
}

/**
 * 
 * @param spec Partial<SpecType["Renderer"]>
 * @returns
 * 
 * @argument clearColor: number    // 背景色
 * @argument videoTarget: string   // ビデオターゲットのID
 * @argument xrViewTarget: string  // XRビューターゲットのID
 */
export const setRendererSpec = (spec: Partial<SpecType["Renderer"]>) => {
    Object.assign(Spec.Renderer, spec)
}

// カメラの角度を変更する関数
export const setCameraAngle = (
    lookAtX: number,
    lookAtY: number,
    lookAtZ: number
) => {
    if (!camera) return
    const filteredX = kfX.filter(lookAtX);
    const filteredY = kfY.filter(lookAtY);
    const filteredZ = kfZ.filter(lookAtZ);

    // 加速度を積分して速度を計算
    velocity.x += filteredX * 0.01; // 0.01はサンプリング間隔の例
    velocity.y += filteredY * 0.01;
    velocity.z += filteredZ * 0.01;

    // 速度を減衰させる
    velocity.x *= dampingFactor;
    velocity.y *= dampingFactor;
    velocity.z *= dampingFactor;
    

    // 速度を積分して位置を計算し、カメラの回転を更新
    cameraRotation.x += MathUtils.degToRad(velocity.x);
    cameraRotation.y -= MathUtils.degToRad(velocity.y);
    cameraRotation.z += MathUtils.degToRad(velocity.z);

    camera.rotation.x = cameraRotation.x;
    camera.rotation.y = cameraRotation.y;
    camera.rotation.z = cameraRotation.z;
}



let scene: Scene | null = null
let camera: PerspectiveCamera | null = null
let renderer1: WebGLRenderer | null = null
let renderer2: WebGLRenderer | null = null
let videoTexture: Texture | null = null

let videoTarget: HTMLVideoElement | null = null
let xrViewTarget1: HTMLElement | null = null
let xrViewTarget2: HTMLElement | null = null

let animateId: number | null = null

const kfX = new KalmanFilter({ R: 0.01, Q: 4 });
const kfY = new KalmanFilter({ R: 0.01, Q: 4 });
const kfZ = new KalmanFilter({ R: 0.01, Q: 4 });

// カメラの回転を保持する変数
let cameraRotation = { x: 0, y: 0, z: 0 };
let velocity = { x: 0, y: 0, z: 0 };

// 減衰係数
const dampingFactor = 0.6;
const decay = 0.8;

/**
 * 初期化関数
 */
export const init = () => {
    // シーン、カメラ、レンダラーのセットアップ
    scene = new Scene()
    camera = new PerspectiveCamera(
                Spec.Camera.fov,
                Spec.Screen.width / Spec.Screen.height,
                Spec.Camera.near,
                Spec.Camera.far
            )

    // カメラの位置設定
    camera.position.set(Spec.Camera.x, Spec.Camera.y, Spec.Camera.z)
    scene.add(camera)
    camera.lookAt(scene.position);

    // ビデオテクスチャの作成
    videoTarget = document.getElementById(Spec.Renderer.videoTarget) as HTMLVideoElement
    videoTarget.play()

    // キャンバスの作成
    const canvas = document.createElement('canvas');
    canvas.width = Spec.Screen.width;
    canvas.height = Spec.Screen.height;
    const context = canvas.getContext('2d');

    // アニメーションループでビデオをキャンバスに描画
    function drawVideoToCanvas() {
        if (context && videoTarget) {
            context.fillStyle = 'green';
            context.fillRect(0, 0, canvas.width, canvas.height);
            context.drawImage(videoTarget, 1000, 200, Spec.Screen.width / 1.5, Spec.Screen.height / 1.5);
        }
        requestAnimationFrame(drawVideoToCanvas);
    }
    drawVideoToCanvas();

    videoTexture = new Texture(canvas);
    videoTexture.needsUpdate = true

    // 球体ジオメトリとマテリアルの作成
    const geometry = new SphereGeometry(Spec.Sphere.radius, Spec.Sphere.widthSegments, Spec.Sphere.heightSegments)
    geometry.scale(-1, 1, 1); // 内側に描画するためにスケールを反転
    const material = new MeshBasicMaterial({ map: videoTexture })

    // メッシュの作成とシーンへの追加
    const sphere = new Mesh(geometry, material)
    scene.add(sphere)

    renderer1 = new WebGLRenderer({antialias: true})
    renderer1.setPixelRatio(window.devicePixelRatio)
    renderer1.setSize(Spec.Screen.width * 0.48, Spec.Screen.height * 0.8)

    renderer2 = new WebGLRenderer({antialias: true})
    renderer2.setPixelRatio(window.devicePixelRatio)
    renderer2.setSize(Spec.Screen.width * 0.48, Spec.Screen.height * 0.8)

    xrViewTarget1 = document.getElementById(Spec.Renderer.xrViewTarget1) as HTMLElement
    xrViewTarget2 = document.getElementById(Spec.Renderer.xrViewTarget2) as HTMLElement
    xrViewTarget1.appendChild(renderer1.domElement)
    xrViewTarget2.appendChild(renderer2.domElement)

    // OrbitControlsの設定
    const controls = new OrbitControls(camera, renderer1.domElement)
    controls.enableZoom = Spec.Controls.enableZoom
    controls.enablePan = Spec.Controls.enablePan
    controls.enableRotate = Spec.Controls.enableRotate
    controls.rotateSpeed = Spec.Controls.rotateSpeed
    controls.maxPolarAngle = Math.PI * 0.6
    controls.minPolarAngle = Math.PI * 0.4

    // アニメーションループ
    const animate = () => {
        if (!scene || !camera || !renderer1 || !renderer2 || !videoTexture) return
        animateId = requestAnimationFrame(animate)
        videoTexture.needsUpdate = true;
        renderer1.render(scene, camera)
        renderer2.render(scene, camera)
    }

    animate()
}

export const stopAnimation = () => {
    scene = null
    camera = null
    renderer1 = null
    renderer2 = null
    videoTarget = null
    xrViewTarget1 = null
    xrViewTarget2 = null
    cancelAnimationFrame(animateId!)
}

```

</details>


## 試験

以上の変更を行い、デザインをいい感じに調整できると
こんな感じに近いものになるかと思います。

<iframe width="560" height="315" src="https://www.youtube.com/embed/4CqDQ3-hs8o?si=iLJLcKX1Bk_v-zyv" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

この状態で、VRゴーグルに装着すると
しっかりVR体験が出来ます。



<br></br>
<br></br>
# UZAYA
Uzayaでは、多分仕事を求めています。
何かの役に立ちそうでしたら、是非お知らせを。