2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

##方法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なる便利なものが付属しているので、これを使う。詳しくは、このページなんかに載っている(もちろんこれも、FileReader.readAsDataURL()URL.createObjectURL()で生成したURLを渡す)。
読み込んだファイル内には、textureへの相対パスが記載されているので、自動的にロードしようとするが、当然ながらサーバー側にはないので404 Not Foundが返ってくる。そのようなエラーは無視して、手動でテクスチャを張る。

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

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

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

......なら、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で読み込む。この記事を参考にした。

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週間近くずっと悩み続けていた時に、ささやかな希望を与えてくれたページ達。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?