Webブラウザで業務をVRっぽくし表示てみようという試み	

# はじめに	
XR面白いですよね。
とはいえ、HololensやXRを一般業務に提案するのもなかなか難しそう。

なので、Webブラウザでそれっぽい表示が出来たら業務アプリにしれっとねじ込めていいんじゃないかと、Three.jsを少し齧って作ってみました。
今回は場所が意味を持ちデータと紐づくイメージのしやすい、資産管理、在庫管理　ぽいものです。

## 出来たもの
![webgl1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/7ead5a40-0769-df59-1066-e01b7fd11645.gif)
カーソルをオブジェクトに重ねると情報が表示されたり
表示方法を選択することで、データに合わせて着色したりサイズを変えたりできます。


↓下記のURLにWebページがあります↓

https://ddy.nukenin.jp/stockdemo2022.html

## 開発環境

以下の環境で作成しました。

|項目|使用したもの|
|:-|:-|
|OS|Windwos10|
|IDE|VisualStudioCode|
|言語<br/>ライブラリ|JavaScript <br/> Three.js <br/> JQuery|
|Webサーバ|忍者HP|


余談ですが、VSCodeのLiveServer便利ですね。Webサーバ用意しなくてもいいんで今回のような開発はすげー楽でした。ありがたや。

## 実装
HTMLのレイアウトは下記のように。
```html:HTML部分概要
    <body>
	<div class="pageHeader"></div>
        <div class="pageMenu">ここに操作用のチェックボックスとか各</div>
        <div class="pageMain">
            <canvas id="cvs"></canvas>
        </div>
        <div class="pageFooter"></div>
    </body>
```
枠組みはこんな感じ
![html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/d25d4e0b-996c-30a4-dcbf-961a29bd92f4.png)<details><summary>css　（grid-templateを試してみました）</summary><div>
    body {
        display:grid;
        margin:0;
        grid-template-rows:40px 1fr 100px;
        grid-template-columns:300px 1fr;
    }

    .pageHeader {
        grid-row:1;
        grid-column: 1/ span 2;
        background-color: lightsteelblue;
        border-radius: 10px 10px 0 0 ;
        padding: 8px;
    }
    
    .pageMenu {
        grid-row:2;
        grid-column:1;
        background-color:lightsteelblue;
        padding:8px;
    }
    
    .pageMain {
        grid-row:2;
        grid-column:2;
        background-color:cornsilk;
    }
    
    .pageFooter {
        grid-row:3;
        grid-column: 1 / span 2;
        background-color: lightsteelblue;
        border-radius: 0 0 10px 10px;
        padding: 8px;
    }
</div></details>


まずCanvasでThreeJSを使う初期設定をします。
・定番の処理なので割愛（sceneを設定して、照明とカメラを配置）
・カメラは簡単に操作できるオービットコントロールにしました。

#### 地面の生成
・PlaneGeometryで平面を作成して、地図画像を張り付けた地面を作成します。
　使用したのは国土地理院の地図から。（GoogleMapはハードコピーの利用NGだったので）
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/e4994dcf-3a0d-2f74-3961-aadb862ab288.png">
```js:地面の生成
	var tiTexture = new THREE.TextureLoader().load('./img/地面画像.png',
	(tex)=>{
		const geometry = new THREE.PlaneGeometry(200,150,10,10);
		const material = new THREE.MeshPhongMaterial({map:tiTexture});
		const ground = new THREE.Mesh(geometry, material);
		scene.add(ground);
	});
```

#### ３Ｄモデルの準備と読み込み
倉庫やオフィスの３Dモデルを用意します。
今回はglbファイルを使用しました。（Blenderで作れる形式）					
Blenderも覚えたかったので、Blenderでオフィスビルと倉庫、建物の中に配置する棚と机を作成しました。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/aec644a4-d7d9-179d-6361-ff55d45706ff.png">　<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/bb4f89c7-cfb4-9a8b-74b5-147a7bb05a26.png">　<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/9c667d47-dae8-6af8-75a2-42c3f1a51c62.png">

作成したデータは、Blenderのエクスポートでglb形式で保存。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/837c47bf-80c3-7f12-d4a7-9cb6056bff93.png">
３Dモデルの用意が出来たので、倉庫とオフィスを地面の上に配置していきます。
Three.jSの中にあるGLTFLoadeで、glbファイルを読み込んでsceneへadd。
```js:オフィスビルの読み込み
	glbLoader.load('./img/build.glb', function (gltf){
		model = gltf.scene;
		model.traverse((object)=>{
			if(object.isMesh){
				object.material.transparent = true;
				//object.material.opacity=0.6;　←　窓を透明にしたかった残骸
				object.position.set(15,1,15);
				object.rotation.y = 3.4;
			}
		});
		scene.add(model);
	}, undefined, function(e){
		console.error(e);
	});
```

