デジタル・デザイン・ラボラトリーな日々

アラフィフプログラマーが数学と物理を基礎からやり直す。https://qiita.com/yaju

3Dを基礎から勉強する アニメーションの表示

今年、最初のブログです。
前回の「3Dを基礎から勉強する 複数モデルの表示」から2ヶ月経ってしまいましたが、また再開したいと思います。
jsdo.itにて、han.kuroさんが馬の3Dモデルにてアニメーション表示していたのを見かけました。
プログラムも3Dライブラリーを使わないで自前で表示していたので、参考にするには丁度いいと思いました。
馬の3Dモデルデータだけ拝借して、アニメーション表示させるだけなら大分前に出来てたわけです。
この馬の3Dモデルデータは、Three.jsサンプルにあるデータであることが分かりまして、早速ダウンロードして見てみました。
horse.js」でJSONデータになっていまして、参考にしたjsdo.itの馬の3Dモデルデータは必要部分のみ切り取ったデータになっていたわけです。

この段階でブログを書けばよかったんですが、欲を書いてJSONデータを読み込んで表示させようと思ってやり始めたところで急にやる気が停滞してしまい正月休みでやっと復活したところです。


プログラムの大まかなところは「複数モデルの表示」の応用です。違いはThree.jsサンプルにあるデータを使用したところにあります。
今回は、その部分について書いていきます。

ラジオボタンでモデルの種類を選びます。選択したモデルのJSONデータをロードします。
わざわざ連想配列にしているのは、jsdo.itではファイルをアップロードするとそれぞれにURLを割り当てられるため、
jsdo.itに投稿する際、例えば"horse.js"は"http://jsrun.it/assets/7/7/Y/3/77Y3s"に書き換えます。

// モデルデータロード完了
changeModelData() {
    var dict: { key?: string; } = {};
    dict["horse"] = "horse.js";         // 馬
    dict["flamingo"] = "flamingo.js";   // フラミンゴ
    dict["stork"] = "stork.js";         // コウノトリ
    dict["parrot"] = "parrot.js";       // オウム

    for (var i = 0; i < this.elmModels.length; i++) {
        var elm: HTMLInputElement = <HTMLInputElement>this.elmModels[i];
        if (elm.checked) {
            // モデルデータ読込
            this.index = 0;
            this.frameCount = 0;
            this.JSONLoader(dict[elm.value], (() => this.onJSONLoaded()));
            break;
        }
    }
}

// モデルJSONデータ読み込み
JSONLoader(file, callback: Function) {
    var x = new XMLHttpRequest();

    x.open('GET', file);
    x.onreadystatechange = () => {
        if (x.readyState == 4) {
            this.json = JSON.parse(x.responseText);
            // 読込完了コールバック
            callback();
        }
    }
    x.send();
}


「horse.js」を例にするとアニメーションに必要なのは、"morphTargets"内の"horse_A_001"~"horse_A_015"にある各頂点データとなります。
頂点データは複数ですが面データは1つです。今回は色も付けるようにしたので"morphColors"から色データを取得しています。
色データはRGBの順に入っていますが値は0.0~1.0までの浮動小数点となっていますので、255倍して16進で格納するようにしています。

// モデルデータの生成
createModel(modelObject: any) {
    this.modelData = new ModelData();

    this.modelData.vertices = new Array();

    // 頂点データ
    if (modelObject.morphTargets !== undefined) {
        // アニメーション分
        this.animeLength = modelObject.morphTargets.length;
        for (var j = 0; j < this.animeLength; j++) {
            this.modelData.vertices[j] = new Array();
            for (var i = 0; i < modelObject.morphTargets[j].vertices.length; i++) {
                this.modelData.vertices[j].push(modelObject.morphTargets[j].vertices[i]);
            }
        }
    }
    else {
        // アニメーションなし
        this.animeLength = 1;
        this.modelData.vertices[0] = new Array();
        for (var i = 0; i < modelObject.vertices.length; i++) {
            this.modelData.vertices[0].push(modelObject.vertices[i]);
        }
    }

    // 面データ
    this.modelData.faces = new Array();
    for (var i = 0; i < modelObject.faces.length; i++) {
        this.modelData.faces.push(modelObject.faces[i]);
    }

    // 色データ
    this.hasColor = false;
    if (modelObject.morphColors !== undefined) {
        this.hasColor = true;
        this.modelData.colors = new Array();
        for (var i = 0; i < modelObject.morphColors[0].colors.length; i+=3) {
            var r: number = modelObject.morphColors[0].colors[i + 0];
            var g: number = modelObject.morphColors[0].colors[i + 1];
            var b: number = modelObject.morphColors[0].colors[i + 2];

            this.modelData.colors.push(this.getColorHexString(r,g,b));
        }
    }
}


