LoginSignup
4
3

More than 5 years have passed since last update.

Amazon Sumerain(またはunity)でobjファイルの動的読み込みをしてみる

Last updated at Posted at 2018-11-26

Amazon Sumerianでobjファイルを動的に読み込んで3Dオブジェクトとして表示するのをやってみたので内容をメモしておく。

sumerianObjImporterCapture.PNG

参考にした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);
4
3
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
4
3