__早速問題発生__
ブラウザで表示したところ、ビルのガラスが半透明になりませんでした。
Blender上の機能全てがブラウザで対応はしてないので同じ見た目にならないのは仕方ないですが、半透明になってくれないのはちょっとなぁ。

←　Blenderで編集中　　　　　　　　Webブラウザで表示した状態　→
![aaa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/e4b90ece-925c-4b67-f451-ab780e094325.png)


光沢とかはともかく、半透明の場所をコントロールできないのは色々と制約になりそうなので少し調べてみます。
窓の中の情報を表示したいとか、細胞を半透明で表示してミトコンドリアを見たいとかそういうのやりたくなった時。

Three.jsのGLTFLoaderにmaterial.opacityという半透明の指定があるので、それを指定してみると、ビル全体が半透明に（そりゃそうだ）
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/6f3c726e-8735-6548-a02d-5e36a3964e22.png">

Blenderのマテリアルとか見てみるけど、こちらでは半透明になっているので一体どこを触れば…というところで躓いてしまいました。
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/c4b8bb47-6514-5442-323b-0ee1d00bde62.png">
普通はαチャンネルとかでやるもんだろうけどマテリアルのプロパティに見当たらな・・・・
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/8f3cd4db-f6df-bdbb-5c02-6c02f3d83134.png">
・・・あったよαチャンネル。
マウスカーソル右端に持っていったらスクロールバーが出てきて、下に隠れてました。スクロールバーは隠れないで欲しいかも。
自分のPCは1920ｘ1200の画面なんですがBlender使うには狭いですね。でかいディスプレイ欲しーい。					
というわけで窓のマテリアルのαチャンネルを設定して再度ファイルをエクスポート。

ちゃんと窓だけが半透明になりました！
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/f1aace05-7853-0665-9241-db11e962e146.png">
次は、棚や机の表示です。
複数並べたいので、配列を作って座標を変えながら繰り返しで読み込む。（同じファイルを何度も呼び出すのもちょっとアレだけど動かすの優先で目を瞑る）
```js:棚の配列
	let tanaData=[
		{"id":1 ,"x": 1, "y":4.5, "z":-13.5, "rx":1.5708},
		{"id":2 ,"x": 7, "y":4.5, "z":-13.5, "rx":1.5708},
		{"id":3 ,"x":13, "y":4.5, "z":-13.5, "rx":1.5708},
          略
	];
    let arrayListTana = JSON.parse(JSON.stringify(tanaData));
    let tanaCnt=0;
    let tanaAry=[];
    for(let tCnt = 0; tCnt < arrayListTana.length; tCnt++){
        glbLoader.load('./img/tana.glb', function (gltf) {
            model = gltf.scene;
            model.traverse((object)=>{
                if(object.isMesh){
                    object.material.transparent = true;
                    object.material.opacity=0.6;
                    object.material.depthTest = true;
                    object.position.x = arrayListTana[tCnt].x;
                    object.position.y = arrayListTana[tCnt].y;
                    object.position.z = arrayListTana[tCnt].z;
                    object.rotation.y = arrayListTana[tCnt].rx;
                }
                scene.add(object);
            });
        });
    }
```


__ここまでの実装__
ちょっとそれっぽい見た目になってきました。
![Image11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/05db9d8b-1dae-5632-f6ba-94857f433514.png)


#### データ（商品）を置く

場所の他に、商品名や在庫数等を持たせるので、下記のようなJSONデータを用意しました。
```js:商品の配列
        {"name":" カクテルシェーカーS ","x": 38 ,"y": 5 ,"z": -13 ,"在庫": 21 ,"注目": 0 ,"情報":" 在庫 : 21 "},
        {"name":" メジャーカップ ","x": 38 ,"y": 6.5 ,"z": -13 ,"在庫": 6 ,"注目": 0 ,"情報":" 在庫 : 6 "},
```
						
これを立方体で表現します。
```js:商品を箱型で表示する
    const cube=new THREE.Mesh(geometry, material);
    cube.position.set(arrayList1[i].x, yy, arrayList1[i].z);
    cube.objName=arrayList1[i].name;
    cube.objInfo1=arrayList1[i].情報;
    scene.add(cube);
```
__ここまでの表示__
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/227de4ca-9c26-eb0d-7e2e-d6627a05e694.png">

