【9-2】センサー情報のブレを取り除き、スムーズに動かす。
前回に続き、スマホVRゴーグル用に現在1画面表示になっている要素を
2画面で表示を行うように修正していきます。
目次
- 【1】Raspberry pi の、GPIOをTypescriptから操作
- 【2】DCモーターをPWMで速度制御
- 【3】サーボモーターを制御
- 【4】DualSenseをブラウザに接続
- 【5】DualSenseの情報をRaspberryPiに飛ばす
- 【6】DualSenseのチャタリング?問題対応
- 【7-1】RaspberryPiから低遅延で映像を飛ばす
- 【7-2】RaspberryPiから低遅延で映像を飛ばす
- 【8】ThreeJSでVRもどきを作成
- 【9-1】iPhoneの加速度から頭の向きをVRに反映
- 【9-2】スマホVRゴーグル向けデザインに変える
- 【10】ラジコン本体の製作
- 【11】パワーアップとバッテリー問題の解決
コード修正
理想を言うと、iPhoneにWebXRのWebVRAPIが残っていれば良かったのですが
消えてしまったので、VR画面ぽいデザインで乗り越えます。
映像を出力する画面が2つあり、VRゴーグルの左右の目の部分にフィットするように収めます。
CSSの調整だけの問題ですので何とかなると思います。
デザイン以外にも、画面を2つ出力する場合
ThreeJSのレンダラーを2つ用意する必要があります。
出力先も2つになりますので、設定情報他細かいところが変わります。
設定
const Spec = {
......
Renderer: {
videoTarget: 'remote-video',
xrViewTarget1: 'xr_view1',
xrViewTarget2: 'xr_view2',
}
}
レンダラー
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)
レンダリングアニメーション
// アニメーションループ
const animate = () => {
if (!scene || !camera || !renderer1 || !renderer2 || !videoTexture) return
animateId = requestAnimationFrame(animate)
videoTexture.needsUpdate = true;
renderer1.render(scene, camera)
renderer2.render(scene, camera)
}
変更内容を含めた全文になります。
panorama.service.ts
'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!)
}
試験
以上の変更を行い、デザインをいい感じに調整できると
こんな感じに近いものになるかと思います。
この状態で、VRゴーグルに装着すると
しっかりVR体験が出来ます。
UZAYA
Uzayaでは、多分仕事を求めています。
何かの役に立ちそうでしたら、是非お知らせを。