はじめに
p5.Geometryで作った図形をobjファイルの形式で保存したいと思います。参考にしたのは次のサイトです。
メッシュを.objにしてUnityから出す
objファイルの形式とは次のようなものです。
# 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で保存すると、たとえばビューアーなどで内容が確認できます:
正八面体ですね。色が無いので灰色になってしまっています。
頂点、法線、面のみのデータなので汎用性が高いです。Blenderに落として好きなようにカスタマイズしたりできます。便利です。
押し出しただけ:
これをp5.jsのp5.Geometryで作ったgeomに適用しようというわけです。
コード全文
自作ライブラリでABCのメッシュを作ってそれを保存するコードです。
fisceToyBoxSample:textMesh
// 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なら何でも大丈夫です。
テキストを作る
ここでテキストデータを作っています。
// 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を使ってtextで出力してから拡張子を書き換えようと思っていたんですが、改行の仕様が意味不明で失敗したのでやめました。代わりに自作メソッドを使っています。
// 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ファイルを保存する/開く方法
第一引数にさっき作ったテキスト、次にファイル名、最後に"obj"を指定します。ここが"txt"のままだとテキストファイルとして出力されます。
実行結果
ひっくりかえっているのはp5.jsではy軸を下向きに設定しているからです。面の向きに矛盾は無いので大丈夫です。気になる人は送る前にrotateすればいいです。
これをBlenderに落とせば加工したりできます。
おわりに
ここまでお読みいただいてありがとうございました。
追記(saveObj)
p5.jsにはsaveObjという関数があり、これをp5.Geometryに使うとobjファイルを吐きます。
saveObj
こっちのやり方との相違点。まずFloat32化はしていないようです。おそらくjsで使われている64bitの値そのままですね。次にコメントは一切なく延々とvやvnについての記述が並びます。この例のようにv,vn,fからなる構成の場合、 f の記述の仕方はこっちと同じです。そのくらいですね。こっちのやり方の場合は#でコメントを付記できます。差別化点はそのくらいです。またsaveObj()はもちろんUVにも対応しています。
この記事の価値
そういうわけでこの記事は価値を失ってしまいました。saveObj()を使えばいいわけですから。ただsaveObj()は1.10.0で実装された関数です。実装されていない場合、この記事のようなやり方でやるしかありません。また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 (${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;
}
カスタマイズは楽しいものです。