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が返ってくる。そのようなエラーは無視して、手動でテクスチャを張る。
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={};
と宣言した上で、
//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を設定してやれば、上手く動く。
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などのモダンブラウザが対応しているだけであり、鬱陶しい ダイアログが出てくる。
<input id="inputFile" type="file" name="upfile[]" webkitdirectory>
仕方がないので、DragEventを捕まえてファイルをwebkitGetAsEntryで読み込む。この記事を参考にした。
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週間近くずっと悩み続けていた時に、ささやかな希望を与えてくれたページ達。