表示する立方体ですが、白い豆腐が並んでいるだけでは寂しいので下記の機能を追加しとこうと思います。
・在庫数によって、着色したい（少なくなったらだんだん赤くする）
・在庫数によって、高さを可変にしたい。（BIっぽい）
・マウスカーソルを当てたら詳細情報を表示したい
![menu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/d9e0580c-8f6e-9661-f26f-8fa726b0b370.png)
画面からの操作で、色やサイズを変更するので上記のようなチェックボックスを追加。
商品用のデータの表示処理を、一旦全クリアして変更後の情報で再作成する処理にします。
（横着ですがとにかく動くの優先。）

在庫数で高さを変更する指定が入っていれば、Heightを変更する処理を追加
```js:チェックがONであれば、トルグ値と在庫数によって高さを再設定
            if($('#chkStockHeight').prop('checked')){
                arrayList1[i].height = arrayList1[i].在庫 * $("#rngHeight").val() * 0.001;
```

着色のチェックがONなら、着色を行う
```js:数が少なければ赤く、多ければだんだん緑色になる用カラーコードを生成して、プロパティにセット
    if($('#chkStockColor').prop('checked')){
        if(i数量<=50){
            iColR=255;
            iColG=Math.floor((i数量 / 50)*255);
        }else{
            if(i数量>100){
                iColR=0;
            }else{
                iColR=Math.floor(Math.abs((100-i数量)/50)*255);
            }
            iColG=255;
        }
        let col = "0x" + iColR.toString(16) + iColG.toString(16)+"00";
        略
        cube.color=parseInt(col);
```
__ここまでの表示__
色のチェックボックスをクリックすると在庫数に合わせて色が付き
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/1d18a19a-f926-f7b1-38de-71b9c960f585.png">
高さのチェックボックスをクリックすると在庫数に合わせて高さが変化します
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/fcd89d77-c15d-7bc4-9b42-7e14bbb9c112.png">
ついでに、注目データの表示もしましょう。多分在庫切れ間近とか目立たせたい気がする。	
注目のチェックボックスがONなら、在庫数関係なくマゼンダ色にしてデータの上のあたりに、特記事項になる文章を表示します。


#### ３Ｄ空間に文字を表示する時の問題点
３Ｄ空間って、方向があるので文字を正面以外から見ると見えにくくなりますよね。
以前、XRTechNagoyaで３Ｄ用のフォントを作っている方がいらっしゃいましたが、今回はどうしたものかと調べていると
ThreeJSにSpriteがあるようです。これだと向きが無いので正面からの表示になるとのこと。
スプライトってファミコンとかX68000とかのイメージでしたが久しぶりの再会です。２０年以上ぶり？
使い方は３Ｄモデルと同じく座標を指定してaddで配置する感じですね。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/a8ddf0f9-bbef-e6ac-0f51-11d57951be64.png">

色や画像や文字を指定して、こんな感じになりました。
```js:Spriteのマテリアルを作成して、sceneにaddする
        const spriteMaterial = new THREE.SpriteMaterial({map: texture });
        const sprite = new THREE.Sprite(spriteMaterial);
        sprite.scale.set(scale.x,scale.y,scale.z);
        sprite.position.set(position.x,position.y,position.z);
        scene.add(sprite);
```
３Ｄ空間内に配置するので、遠くなると小さくなりますが文字が斜めになったりはしないですね。

#### 箱型以外のデータ表示
Blenderで自作以外の３Dデータの利用を試してみようと思います。
オフィスビルの机の上に、ノートパソコンを表示。
![tofucolor4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/8b3b40fa-a981-c7d3-1262-308f90619f64.png)
ノートPCは　cgtraderさんのサイトに無料のものがありましたので拝借しました。
ダウンロードしたファイルは棚データと同じようにJSONデータからglbファイルを読み込んで机の上の座標を指定して表示します

もう一つ、フォトグラメトリから生成したモデルも試しに配置してみます。
3DZepherを使用して、料理の写真からデータ作成しました。作ったファイルはglbでエクスポート。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/66d63701-00db-ab53-b54a-383fa2eb789e.gif">
ビルと同じようにまずglbファイルをアップロード・・・・アップロードできねぇorz

確認したら忍者ホームページのアップロード出来るファイルサイズが上限10MBでした。
生成されたファイルは約50MB。ファイルサイズが大き過ぎた。

