Amazon Sumerianでobjファイルを動的に読み込んで3Dオブジェクトとして表示するのをやってみたので内容をメモしておく。
参考にしたUnityスクリプト
以下のスクリプトを利用してみた(ひとまずUnity Editor上だけで動作確認)。
ObjImporter - Unify Community Wiki
テクスチャの読み込み処理はやっていないが、そこらへんはRuntime OBJ Importer Assetあたりを参照したら良さそう。
obsolete対応
以下のメソッドがobsoleteでエラーになる。
mesh.Optimize();
次のサイトを参考にして以下のように対応した。
Mesh.Optimize() obsolete in Unity 5.5.05b - Unity Answers
# if UNITY_EDITOR
UnityEditor.MeshUtility.Optimize(mesh);
# endif
大きいobjファイルの読み込み
mesh.verticesで設定した値の65536番目より大きい値を参照すると値がおかしくなり3Dオブジェクトの形が崩れる。
このため、次のようなサイトを参照して以下のように対応した。
c# - Issue With setting Mesh.triangles in Unity Script - Game Development Stack Exchange
mesh.verticesの配列数が2 ** 32を超えるようだとメッシュを分割した方がよさそう。
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
Sumerianでの作成手順
- エンティティの作成でHTMLエンティティを追加する。
- HTMLコンポーネントを以下のようにする。
<input id="infile" type="file" value="obj">
- スクリプトコンポーネントを以下のようにしてtextureパラメータに適用したいテクスチャを指定する。
'use strict';
// objファイル読み込みクラス
class ObjImporter {
constructor() {
}
loadFromFile(file, finishFunc) {
let vertices = [];
let normals = [];
let uv = [];
let triangles = [];
let faceData = [];
this.readLine(file, (line) => {
let bstring = line.trim().split(/\s+/);
switch (bstring[0]) {
case 'v':
vertices.push([parseFloat(bstring[1]),
parseFloat(bstring[2]),
parseFloat(bstring[3])]);
break;
case 'vt':
uv.push([parseFloat(bstring[1]),
parseFloat(bstring[2])]);
break;
case 'vn':
normals.push([parseFloat(bstring[1]),
parseFloat(bstring[2]),
parseFloat(bstring[3])]);
break;
case 'f':
let intArray = [];
for (let j = 1; j < bstring.length && bstring[j].length > 0; j++) {
let bbstring = bstring[j].split("/", 3);
faceData.push([parseInt(bbstring[0]),
parseInt(bbstring[1]),
parseInt(bbstring[2])]);
intArray.push(faceData.length - 1);
}
for (let j = 1; j + 2 < bstring.length; j++) {
triangles.push(intArray[0]);
triangles.push(intArray[j]);
triangles.push(intArray[j + 1]);
}
break;
}
},
() => {
let newVerts = [];
let newUVs = [];
let newNormals = [];
for (let i = 0; i < faceData.length; i++) {
vertices[faceData[i][0] - 1].forEach((e) => {
newVerts.push(e);
});
if (faceData[i][1] != NaN) {
uv[faceData[i][1] - 1].forEach((e) => {
newUVs.push(e);
});
}
if (faceData[i][2] != NaN) {
normals[faceData[i][2] - 1].forEach((e) => {
newNormals.push(e);
});
}
}
let attributes = [sumerian.MeshData.POSITION, sumerian.MeshData.NORMAL, sumerian.MeshData.TEXCOORD0];
let attributeMap = sumerian.MeshData.defaultMap(attributes);
let vertexCount = faceData.length;
let indexCount = triangles.length;
let meshData = new sumerian.MeshData(attributeMap, vertexCount, indexCount);
meshData.getAttributeBuffer(sumerian.MeshData.POSITION).set(
newVerts
);
meshData.getAttributeBuffer(sumerian.MeshData.NORMAL).set(
newNormals
);
meshData.getAttributeBuffer(sumerian.MeshData.TEXCOORD0).set(
newUVs
);
meshData.getIndexBuffer().set(triangles);
finishFunc(meshData);
});
}
// テキストファイルから少しずつ読み込んで1行ずつ処理する
readLine(file, callback, finishfunc, chunk_size = 1024) {
let offset = 0;
let text = "";
let fr = new FileReader();
fr.onload = (event) => {
if(typeof fr.result === "string") {
let lines = (text + fr.result).split(/\r\n|\r|\n/);
for (let i=0; i<lines.length;i++)
callback(lines[i] + "\n");
finishfunc();
return true;
}
let view = new Uint8Array(fr.result);
text += String.fromCharCode(...view);
let lines = text.split(/\r\n|\r|\n/);
for (let i = 0; i < lines.length - 1; i++)
callback(lines[i] + "\n");
text = lines[lines.length - 1];
seek();
};
fr.onerror = () => {
callback(null);
};
let seek = () => {
if (offset + chunk_size >= file.size) {
let slice = file.slice(offset);
fr.readAsText(slice);
} else {
let slice = file.slice(offset, offset + chunk_size);
fr.readAsArrayBuffer(slice);
}
offset += chunk_size;
}
seek();
}
}
// プレイモード開始時に呼び出される
function setup(args, ctx) {
let input = document.getElementById("infile");
input.addEventListener('change', (e) => {
let file = e.target.files[0];
let objimporter = new ObjImporter();
objimporter.loadFromFile(file, (meshData) => {
var material = new sumerian.Material(sumerian.ShaderLib.textured);
material.setTexture('DIFFUSE_MAP', args.texture);
var quadEntity = ctx.world.createEntity(meshData, material).addToWorld();
ctx.myentity = quadEntity;
});
});
}
// プレイモード終了時に呼び出される
function cleanup(args, ctx) {
if (ctx.myentity) {
ctx.world.removeEntity(ctx.myentity);
}
}
var parameters = [
{
name : "texture",
key : "texture",
type : "texture",
}
];
おまけ:作成したエンティティにColliderを設定する
上のスクリプトのcreateEntityの前あたりに以下のようなコード追加することできる。
どうもSumerianのColliderやRigidBodyなどの物理演算の実態はCannon.jsなようでCANNON.Materialで摩擦や反発を設定している。
const colliderComponent = new sumerian.ColliderComponent({collider: new sumerian.MeshCollider({meshData: meshData})});
// 摩擦と反発を設定
colliderComponent.material = new CANNON.Material({restitution: 1, friction: 0});
quadEntity.setComponent(colliderComponent);