p5.jsのp5.Geometryで書いたオブジェクトをobj形式で保存する

# はじめに
p5.Geometryで作った図形をobjファイルの形式で保存したいと思います。参考にしたのは次のサイトです。
　[メッシュを.objにしてUnityから出す](https://techblog.kayac.com/export-unity-mesh-as-obj)
objファイルの形式とは次のようなものです。
```text
# positions
v -0.50000000 0.00000000 0.00000000
v 0.00000000 -0.50000000 0.00000000
v 0.00000000 0.00000000 -0.50000000
v 0.00000000 0.00000000 0.50000000
v 0.00000000 0.50000000 0.00000000
v 0.50000000 0.00000000 0.00000000

# normals
vn -0.50000000 0.00000000 0.00000000
vn 0.00000000 -0.50000000 0.00000000
vn 0.00000000 0.00000000 -0.50000000
vn 0.00000000 0.00000000 0.50000000
vn 0.00000000 0.50000000 0.00000000
vn 0.50000000 0.00000000 0.00000000

# triangle faces
f 1//1 3//3 2//2
f 1//1 5//5 3//3
f 2//2 4//4 1//1
f 1//1 4//4 5//5
f 2//2 6//6 4//4
f 4//4 6//6 5//5
f 2//2 3//3 6//6
f 3//3 5//5 6//6
```
これを拡張子.objで保存すると、たとえばビューアーなどで内容が確認できます：

![wfwf3r3fee3rerfe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/2a29de97-b45e-fa41-6d4d-ced1e6206344.png)

正八面体ですね。色が無いので灰色になってしまっています。
頂点、法線、面のみのデータなので汎用性が高いです。Blenderに落として好きなようにカスタマイズしたりできます。便利です。
押し出しただけ：
![efwrf33f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/d926616c-18ca-ae15-468e-68fc37bca516.png)

これをp5.jsのp5.Geometryで作ったgeomに適用しようというわけです。

## コード全文
自作ライブラリでABCのメッシュを作ってそれを保存するコードです。
[fisceToyBoxSample:textMesh](https://openprocessing.org/sketch/2450356)
```javascript
// fisceToyBox.
// https://github.com/inaridarkfox4231/fisceToyBox
// code: https://inaridarkfox4231.github.io/fisceToyBox/fisceToyBox.js

// じゃあテキストメッシュのサンプルおいておくか
let font;
let geom;
function preload() {
  font = loadFont('https://inaridarkfox4231.github.io/assets/KosugiMaru-Regular.ttf');
}
function setup() {
  createCanvas(windowWidth,windowHeight, WEBGL);
  // テキストメッシュを取得
  // まずテキストパスのサイクル集合を取得
  const cycles = fisceToyBox.getTextContours({
    font:font, targetText:"ABC", textScale:240
  });
  // これを加工してテッセレーションなどをしやすくする処理
  const result = fisceToyBox.cyclesToCycles(cycles);
  // ここから板を作る。今回は厚さ5で。
  geom = fisceToyBox.createBoardMeshFromCycles({result:result, thick:5});
  // あとはmodelで書くだけ

  const objFile = createObjFile(geom);
  // これでsaveできる。
  // コメントアウトを外せば保存されます。
  //saveTextData(objFile, "myABC", "obj")
}

function draw() {
  background(0);
  fill(255);
  orbitControl();
  lights();
  model(geom);
}

// reference:https://techblog.kayac.com/export-unity-mesh-as-obj
function createObjFile(g){
  const vArray = new Float32Array(g.vertices.length*3);
  const nArray = new Float32Array(g.vertexNormals.length*3);
  for(let k=0; k<g.vertices.length; k++){
    const v = g.vertices[k];
    vArray[3*k] = v.x;
    vArray[3*k+1] = v.y;
    vArray[3*k+2] = v.z;
  }
  for(let k=0; k<g.vertexNormals.length; k++){
    const n = g.vertexNormals[k];
    nArray[3*k] = n.x;
    nArray[3*k+1] = n.y;
    nArray[3*k+2] = n.z;
  }

  let result = "# vertice data\n";
  for(let k=0; k<g.vertices.length; k++){
    result += `v ${vArray[3*k]} ${vArray[3*k+1]} ${vArray[3*k+2]} \n`;
  }
result += "# normal data\n";
  for(let k=0; k<g.vertexNormals.length; k++){
    result += `vn ${nArray[3*k]} ${nArray[3*k+1]} ${nArray[3*k+2]} \n`;
  }
  result += "# faces data\n";
  for(let k=0; k<g.faces.length; k++){
    const f = g.faces[k];
    result += `f ${1+f[0]}//${1+f[0]} ${1+f[1]}//${1+f[1]} ${1+f[2]}//${1+f[2]} \n`;
  }
  return result;
}