再度3DZepherで作成。オプションで低ポリゴンを指定して作り直してみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/b2d63b80-37a0-6c0b-1135-629aca4da7e9.png">

Oh・・・これはかなり低ポリゴン	
ファイルサイズは２ＭＢになりました。まぁ今回は小さくしか見えないと思うのでヨシ！

アップロードして、同じようにGLTFLoadeで読込ます。
![omama3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/526babc7-eb4a-9ded-8dd7-1985bee18a0e.png)
いやでかいでかい。__隕石落したみたいじゃん__
デフォルトのサイズやXYZ軸はモデリングするソフト次第なので、そのあたり気にしないとですね。
今回はモデルのサイズがでか過ぎるのと、XYZ座標が異なるようです。
読み込み時にサイズを指定するようにscale.setで調整（思いっきり縮小）

    object.scale.set(arrPcList[i].scale,arrPcList[i].scale,arrPcList[i].scale);

![office1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/150da210-3eff-52bd-13c4-a4f98e2e4766.png)
オフィス内の表示が出来ました。

#### マウスカーソルでオブジェクトを指定する
さて、３Dで表示しただけでは、表示されているものが何者なのかが判りません。
マウスカーソルを重ねたら、詳細情報が表示されるとかやりたいですね。

マウスとオブジェクトの衝突処理を調べてみると、どうやらレイキャストのインターセクトオブジェクトというのを使うと、カメラとマウスを結んだ線に重なるオブジェクトを確認できるみたいです。
これを使うと、マウスと重なったオブジェクトを手前から配列で取得できるようなので、配列の０番にオブジェクトがあれば（一番手前）その色を変更して、情報を表示するようにします。
```js:mouseとcameraを渡すと、その直線状にあるものをintersectsに返す
    raycaster.setFromCamera(mouse,camera);
    const intersects = raycaster.intersectObjects(scene.children);
```

コンソールで確認してみると
![src1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/39d0823d-9178-a458-1038-1cadb63a54fd.png)				
取れてるみたいですね。
一番手前が[0]、奥に行くにつれて[1][2]...となるようです。

上記で[0]にオブジェクトが取得できていれば、htmlの情報欄に、商品名と、情報を表示します。
```js:カーソル部にオブジェクトがあれば、色を変えて、ラベルに情報を表示する
        if(intersects.length > 0){
            niList.map((mesh)=>{
                if( mesh === intersects[0].object){
                    mesh.material.color.setHex(0xCf00C0);
                    $("#lblName").text(mesh.objName);
                    $("#lblInfo1").html(mesh.objInfo1);
```

Cubeで作成した箱（商品）にカーソルを当てれば商品情報が表示できるようになりました。
![nicheck1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/5b0fe3d0-c08c-5532-d5a1-38257c0c5fa6.gif)
これで試してみたかったことはまぁまぁ実現できました。
＼(^o^)／ｵﾜﾀ

データ量がさほど多くないのもありますが、横着なコードの割に動きますね。

## 得られた知見
__・Three.JS すげぇ！__
__・商品を座標指定で置いたけど、棚や机に置けるデータ構成にしないと位置合わせで死ねる。__
__・mouseClickの操作を入れようとしたけど発火しなかった。オービットコントロールに持っていかれてるかも。
　（mouseMoveは上記の通り呼べた）__
__・Babylon.JS てのもいいらしい。（あとでやる）__

## 最後に

以上、一般のPCやスマホのブラウザで動くWebGL関係でそれっぽいWebページを作ってみました。ソースはかなりアレですが見た目はそこそこになったのではないかなと思います。

今回はイメージしやすい __在庫量__ を対象にしましたが、これだと商品が山積みか在庫切れしそうかくらいしか解らないので、倉庫でやるなら __「倉庫　効率化」__ とかで検索して出てくるような項目を表示したほうが、現場のユーザーさんへの訴求力が上がると思います。
また、最近はGISデータも手軽に使えるので下記のようなページも作れますし、倉庫に拘らず色んな用途に応用できるのではないでしょうか。

__名古屋駅周辺GISデータを３ＤモデルにしてWebページで表示したデモ__
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594551/728c0649-d414-ea2f-1195-a52e82cc3abb.gif">

https://ddy.nukenin.jp/gis_test.html


Webブラウザはどんどん進化していますが、業務アプリはGridView形式でデータの一覧表示を行っているのが多数派かと思います。
一覧表で表示されても解りにくいようなデータや画面があった場合、足元からVRのようなシステムを浸食させて、現場のユーザーさんの業務を楽しくさせてみたら素敵ではないでしょうか。