はじめに
XR面白いですよね。
とはいえ、HololensやXRを一般業務に提案するのもなかなか難しそう。
なので、Webブラウザでそれっぽい表示が出来たら業務アプリにしれっとねじ込めていいんじゃないかと、Three.jsを少し齧って作ってみました。
今回は場所が意味を持ちデータと紐づくイメージのしやすい、資産管理、在庫管理 ぽいものです。
出来たもの
カーソルをオブジェクトに重ねると情報が表示されたり
表示方法を選択することで、データに合わせて着色したりサイズを変えたりできます。
↓下記のURLにWebページがあります↓
開発環境
以下の環境で作成しました。
項目 | 使用したもの |
---|---|
OS | Windwos10 |
IDE | VisualStudioCode |
言語 ライブラリ |
JavaScript Three.js JQuery |
Webサーバ | 忍者HP |
余談ですが、VSCodeのLiveServer便利ですね。Webサーバ用意しなくてもいいんで今回のような開発はすげー楽でした。ありがたや。
実装
HTMLのレイアウトは下記のように。
<body>
<div class="pageHeader"></div>
<div class="pageMenu">ここに操作用のチェックボックスとか各</div>
<div class="pageMain">
<canvas id="cvs"></canvas>
</div>
<div class="pageFooter"></div>
</body>
枠組みはこんな感じ
css (grid-templateを試してみました)
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;
}
まずCanvasでThreeJSを使う初期設定をします。
・定番の処理なので割愛(sceneを設定して、照明とカメラを配置)
・カメラは簡単に操作できるオービットコントロールにしました。
地面の生成
・PlaneGeometryで平面を作成して、地図画像を張り付けた地面を作成します。
使用したのは国土地理院の地図から。(GoogleMapはハードコピーの利用NGだったので)
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);
});
3Dモデルの準備と読み込み
倉庫やオフィスの3Dモデルを用意します。
今回はglbファイルを使用しました。(Blenderで作れる形式)
Blenderも覚えたかったので、Blenderでオフィスビルと倉庫、建物の中に配置する棚と机を作成しました。
作成したデータは、Blenderのエクスポートでglb形式で保存。
3Dモデルの用意が出来たので、倉庫とオフィスを地面の上に配置していきます。
Three.jSの中にあるGLTFLoadeで、glbファイルを読み込んでsceneへadd。
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ブラウザで表示した状態 →
光沢とかはともかく、半透明の場所をコントロールできないのは色々と制約になりそうなので少し調べてみます。
窓の中の情報を表示したいとか、細胞を半透明で表示してミトコンドリアを見たいとかそういうのやりたくなった時。
Three.jsのGLTFLoaderにmaterial.opacityという半透明の指定があるので、それを指定してみると、ビル全体が半透明に(そりゃそうだ)
Blenderのマテリアルとか見てみるけど、こちらでは半透明になっているので一体どこを触れば…というところで躓いてしまいました。
普通はαチャンネルとかでやるもんだろうけどマテリアルのプロパティに見当たらな・・・・
・・・あったよαチャンネル。
マウスカーソル右端に持っていったらスクロールバーが出てきて、下に隠れてました。スクロールバーは隠れないで欲しいかも。
自分のPCは1920x1200の画面なんですがBlender使うには狭いですね。でかいディスプレイ欲しーい。
というわけで窓のマテリアルのαチャンネルを設定して再度ファイルをエクスポート。
ちゃんと窓だけが半透明になりました!
次は、棚や机の表示です。
複数並べたいので、配列を作って座標を変えながら繰り返しで読み込む。(同じファイルを何度も呼び出すのもちょっとアレだけど動かすの優先で目を瞑る)
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);
});
});
}
データ(商品)を置く
場所の他に、商品名や在庫数等を持たせるので、下記のようなJSONデータを用意しました。
{"name":" カクテルシェーカーS ","x": 38 ,"y": 5 ,"z": -13 ,"在庫": 21 ,"注目": 0 ,"情報":" 在庫 : 21 "},
{"name":" メジャーカップ ","x": 38 ,"y": 6.5 ,"z": -13 ,"在庫": 6 ,"注目": 0 ,"情報":" 在庫 : 6 "},
これを立方体で表現します。
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);
表示する立方体ですが、白い豆腐が並んでいるだけでは寂しいので下記の機能を追加しとこうと思います。
・在庫数によって、着色したい(少なくなったらだんだん赤くする)
・在庫数によって、高さを可変にしたい。(BIっぽい)
・マウスカーソルを当てたら詳細情報を表示したい
画面からの操作で、色やサイズを変更するので上記のようなチェックボックスを追加。
商品用のデータの表示処理を、一旦全クリアして変更後の情報で再作成する処理にします。
(横着ですがとにかく動くの優先。)
在庫数で高さを変更する指定が入っていれば、Heightを変更する処理を追加
if($('#chkStockHeight').prop('checked')){
arrayList1[i].height = arrayList1[i].在庫 * $("#rngHeight").val() * 0.001;
着色のチェックがONなら、着色を行う
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);
ここまでの表示
色のチェックボックスをクリックすると在庫数に合わせて色が付き
高さのチェックボックスをクリックすると在庫数に合わせて高さが変化します
ついでに、注目データの表示もしましょう。多分在庫切れ間近とか目立たせたい気がする。
注目のチェックボックスがONなら、在庫数関係なくマゼンダ色にしてデータの上のあたりに、特記事項になる文章を表示します。
3D空間に文字を表示する時の問題点
3D空間って、方向があるので文字を正面以外から見ると見えにくくなりますよね。
以前、XRTechNagoyaで3D用のフォントを作っている方がいらっしゃいましたが、今回はどうしたものかと調べていると
ThreeJSにSpriteがあるようです。これだと向きが無いので正面からの表示になるとのこと。
スプライトってファミコンとかX68000とかのイメージでしたが久しぶりの再会です。20年以上ぶり?
使い方は3Dモデルと同じく座標を指定して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);
3D空間内に配置するので、遠くなると小さくなりますが文字が斜めになったりはしないですね。
箱型以外のデータ表示
Blenderで自作以外の3Dデータの利用を試してみようと思います。
オフィスビルの机の上に、ノートパソコンを表示。
ノートPCは cgtraderさんのサイトに無料のものがありましたので拝借しました。
ダウンロードしたファイルは棚データと同じようにJSONデータからglbファイルを読み込んで机の上の座標を指定して表示します
もう一つ、フォトグラメトリから生成したモデルも試しに配置してみます。
3DZepherを使用して、料理の写真からデータ作成しました。作ったファイルはglbでエクスポート。
ビルと同じようにまずglbファイルをアップロード・・・・アップロードできねぇorz
確認したら忍者ホームページのアップロード出来るファイルサイズが上限10MBでした。
生成されたファイルは約50MB。ファイルサイズが大き過ぎた。
再度3DZepherで作成。オプションで低ポリゴンを指定して作り直してみます。
Oh・・・これはかなり低ポリゴン
ファイルサイズは2MBになりました。まぁ今回は小さくしか見えないと思うのでヨシ!
アップロードして、同じようにGLTFLoadeで読込ます。
いやでかいでかい。隕石落したみたいじゃん
デフォルトのサイズやXYZ軸はモデリングするソフト次第なので、そのあたり気にしないとですね。
今回はモデルのサイズがでか過ぎるのと、XYZ座標が異なるようです。
読み込み時にサイズを指定するようにscale.setで調整(思いっきり縮小)
object.scale.set(arrPcList[i].scale,arrPcList[i].scale,arrPcList[i].scale);
マウスカーソルでオブジェクトを指定する
さて、3Dで表示しただけでは、表示されているものが何者なのかが判りません。
マウスカーソルを重ねたら、詳細情報が表示されるとかやりたいですね。
マウスとオブジェクトの衝突処理を調べてみると、どうやらレイキャストのインターセクトオブジェクトというのを使うと、カメラとマウスを結んだ線に重なるオブジェクトを確認できるみたいです。
これを使うと、マウスと重なったオブジェクトを手前から配列で取得できるようなので、配列の0番にオブジェクトがあれば(一番手前)その色を変更して、情報を表示するようにします。
raycaster.setFromCamera(mouse,camera);
const intersects = raycaster.intersectObjects(scene.children);
コンソールで確認してみると
取れてるみたいですね。
一番手前が[0]、奥に行くにつれて[1][2]...となるようです。
上記で[0]にオブジェクトが取得できていれば、htmlの情報欄に、商品名と、情報を表示します。
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で作成した箱(商品)にカーソルを当てれば商品情報が表示できるようになりました。
これで試してみたかったことはまぁまぁ実現できました。
\(^o^)/オワタ
データ量がさほど多くないのもありますが、横着なコードの割に動きますね。
得られた知見
・Three.JS すげぇ!
・商品を座標指定で置いたけど、棚や机に置けるデータ構成にしないと位置合わせで死ねる。
・mouseClickの操作を入れようとしたけど発火しなかった。オービットコントロールに持っていかれてるかも。
(mouseMoveは上記の通り呼べた)
・Babylon.JS てのもいいらしい。(あとでやる)
最後に
以上、一般のPCやスマホのブラウザで動くWebGL関係でそれっぽいWebページを作ってみました。ソースはかなりアレですが見た目はそこそこになったのではないかなと思います。
今回はイメージしやすい 在庫量 を対象にしましたが、これだと商品が山積みか在庫切れしそうかくらいしか解らないので、倉庫でやるなら 「倉庫 効率化」 とかで検索して出てくるような項目を表示したほうが、現場のユーザーさんへの訴求力が上がると思います。
また、最近はGISデータも手軽に使えるので下記のようなページも作れますし、倉庫に拘らず色んな用途に応用できるのではないでしょうか。
名古屋駅周辺GISデータを3DモデルにしてWebページで表示したデモ
Webブラウザはどんどん進化していますが、業務アプリはGridView形式でデータの一覧表示を行っているのが多数派かと思います。
一覧表で表示されても解りにくいようなデータや画面があった場合、足元からVRのようなシステムを浸食させて、現場のユーザーさんの業務を楽しくさせてみたら素敵ではないでしょうか。