// postFixを"obj"にするとobjファイルとして保存される
function saveTextData(data, name, postFix = "txt"){
  const fileName = name.concat(".").concat(postFix);

  // HTMLのリンク要素を生成する。
  const link = document.createElement("a");

  // リンク先にJSON形式の文字列データを置いておく。
  link.href = "data:text/plain," + encodeURIComponent(data);

  // 保存するJSONファイルの名前をリンクに設定する。
  link.download = fileName;

  // ファイルを保存する。
  link.click();

  link.remove();
}
```
getTextContours()はテキストとフォントとサイズからそのテキストのcontoursを取得する関数です。ざっくり。で、cyclesToCycles()はその相互関係などのメタデータを用意するための関数です。さらにテッセレーションしやすいように下処理を施しています。この形式で、createBoardMeshFromCycles()に渡すとそのcontoursのGeometryができます。この場合は板です。厚さは5としています。このデータをもとにp5.Geometry()を構成していますが、まあp5.Geometry()なら何でも大丈夫でしょう。
　p5.Geometryなら何でも大丈夫です。

## テキストを作る
ここでテキストデータを作っています。
```javascript
// reference:https://techblog.kayac.com/export-unity-mesh-as-obj
function createObjFile(g){
  const vArray = new Float32Array(g.vertices.length*3);
  const nArray = new Float32Array(g.vertexNormals.length*3);
  for(let k=0; k<g.vertices.length; k++){
    const v = g.vertices[k];
    vArray[3*k] = v.x;
    vArray[3*k+1] = v.y;
    vArray[3*k+2] = v.z;
  }
  for(let k=0; k<g.vertexNormals.length; k++){
    const n = g.vertexNormals[k];
    nArray[3*k] = n.x;
    nArray[3*k+1] = n.y;
    nArray[3*k+2] = n.z;
  }

  let result = "# vertice data\n";
  for(let k=0; k<g.vertices.length; k++){
    result += `v ${vArray[3*k]} ${vArray[3*k+1]} ${vArray[3*k+2]} \n`;
  }
result += "# normal data\n";
  for(let k=0; k<g.vertexNormals.length; k++){
    result += `vn ${nArray[3*k]} ${nArray[3*k+1]} ${nArray[3*k+2]} \n`;
  }
  result += "# faces data\n";
  for(let k=0; k<g.faces.length; k++){
    const f = g.faces[k];
    result += `f ${1+f[0]}//${1+f[0]} ${1+f[1]}//${1+f[1]} ${1+f[2]}//${1+f[2]} \n`;
  }
  return result;
}
```
glsl内部ではFloat32Arrayでデータが処理されるので、同じ長さのFloat32Arrayを作り、そこに頂点と法線のデータを放り込んでいます。あとはobjファイルの形式にのっとって、
　v v.x v.y v.z
　vn n.x n.y n.z
などとします。改行で切り替えます。#でコメントを書けます。 f については、さっきのサイトにあるように、
　f a//a b//b c//c
などとしますが、1ベースなので1を足さないといけません。そこだけ注意です。ほんとはuvとかも設定できたりします。自分で調べてください。

## objファイルとして出力する。
　最初は[saveStrings](https://p5js.org/reference/p5/saveStrings/)を使ってtextで出力してから拡張子を書き換えようと思っていたんですが、改行の仕様が意味不明で失敗したのでやめました。代わりに自作メソッドを使っています。
```javascript
// postFixを"obj"にするとobjファイルとして保存される
function saveTextData(data, name, postFix = "txt"){
  const fileName = name.concat(".").concat(postFix);

  // HTMLのリンク要素を生成する。
  const link = document.createElement("a");

  // リンク先にJSON形式の文字列データを置いておく。
  link.href = "data:text/plain," + encodeURIComponent(data);

  // 保存するJSONファイルの名前をリンクに設定する。
  link.download = fileName;

  // ファイルを保存する。
  link.click();

  link.remove();
}
```
こちらを参考にしました。jsonですが、まあ何でもありです。
[JavaScriptでJSONファイルを保存する/開く方法](https://slash-mochi.net/blog/2019/12/18/post-3073/)
第一引数にさっき作ったテキスト、次にファイル名、最後に"obj"を指定します。ここが"txt"のままだとテキストファイルとして出力されます。

## 実行結果
![swfetfef343433.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/a8ab9a03-ad4e-7d63-4247-9fbba85e5a0a.png)
ひっくりかえっているのはp5.jsではy軸を下向きに設定しているからです。面の向きに矛盾は無いので大丈夫です。気になる人は送る前にrotateすればいいです。
これをBlenderに落とせば加工したりできます。
![wrg4efreg4444.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437349/504a12ab-9423-07a8-d8c5-fe52ab12fb9d.png)

# おわりに
　ここまでお読みいただいてありがとうございました。
 
# 追記（saveObj）
　p5.jsにはsaveObjという関数があり、これをp5.Geometryに使うとobjファイルを吐きます。
　[saveObj](https://p5js.org/reference/p5.Geometry/saveObj/)
こっちのやり方との相違点。まずFloat32化はしていないようです。おそらくjsで使われている64bitの値そのままですね。次にコメントは一切なく延々とvやvnについての記述が並びます。この例のようにv,vn,fからなる構成の場合、 f の記述の仕方はこっちと同じです。そのくらいですね。こっちのやり方の場合は#でコメントを付記できます。差別化点はそのくらいです。またsaveObj()はもちろんUVにも対応しています。

# この記事の価値
　そういうわけでこの記事は価値を失ってしまいました。saveObj()を使えばいいわけですから。ただsaveObj()は1.10.0で実装された関数です。実装されていない場合、この記事のようなやり方でやるしかありません。またobjファイルの内容について知っていること。それは無駄にならないはずです。保存の仕方についても、です。

　以上です。

# 蛇足🐍
　コメントくらいしか差別化点が無いので、どうせならと頂点と面の個数を追記できるように修正しました。
```javascript
function createObjFile(g){
  const vArray = new Float32Array(g.vertices.length*3);
  const nArray = new Float32Array(g.vertexNormals.length*3);
  for(let k=0; k<g.vertices.length; k++){
    const v = g.vertices[k];
    vArray[3*k] = v.x;
    vArray[3*k+1] = v.y;
    vArray[3*k+2] = v.z;
  }
  for(let k=0; k<g.vertexNormals.length; k++){
    const n = g.vertexNormals[k];
    nArray[3*k] = n.x;
    nArray[3*k+1] = n.y;
    nArray[3*k+2] = n.z;
  }

  // ここ。
  let result = `# vertice data (${g.vertices.length} vertices)\n`;

  for(let k=0; k<g.vertices.length; k++){
    result += `v ${vArray[3*k]} ${vArray[3*k+1]} ${vArray[3*k+2]} \n`;
  }
result += "# normal data\n";
  for(let k=0; k<g.vertexNormals.length; k++){
    result += `vn ${nArray[3*k]} ${nArray[3*k+1]} ${nArray[3*k+2]} \n`;
  }

  // こっちも
  result += `# faces data (${g.faces.length} faces)\n`;

  for(let k=0; k<g.faces.length; k++){
    const f = g.faces[k];
    result += `f ${1+f[0]}//${1+f[0]} ${1+f[1]}//${1+f[1]} ${1+f[2]}//${1+f[2]} \n`;
  }
  return result;
}
```
カスタマイズは楽しいものです。