THREE.jsのモデルデータがどうなっているのかのを説明している日本語サイトって検索しても見当たらないんですよね。
JSON Model format 3 と、THREE.JSONLoader.createModelあたりを見るしかないです。
「horse.js」の"faces"では、区切りが10になっており、bitに変換すると「00001010」です。
0: 0 = triangle (3 indices), 1 = quad (4 indices)
1: 0 = no face material, 1 = face material (1 index)
2: 0 = no face uvs, 1 = face uvs (1 index)
3: 0 = no face vertex uvs, 1 = face vertex uvs (3 indices or 4 indices)
4: 0 = no face normal, 1 = face normal (1 index)
5: 0 = no face vertex normals, 1 = face vertex normals (3 indices or 4 indices)
6: 0 = no face color, 1 = face color (1 index)
7: 0 = no face vertex colors, 1 = face vertex colors (3 indices or 4 indices)

0ビットは「0」なので三角形ポリゴンです。1ビット目と3ビットは「1」なんですが今回は使用しません。
区切りの先頭データはType(10=00001010)となり、例 10,v0,v1,v2,0,fv0,fv1,fv2 として8個単位でデータを区切ります。
今回Faceクラスに色データを追加したのは、Faceクラスを面を奥行き座標で並び替えるためにソート(this.faces.sort)させているからです。
最初ソートしていたことに気が付いてなくて、データの順番通りに色をセットしていたため、オウムとか妙なカラフルで表示されてたんですよね。

// THREE.JSONLoader.createModel の超簡易版
var j = 0;
for (var i = 0; i < this.modelData.faces.length; i += 8) {
    // 先頭データはType(10=00001010) 例 10,v0,v1,v2,0,fv0,fv1,fv2  
    if (this.modelData.faces[i] != 10) {
        continue;
    }
    // 頂点インデックスの取得 3頂点のみ対応
    var v0 = this.modelData.faces[i + 1];
    var v1 = this.modelData.faces[i + 2];
    var v2 = this.modelData.faces[i + 3];

    // 面列に新しい面を追加
    this.faces.push(new Face(this.vertices[v0 + addfaces],
                             this.vertices[v1 + addfaces],
                             this.vertices[v2 + addfaces],
                             this.modelData.colors[j++]));
}


3Dモデルはフラットシェーディングで表示しているので、面ごとに色を付けるようにした際も光源の位置によって差をつけています。
ぱっと見では差が出てないようですがですが、よくよく見れば差がついています。
Colorfulチェックボックスを付けて、オンではカラフルにオフの場合には単色の緑色で表示するようにしました。

// 面の塗りつぶし
if (this.isFill) {
    // 描画色の指定
    if (this.isColorful && this.hasColor) {
        // フルカラー
        var col = this.setHex(parseInt(face.color, 16));
        var hsv = this.RGBtoHSV(col.r, col.g, col.b, false);
        var rgb = this.HSVtoRGB(hsv.h, hsv.s, hsv.v - Math.round((1 - face.nz) * 50));
    }
    else {
        // 単色
        var rgb = this.HSVtoRGB(0.4 * 360, 0.5 * 255, face.nz * 255);
    }
    g.fillStyle = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';     // 面の塗りつぶし
    g.fill();
}



Typescriptで組んだソースリストは、github/Study3Dに公開しました。