ThreeJSでローカルファイルを読み込む

ThreeJSでローカルにある3Dファイルを読み込みたい。もっとも、[そのようなサイト自体はGitHubで探せばいくらでもある](https://github.com/mrdoob/three.js/tree/master/editor)わけなのだが、大量にあるソースコードを読むことも面倒くさい。ということで、方法を自分用メモとしてまとめておく。

##方法1（サーバー側にアップロード）
選択されたファイルをサーバーにアップロードする。これが絶対手っ取り早いし、面倒なことを考えなくても良い（ただしアップロードしたファイルの配置場所や削除までの時間、読み込みまでの時間等の考慮は必要）ので、楽。
とはいえ、サーバー側の通信やストレージに不安がある場合、PHP等が利用できない場合などは、ローカルで処理したい。方法2以降はそのためのメモである。

##方法2（読み込むファイルが1つの場合）
読み込むファイルが1つの場合は、非常に単純である。`FileReader.readAsDataURL()`を使うなり、`URL.createObjectURL()`を使うなりして、URLを生成し、`new THREE.(任意のローダー)().load(myURL);`に渡してやれば良い。
STLファイルやGLBファイルなどはこれでロードできる。厄介なのが、複数ファイルがある場合だ。

##方法3（読み込むファイルが複数ある場合：正攻法）
OBJファイルの例をとって説明する。OBJファイルは、

hoge/
 ├ texture/
 │　├ fuga01.png
 │　├ fuga02.png
 │　├ fuga03.png
 │　└ fuga04.png
 ├ scene.obj
 └ scene.mtl

例えば、このような階層構造になっている。ところで、ThreeJSにはOBJLoaderやMTLLoaderなる便利なものが付属しているので、これを使う。詳しくは、[このページ](https://qiita.com/mczkzk/items/a934a71a6078834ecbed#obj-mtl)なんかに載っている（もちろんこれも、`FileReader.readAsDataURL()`や`URL.createObjectURL()`で生成したURLを渡す）。
読み込んだファイル内には、textureへの相対パスが記載されているので、自動的にロードしようとするが、当然ながらサーバー側にはないので404 Not Foundが返ってくる。そのようなエラーは無視して、手動でテクスチャを張る。

``` main.js
var fuga01=new THREE.TextureLoader().load(URL.createObjectURL(PNGFile));
```

こんな具合で、読み込んで手動で貼れば良い。続きは↓URL参照。

https://stackoverflow.com/questions/39850083/three-js-objloader-texture

##方法4（読み込むファイルが複数ある場合：邪道）
面倒くさいので、three.jsを自分好みに変更してしまおうという話。そもそも、読み込んだファイル群に対しては、一意なURLが`URL.createObjectURL()`で生成できる。実際、GLBファイルなどはこれをそのまま、`new THREE.GLTFLoader().load()`に入れるだけで、読み込めてしまう。GLTFファイル（+ binファイル + テクスチャ類）などの場合、binファイルやテクスチャ類へのURLを個別に指定できれば容易にローカルファイルを読み込み可能なのに、そうではないから困っているのである。

https://teratail.com/questions/175146

......なら、ThreeJS書き換えちゃうか。

とりあえず、先頭の方で、`var urlAlias={};`と宣言した上で、

```three.js
//28147行目
//request.open('GET', url, true);
request.open('GET', url in urlAlias?urlAlias[url]:url, true);

//30346行目
//fetch(url, fetchOptions).then(function (res) {
fetch(url in urlAlias?urlAlias[url]:url, fetchOptions).then(function (res) {
```

これで、後はurlAliasに、例えば以下のようにURLを設定してやれば、上手く動く。

```main.js
const myWEB=location.protocol+"//"+location.host+"/";
for(let i=0; i<fileList.length; i++){
	const tempURL=URL.createObjectURL(fileList[i]);
	urlAlias["blob:"+myWEB+fileList[i].myPath]=tempURL;
}
```

ただし、fileList[i].myPathには、OBJファイルやGLTFファイルからの相対パス（方法3の例で言えば、"texture/fuga01.png"など）が代入済であることが期待されているので、良い感じに整えてあげる。

##おまけ
こんな感じでフォルダごと選択できるようにするのも、いいかもしれないが、ChromeやFirefoxなどのモダンブラウザが対応しているだけであり、~~鬱陶しい~~ ダイアログが出てくる。

```index.html
<input id="inputFile" type="file" name="upfile[]" webkitdirectory>
```

仕方がないので、DragEventを捕まえてファイルをwebkitGetAsEntryで読み込む。[この記事](https://www.cresco.co.jp/blog/entry/16359/)を参考にした。

```main.js
const file_checker=async(entry,path="")=>{
		if(entry.isFile){
			const file=await new Promise((resolve)=>{entry.file(f=>{resolve(f);});});
			file.myPath=path+file.name;fileList.push(file);
		}else if(entry.isDirectory){
			const dReader=entry.createReader();
			const entries=await new Promise((resolve)=>{
				dReader.readEntries(e=>{resolve(e);});
			});
			for(let i=0;i<entries.length;i++){await file_checker(entries[i],path+entry.name+"/");}
		}else{reject("ファイルまたはフォルダではありません。");}
	};
	(async()=>{
		let runList=[];
		for (let i=0;i<e.dataTransfer.items.length;i++){
			runList.push(file_checker(e.dataTransfer.items[i].webkitGetAsEntry()));
		}
		await Promise.all(runList)
		.then(()=>{
			//よしなに。
		})
		.catch((e)=>{alert(e);return;});
	})();
```

※このままでは、fileList[i].myPathは読み込んだ一番上からのパスなので、別途、OBJファイルやGLTFファイルからの相対パスに書き換えてやらないといけない。

##参考文献
1週間近くずっと悩み続けていた時に、ささやかな希望を与えてくれたページ達。

- https://stackoverflow.com/questions/42760952/how-to-create-material-texture-using-file-api-in-three-js
- https://threejs.org/docs/?q=tex#api/en/loaders/Loader
- https://teratail.com/questions/175146
- https://stackoverflow.com/questions/39850083/three-js-objloader-texture