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

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

UnityでVisual Basicを使ってみる

これは、Visual Basic Advent Calendar 2015およびUnity Advent Calendar 2015の18日目の記事です。
Visual Basicのの17日目の記事はhilaponさんの「Visual Basic 2015 の新機能(その1)」でした。
Unityの17日目の記事はcra_cellarさんの「EditorWindowのデザインを作成するエディター拡張」でした。

Unityでは3種類のプログラミング言語C#, JavaScript(型付), Boo(Python方言)を使用することが出来ます。しかし、Unity5からはスクリプトリファレンスや新規作成から Boo が無くなりましたので、実質は2種類ということになります。
ここで書くまで勘違いしていたのですが、UnityScriptはJavaScript(型付)のことだったんですね、Booと間違えてました。
Unityの言語使用比率は、C# 約80%、JavaScript 約20%となっているようです。

.NET系開発の2大言語は、C#VisualBasicですよね。だとしたらUnityでもVisual Basicを選択言語としてサポートしてくれてもいいのに…。
ま、コスト的にはサポートする言語を増やすのは難しいでしょうけど。

ということで、無理矢理にでもUnityでVisual Basicを使ってみようというのが、今回のテーマです。

自分が考えた方法は下記の2点となります。どちらにしてもIDEによるデバッグ難しいでしょう。
1. Visaul Basicで作成したクラスライブラリ(dll)を参照して使用する
2. .NET Compiler Platform(Roslyn)を使いVisual BasicからC#に変換して使用する


1. Visaul Basicで作成したクラスライブラリ(dll)を参照して使用する

以前、「初心者がF#をUnityで使ってみた!」の記事を見たことがあったので、この方法ならVisual Basicでも出来ると思っていました。

Visual Basicのプログラム 「unityVBTest.dll」を作成

Public Class UnityVBTest

    Public Function Message() As String
        Return "Hello VB DLL"
    End Function

End Class

UnityのC#スクリプト 参照に「unityVBTest.dll」を追加

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class test : MonoBehaviour
{
    public Text Message;

    // Use this for initialization
    void Start()
    {
        var vbTest = new unityVBTest.UnityVBTest();
        Message.text = vbTest.Message();
    }
}

【実行結果】文字をDLLから取得して表示している。
f:id:Yaju3D:20151218010435j:plain


2. .NET Compiler Platform(Roslyn)を使いVisual BasicからC#に変換して使用する

.NET Compiler Platform(Roslyn)ですが、2年前の「Visual BasicでRoslynを使ってみる」でプログラムを作成した以来です。久しぶりにさわってみるといろいろ変更があったようで、この記事向けに数時間で組むとなると自分の力量では無理と判断しました。
すみませんが、日を改めて別記事とさせて頂きます。

構想としては、Visual Studioにビルドイベントがありますので、Visual BasicでUnityのプログラムを作成し、ビルド完了時にNET Compiler Platform(Roslyn)でVisual BasicからC#に変換します。もちろん、アタッチするのは変換したC#スクリプトとなります。

Visual BasicからC#に変換するプログラムは、gekkaさんの下記サイトを参考にして逆のものを作成してみるつもりです。
Roslynを使用してC#からVBに変換する方法


ではでは、
19日目のVisual Basic Advent Calendar 2015は、hilaponさんです。
19日目のUnity Advent Calendar 2015は、kyubunsさんです。

静岡Developers勉強会の「UnityとBlenderハンズオン第10章」の資料公開

2015/11/21(土) 13:00~17:00に、静岡Developers勉強会 UnityとBlenderハンズオンが行われる島田市地域交流センター歩歩路(ぽぽろ) 第4会議室には4名が集まりました。

今年最後の勉強会となります。
当日は会場準備やらプロジェクターが正常に映らなかったりしてPCを再起動するも、Windows Updateがかかって使用できるまでに10分かかってしまったりと時間がかかってしまい40分遅れで開始となってしまいました。うーむ、勉強会の前に予めPCを一度再起動しておけば良かったかな。

Unityは40分遅れで開始して15:30までとなり、Blenderは残り1:30となってしまいました。全てこなすには時間が足りないってことで、3分クッキング形式でボーン作成完了から開始することにしたんですが、手を振るアニメーションを作成したところで残り10分となってしまったため、Unityにエクスポートして表示させた部分は見せただけで終わってしまいました。もっとも資料作成が最後まで間に合わなくて、その部分は未作成のままでした。

約1年(2月~11月)、なんとか最後までやり通すことが出来ました。資料作成が毎月大変でゲーム作成まで手が回らなかったです。ゲームアイディアだけは最初の段階で発表していたんですがね。まー12月は残っているのでゲーム作成に集中して公開したいと思います。


静岡Developers勉強会の「UnityとBlenderハンズオン第9章」の資料公開

2015/10/24(土) 13:00~17:00に、静岡Developers勉強会 UnityとBlenderハンズオンが行われる島田市民総合施設プラザおおるり 第5会議室には4名が集まりました。
残念なことに毎回参加してくれていた古参メンバーの方が10月中旬から東京に転勤してしまい、少ない人数が更に少なくなってしまいした。次回が最後として、来年の勉強会をやるにしても毎月ではなく3、4ヶ月くらいのペースになるでしょうね。

今回も前回同様にUnityについては学び直しということで、アニメーション機能であるMecanim(メカニム)について行いました。本当はアニメーション以外についても書く予定でいたのですが、Mecanim(メカニム)だけでも結構なページ数になってしまったため、次回に持ち越しとなりました。
次回で最後になるので、残ったのは次で全部説明しないといけませんね。

Blenderについて今回はUnityとの連携する準備として、写真を使って3Dモデルを作れるAutodesk社の「123D CATCH」を使って、Unityで動かそうという企画です。※残念ながら今回はエラーで3Dモデルを作成出来なかったため、公開しているものを使用しました。

もともと、静岡Developers勉強会にて「UnityとBlenderハンズオン」を企画したのは、下記2つサイトを見たからなんですね。
・友人をスマホで撮影して3Dモデル化してUnityで動かしてみた
・【ユニティちゃん】HGベアッガイさんが踊ってみた【3Dキャプチャ】 ‐ ニコニコ動画:GINZA

当初はUnity 3Dを7月以降やる予定だったんですが、思ったより2Dだけでも学ぶことが多く3Dまで手が回らなくなってしまったので、最後くらいはUnity 3Dでキャラクターを動かしてみたいわけです。
ボーンを入れてUnity 3Dで動かすのは次のセッションで、今回は写真を使って3Dモデルをインポートしてゴミを削除するところまでです。

左右対称なら写真から3Dモデルを作れるサイトとして、なぞるだけで3D化するツール「Smoothie-3D」なんてのもあります。

<

【追記】2015/11/01
Blenderでボーンを追加してアニメーションを付けてからFBXにエクスポートして、Unityでインポートしてみました。

f:id:Yaju3D:20151101165717g:plain

静岡Developers勉強会の「UnityとBlenderハンズオン第8章」の資料公開

2015/09/26(土) 13:00~17:00に、静岡Developers勉強会 UnityとBlenderハンズオンが行われる島田市民総合施設プラザおおるり 第2会議室には4名が集まりました。

今回も前回同様にUnityについては学び直しということで、これまでチュートリアルでやってきた中でなんとなく見過ごしてしまった部分や分からないまま進んでしまった部分を見直してみました。
また、フォントについてはゲームを作る上で単色ではなくカラフルにしたい思いがあったので見直して良かったです。

ただ、資料を作る上では2Dの間はUnity 4.6でと思っていたのですが、今回セッションしている際にUnity 5以上を使用している方ではエラーになってしまうところがあり、もはや無理だと悟ったので次回は最新バージョンで望む予定です。


今回は、前回の資料のBlenderのパンダのモデリング修正に2週間かかってしまった上にモチベーションが上がらなく、また会社はトヨタカレンダーなのでシルバーウィークは関係なく出勤(21日は有給休暇とったけど)だっただけに、いつも100ページ近く書いていたのにパワーダウンで70ページ近くとなりました。Unityはまだ経験から分からない部分もなんとかなるんですが、Blenderは経験が乏しいだけに分からない部分は悩むと解決できないで作成するしかない状態です。

前回のUnityでは言葉で説明するだけで終わってしまったので、今回のUnityはハンズオン形式で出来るようにゆったりと行いました。正直、このくらいのほうが分からないところを相談出来たりして、逆に良かったのかも知れません。

 

資料はまだ修正する予定ですが、とりあえず公開しておきます。

静岡Developers勉強会の「UnityとBlenderハンズオン第7章」の資料公開

2015/08/29(土) 13:00~16:30に、静岡Developers勉強会 UnityとBlenderハンズオンが行われる静岡市民文化会館 第4会議室には6名が集まりました。

今回は終わったら、ホテルセンチュリー静岡 17:30~ 2時間4,000円でビアガーデンでした。もとから天気は良くなかったんですが、今までの苦労が報われたのか雨がかろうじて降らずに盛り上がって終わることが出来ました。前回、仲間が作り終えなかったミニゲーム(25までの数字の早押し)が遊べる段階まで完成したので、秒数を競ったりしたんですが、自分の反応の遅さにおもわず歳を感じてしまいました。

connpass.com

今回Unityについては学び直しということで、これまでチュートリアルでやってきた中でなんとなく見過ごしてしまった部分や分からないまま進んでしまった部分を見直してみました。
特に見直して良かったのは、GUI Textのレンダーモードですね。これまでスコアの表示やタイトル表示はCanvasを移動できる「World Space」を使っていたのですが、今回「Screen Space - Camera」を使うのが一番いいことが分かりました。Render CameraにMain Cameraを指定するのがミソだったわけです。他にもカメラサイズの数値をなんとなく指定していたけど根拠をもって指定できるようになった。後はゲームオブジェクトの取得方法ですね、基本的なところなんですがチュートリアルだけでは理解が不足してた部分です。

 

Blenderについては、今回は下記サイトを参考にパンダのモデリングをしました。

日本VTR実験室 初心者のための!作って学ぶBlenderの基礎:②モデリング-日本VTR実験室-映像会社のスマホアプリ開発部隊

ガイド絵を見ながらパンダをモデリングしていくのですが、皆がBlender自体が不慣れな上にモデリングは慣れないと時間がかかるわけです。残り1時間30分くらいではとても完成させることが出来ませんでした。それでもガイド絵をみながらモデリングするという体験はさせることが出来ました。


今回は資料の修正に時間がとれずブログ公開まで2週間経ってしまいました。特に時間がかかったのはパンダのモデリングです。最後まで参考元のようなパンダのようなフォルムには出来なかったです。Blenderは独学でやっているので、ちゃんとした講師の人に1回教わりたいところです。モデリングは慣れるしかないのかな。

静岡Developers勉強会の「UnityとBlenderハンズオン ミニゲーム発表会」

2015/07/25(土) 13:00~16:30に、静岡Developers勉強会 UnityとBlenderハンズオンが行われる静岡市民文化会館 第4会議室には6名が集まりました。

今回はミニゲーム発表会です。今までチュートリアルをハンズオン形式で作成してきましたが、いざ1からゲームを作ろうとすると理解できてなかったところが出てくるはずなので、自分で考えて答えを出さないと本当に作れるようにはならない。

 

自分は事前にそこそこ動くゲームを作成して臨んだわけです。残りを本時間内でほぼ完成させて発表という魂胆だったんですが、思うように進まず未完成のまま16:00になり発表となってしまいました。

今回はミニゲーム発表会とはいえ、固定で発表時間は設けないで「見て見て時間」ってことで好きな時に発表していいですよって形式にしたんですが、事前に作って来られた1人の方以外は結局終盤で発表という展開になってしまった。

今回の作品群は、ゲームウォッチの「タートルブリッジ」のコピー、加速度センサーを使った玉ころがし、パズルゲーム、ターミナルツール、自分が作成した下記のアクションゲームです。参照:懐かしのゲーム「GOKIVADER」(ゴキベーダー)

f:id:Yaju3D:20150729021410p:plain f:id:Yaju3D:20150729021421j:plain

今回、チュートリアル無しでUnityを使ってゲームを作成したわけですが、分かってないことややっていなかったことがあったなとつくづく実感しました。今までハンズオン形式でテトリスアルカノイドパックマンシューティングゲームと4つのチュートリアルをやってきたにも関わらず。
例えば、オープニングとメインのシーンの分け方、設定画面の作り方、ゲーム用フォントでの表示方法、モバイル用のビルドなどなど習ってないんですよね。また、思い通りのGameObjectの取得方法やメカニムやアニメーションといったところは再度復習が必要だと思いました。
次回のセッション資料では、その部分を理解出来るようなものを作る予定です。

あと毎回セッション資料に掲載だけしている、Shizudev名義で子供向けかつ教育向けのゲームアプリを作るってことで、今回のミニゲーム発表会で作って公開するつもりでいたんですが、全然間に合わなくて、構想画像とゲームアイディアだけの発表で終わってしまいました。
まー8月は夏休みがあるので、そこで集中してゲームとして動くところまで作成して、これを皆で面白くして少しずつ完成させていこうという魂胆です。

3Dを基礎から勉強する テクスチャマッピング陰影付け

前回の「3Dを基礎から勉強する テクスチャマッピング」では、テスクチャを貼り付けたことにより陰影が消えてしまいました。
陰影(シェーディング)については、以前に「3Dを基礎から勉強する フラットシェーディング」をやりました。これを応用すれば、陰影はつけれそうです。
今のプログラムはもともと「3Dモデルを表示するJavaアプレットの作成」を参考にTypeScriptに移植したものです。この記事内の「【ステップ4】面の明るさを設定する」でもフラットシェーディングを行っていますので、この考え方で実現させます。

現在、面を塗る際に法線ベクトルのz成分を元に陰影の描画色を定めています。

var rgb = this.HSVtoRGB(0.4 * 360, 0.5 * 255, face.nz * 255);
g.fillStyle = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';     // 面の塗りつぶし
g.fill();

face.nzには1.0~0.0の値が入っており、光源の角度(cos)が「0°」に近くなると値が1.0となり面の明るさが最大値となります。
f:id:Yaju3D:20130525230936j:plainf:id:Yaju3D:20130526212554j:plain

HTML5Canvasでは、ピクセルデータを読み込んで色を演算させることが出来るのですが、今回はもっと簡単な方法を用います。
Canvasに透明度(globalAlpha)のプロパティがあり、その値は0.0(完全に透明)から 1.0(透明度なし)となっています。
ポリゴン面を黒色で塗る時にglobalAlphaプロパティを使ってテクスチャに陰影を付けます。
この際に、face.nzとglobalAlphaプロパティの値は反対関係にありますから、g.globalAlpha = 1 - face.nzとしています。

if (this.isTexture) {
    // テクスチャの描画
    var vertex_list = [pt1.x, pt1.y, pt2.x, pt2.y, pt3.x, pt3.y];
    var uv_list = [face.coords[0].u, face.coords[0].v, face.coords[1].u, face.coords[1].v, face.coords[2].u, face.coords[2].v];
    this.drawTriangle(g, this.texture, vertex_list, uv_list, i);
    g.save();
    g.globalAlpha = 1 - face.nz;
    g.fillStyle = 'Black';
    g.fill();
    g.restore();
}

結果は下記の通りになります。
f:id:Yaju3D:20150712124232j:plain

参考:床井研究室 コンピュータグラフィックス
10.陰影付け、12.テクスチャマッピング


TypeScriptですが、ソースコードを載せておきます。

// Class M22
class M22 {
    _11: number = 1;
    _12: number = 0;
    _21: number = 0;
    _22: number = 1;

    constructor() {
        this._11 = 1;
        this._12 = 0;
        this._21 = 0;
        this._22 = 1;
    }

    // 逆行列
    getInvert() {
        var out = new M22();
        var det = this._11 * this._22 - this._12 * this._21;
        if (det > -0.0001 && det < 0.0001)
            return null;

        out._11 = this._22 / det;
        out._22 = this._11 / det;

        out._12 = -this._12 / det;
        out._21 = -this._21 / det;

        return out;
    }
}

// Class Point
class Point {
    constructor(public x: number, public y: number) { }
}
// Class UV
class UV {
    constructor(public u: number, public v: number) { }
}

// 頂点クラス
class Vertex {

    rx: number = 0;
    ry: number = 0;
    rz: number = 0;
    screenX: number = 0;
    screenY: number = 0;

    constructor(public x: number, public y: number, public z: number) { }
}

// 面クラス
class Face {
    v = [];         // 面を構成する3つの頂点
    z: number = 0;	// 奥行き
    nx: number = 0; // 法線
    ny: number = 0;
    nz: number = 0;
    color: string = "#000000";
    coords = [];

    constructor(v0: Vertex, v1: Vertex, v2: Vertex, c: string, uv0: UV, uv1: UV, uv2: UV) {
        this.v.push(v0);
        this.v.push(v1);
        this.v.push(v2);
        this.color = c;
        this.coords.push(uv0);
        this.coords.push(uv1);
        this.coords.push(uv2);
    }
}

// 平面クラス
class Plane {

    p = [];
    vertices = [];

    constructor(public center: Point, public scale: number, public style: string) { }

    // 頂点のスクリーン座標を更新する
    setScreenPosition(theta, phi) {
        this.p = [];
        for (var i: number = 0; i < this.vertices.length; i++) {
            var v: Vertex = <Vertex>this.vertices[i];

            // 回転後の座標値の算出
            v.rx = v.x * Math.cos(theta) + v.z * Math.sin(theta);
            v.ry = v.x * Math.sin(phi) * Math.sin(theta) + v.y * Math.cos(phi) - v.z * Math.sin(phi) * Math.cos(theta);
            v.rz = - v.x * Math.cos(phi) * Math.sin(theta) + v.y * Math.sin(phi) + v.z * Math.cos(phi) * Math.cos(theta);

            // スクリーン座標の算出
            v.screenX = this.center.x + this.scale * v.rx;
            v.screenY = this.center.y - this.scale * v.ry;

            // 回転後の各頂点の座標を計算
            this.p.push(new Point(v.screenX, v.screenY));
        }
    }

    // 描画処理
    draw(g: CanvasRenderingContext2D, p1: Point, p2: Point) {
        g.beginPath();
        g.lineWidth = 0.5;
        g.moveTo(p1.x, p1.y);
        g.lineTo(p2.x, p2.y);
        g.closePath();
        g.strokeStyle = this.style;
        g.stroke();
    }
}

// 軸クラス
class Axis extends Plane {

    constructor(center: Point, scale: number, style: string, ax: string) {
        super(center, scale, style);

        var vertice = [];
        // 頂点
        switch (ax) {
            case 'x':
                vertice = [[-1, 0, 0], [1, 0, 0], [1, 0, 0], [-1, 0, 0]];
                break;
            case 'y':
                vertice = [[0, -1, 0], [0, 1, 0], [0, 1, 0], [0, -1, 0]];
                break;
            case 'z':
                vertice = [[0, 0, -1], [0, 0, 1], [0, 0, 1], [0, 0, -1]];
                break;
        }

        for (var i: number = 0; i < vertice.length; i++) {
            var v: Vertex = new Vertex(vertice[i][0], vertice[i][1], vertice[i][2]);
            this.vertices.push(v);
        }
    }

    // 軸描画処理
    draw(g: CanvasRenderingContext2D) {
        super.draw(g, this.p[0], this.p[1]);
    }
}

// 立方体軸クラス
class AxisCube extends Plane {

    constructor(center: Point, scale: number, style: string) {
        super(center, scale, style);

        var diff = (f: boolean) => { return f ? 1 : -1; }

        // 立方体の頂点8つを作成する
        //i   x   y   z
        //0   1   1   1
        //1  -1   1   1
        //2  -1  -1   1
        //3   1  -1   1
        //4   1   1  -1
        //5  -1   1  -1 
        //6  -1  -1  -1
        //7   1  -1  -1
        for (var i: number = 0; i < 8; i++) {
            var v: Vertex = new Vertex(diff(i % 4 % 3 == 0), diff(i % 4 < 2), diff(i < 4));
            this.vertices.push(v);
        }
    }

    draw(g: CanvasRenderingContext2D) {
        // 頂点の間を線で結ぶ
        for (var i: number = 0; i < 4; i++) {
            super.draw(g, this.p[i], this.p[i + 4]);
            super.draw(g, this.p[i], this.p[(i + 1) % 4]);
            super.draw(g, this.p[i + 4], this.p[(i + 1) % 4 + 4]);
        }
    }
}

// インターフェイスの定義
class ModelData {
    vertices: number[][];
    faces: number[];
    colors: string[];
    uvs: UV[];
}

// モデルオブジェクトクラス
class ModelObject extends Plane {
    private faces = [];      		// 面(三角形)列を保持する
    private modelData: ModelData;
    private hasColor: Boolean;
    private hasUvs: Boolean;

    animeLength: number;

    isWireFrame: Boolean = true;
    isFill: Boolean = true;
    isTexture: Boolean = true;
    isColorful: Boolean = true;
    isCulling: Boolean = true;

    texture: HTMLImageElement;
    mousePoint: Point;
    faceNo: number;

    constructor(center: Point, scale: number, style: string) {
        super(center, scale, style);
    }

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

        this.modelData.vertices = new Array();

        // 頂点データ
        if (modelObject.morphTargets !== undefined && modelObject.morphTargets.length != 0) {
            // アニメーション分
            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));
            }
        }

        // UVデータ
        this.hasUvs = false;
        if (modelObject.uvs !== undefined) {
            this.hasUvs = true;
            this.modelData.uvs = new Array();
            for (i = 0; i < modelObject.uvs[0].length; i += 2) {
                var u = modelObject.uvs[0][i + 0] % 1.0;
                var v = modelObject.uvs[0][i + 1] % 1.0;
                u = (u < 0) ? 1 + u : u;
                v = (v < 0) ? v * -1 : 1 - v;

                this.modelData.uvs.push(new UV(u, v));
            }
        }

    }

    uv(value) {

    }

    // ビットチェック
    isBitSet(value, position) {
        return value & (1 << position);
    }

    // モデルデータの設定
    setModelData(idx:number) {
        this.vertices = []; 		// 頂点列を初期化
        this.faces = [];			// 面列を初期化
        var minV = new Vertex(10000, 10000, 10000);
        var maxV = new Vertex(-10000, -10000, -10000);
        var models = [];

        // トークンごとの読み込み
        for (var i = 0; i < this.modelData.vertices[idx].length; i += 3) {
            var x = this.modelData.vertices[idx][i + 0];
            var y = this.modelData.vertices[idx][i + 1];
            var z = this.modelData.vertices[idx][i + 2];

            // モデルサイズを更新
            minV.x = Math.min(minV.x, x);
            minV.y = Math.min(minV.y, y);
            minV.z = Math.min(minV.z, z);

            maxV.x = Math.max(maxV.x, x);
            maxV.y = Math.max(maxV.y, y);
            maxV.z = Math.max(maxV.z, z);

            // 頂点列に新しい頂点を追加
            this.vertices.push(new Vertex(x, y, z));
        }

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

            // 面列に新しい面を追加
            var color: string = (this.hasColor) ? this.modelData.colors[j++] : "000000";

            // UV情報
            var uv0: UV = (this.hasUvs) ? this.modelData.uvs[this.modelData.faces[i + 5]] : new UV(0, 0);
            var uv1: UV = (this.hasUvs) ? this.modelData.uvs[this.modelData.faces[i + 6]] : new UV(0, 0);
            var uv2: UV = (this.hasUvs) ? this.modelData.uvs[this.modelData.faces[i + 7]] : new UV(0, 0);

            // 面情報設定
            this.faces.push(new Face(this.vertices[v0], this.vertices[v1], this.vertices[v2], color, uv0, uv1, uv2));
        }

        var modelSize = Math.max(maxV.x - minV.x, maxV.y - minV.y);
        modelSize = Math.max(modelSize, maxV.z - minV.z);

        // モデルの大きさが原点を中心とする1辺が2の立方体に収まるようにする
        for (var i = 0; i < this.vertices.length; i++) {
            var v = this.vertices[i];
            v.x = (v.x - (minV.x + maxV.x) / 2) / modelSize * 2;
            v.y = (v.y - (minV.y + maxV.y) / 2) / modelSize * 2;
            v.z = (v.z - (minV.z + maxV.z) / 2) / modelSize * 2;
        }
    }

    // 頂点のスクリーン座標を更新する
    setScreenPosition(theta: number, phi: number) {
        super.setScreenPosition(theta, phi);

        for (var i: number = 0; i < this.faces.length; i++) {
            var face = this.faces[i];

            // 面の奥行き座標を更新
            face.z = 0.0;
            for (var j: number = 0; j < 3; j++) {
                face.z += face.v[j].rz;
            }

            // 2辺のベクトルを計算
            var v1_v0_x = face.v[1].rx - face.v[0].rx;
            var v1_v0_y = face.v[1].ry - face.v[0].ry;
            var v1_v0_z = face.v[1].rz - face.v[0].rz;
            var v2_v0_x = face.v[2].rx - face.v[0].rx;
            var v2_v0_y = face.v[2].ry - face.v[0].ry;
            var v2_v0_z = face.v[2].rz - face.v[0].rz;

            // 法線ベクトルを外積から求める
            face.nx = v1_v0_y * v2_v0_z - v1_v0_z * v2_v0_y;
            face.ny = v1_v0_z * v2_v0_x - v1_v0_x * v2_v0_z;
            face.nz = v1_v0_x * v2_v0_y - v1_v0_y * v2_v0_x;

            // 法線ベクトルの正規化
            var l: number = Math.sqrt(face.nx * face.nx + face.ny * face.ny + face.nz * face.nz);
            face.nx /= l;
            face.ny /= l;
            face.nz /= l;
        }

        // 面を奥行き座標で並び替える
        this.faces.sort(function (a, b) {
            return a["z"] - b["z"];
        });
    }

    //3点が時計回りかどうかを調べる
    //時計回りなら1,反時計回りで-1、直線で0を返す。
    isFace(p1: Point, p2: Point, p3: Point) {
        var result: number = 0;
        var dx2: number;
        var dy2: number;
        var dx3: number;
        var dy3: number;

        dx2 = p2.x - p1.x;
        dy2 = p2.y - p1.y;
        dx3 = p3.x - p1.x;
        dy3 = p3.y - p1.y;

        if ((dx2 * dy3) > (dx3 * dy2)) result = -1;
        else if ((dx2 * dy3) < (dx3 * dy2)) result = 1;

        return result;
    }

    // モデル描画
    draw(g: CanvasRenderingContext2D) {
        // 三角形描画のための座標値を格納する配列
        var px = [];
        var py = [];

        // 各面の描画
        for (var i: number = 0; i < this.faces.length; i++) {
            var face = this.faces[i];
            var pt1: Point = new Point(face.v[0].screenX, face.v[0].screenY);
            var pt2: Point = new Point(face.v[1].screenX, face.v[1].screenY);
            var pt3: Point = new Point(face.v[2].screenX, face.v[2].screenY);

            // カリング(隠面は描画しない)
            if (this.isCulling) {
                // 裏表を三角形頂点の配置順序で判定(時計回り以外なら描画しない)
                if (this.isFace(pt1, pt2, pt3) <= 0) continue;
                // 裏表を法線ベクトルで判定
                //if (face.nz < 0) continue;
            }

            // 面の輪郭線の描画(三角形)
            g.beginPath();
            g.strokeStyle = 'black';
            g.lineWidth = 1;
            g.moveTo(pt1.x, pt1.y);
            g.lineTo(pt2.x, pt2.y);
            g.lineTo(pt3.x, pt3.y);
            g.closePath();
            if (this.isWireFrame) {
                g.stroke();
            }

            // 面の塗りつぶし
            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();
            }

            if (this.isTexture) {
                // テクスチャの描画
                var vertex_list = [pt1.x, pt1.y, pt2.x, pt2.y, pt3.x, pt3.y];
                var uv_list = [face.coords[0].u, face.coords[0].v, face.coords[1].u, face.coords[1].v, face.coords[2].u, face.coords[2].v];
                this.drawTriangle(g, this.texture, vertex_list, uv_list, i);
                g.save();
                g.globalAlpha = 1 - face.nz;
                g.fillStyle = 'Black';
                g.fill();
                g.restore();
            }

            
        }
    }

    // 三角形テクスチャ描画
    drawTriangle(g, img, vertex_list, uv_list, i) {
        var _Ax = vertex_list[2] - vertex_list[0];
        var _Ay = vertex_list[3] - vertex_list[1];
        var _Bx = vertex_list[4] - vertex_list[0];
        var _By = vertex_list[5] - vertex_list[1];

        var Ax = (uv_list[2] - uv_list[0]) * img.width;
        var Ay = (uv_list[3] - uv_list[1]) * img.height;
        var Bx = (uv_list[4] - uv_list[0]) * img.width;
        var By = (uv_list[5] - uv_list[1]) * img.height;

        //逆行列を求める
        var m = new M22();
        m._11 = Ax;
        m._12 = Ay;
        m._21 = Bx;
        m._22 = By;
        var mi = m.getInvert();
        if (!mi) return;

        //マトリックス変換値を求める
        var a, b, c, d;
        a = mi._11 * _Ax + mi._12 * _Bx;
        c = mi._21 * _Ax + mi._22 * _Bx;

        b = mi._11 * _Ay + mi._12 * _By;
        d = mi._21 * _Ay + mi._22 * _By;

        g.save();
        
        g.beginPath();
        g.moveTo(vertex_list[0], vertex_list[1]);
        g.lineTo(vertex_list[2], vertex_list[3]);
        g.lineTo(vertex_list[4], vertex_list[5]);
        g.clip();	//三角形に切り取る
        if (this.mousePoint != undefined && g.isPointInPath(this.mousePoint.x, this.mousePoint.y)) {
            g.fillStyle = 'rgb(255,0,0)';     // 面の塗りつぶし
            g.fill();
            g.restore();
            this.faceNo = i;
            this.mousePoint != undefined;
            return;
        }
        
        var e = vertex_list[0] - (a * uv_list[0] * img.width + c * uv_list[1] * img.height);
        var f = vertex_list[1] - (b * uv_list[0] * img.width + d * uv_list[1] * img.height);
        g.transform(a, b, c, d, e, f);
        g.drawImage(img, 0, 0);
        g.restore();
    }

    setHex(hex:number) {

        hex = Math.floor(hex);

        var r: number = (hex >> 16 & 255);
        var g: number = (hex >> 8 & 255);
        var b: number = (hex & 255);

        return { 'r': r, 'g': g, 'b': b };
    }

    getColorHex(r:number, g:number, b:number):number {

        return (r * 255) << 16 ^ (g * 255) << 8 ^ (b * 255) << 0;
	}

    getColorHexString(r: number, g: number, b: number):string {

        return ('000000' + this.getColorHex(r, g, b).toString(16)).slice(- 6);
	}

    RGBtoHSV(r: number, g: number, b: number, coneModel:boolean) {
        var h: number,              // 0..360
            s: number, v: number,   // 0..255
            max = Math.max(Math.max(r, g), b),
            min = Math.min(Math.min(r, g), b);

        // hue の計算
        if (max == min) {
            h = 0; // 本来は定義されないが、仮に0を代入
        } else if (max == r) {
            h = 60 * (g - b) / (max - min) + 0;
        } else if (max == g) {
            h = (60 * (b - r) / (max - min)) + 120;
        } else {
            h = (60 * (r - g) / (max - min)) + 240;
        }

        while (h < 0) {
            h += 360;
        }

        // saturation の計算
        if (coneModel) {
            // 円錐モデルの場合
            s = max - min;
        } else {
            s = (max == 0)
            ? 0 // 本来は定義されないが、仮に0を代入
            : (max - min) / max * 255;
        }

        // value の計算
        v = max;

        return { 'h': h, 's': s, 'v': v };
    }

    // HSBからRGB変換
    HSVtoRGB(h: number, s: number, v: number) {
        var r: number, g: number, b: number; // 0..255

        while (h < 0) {
            h += 360;
        }

        h = h % 360;

        // 特別な場合 saturation = 0
        if (s == 0) {
            // → RGB は V に等しい
            v = Math.round(v);
            return { 'r': v, 'g': v, 'b': v };
        }

        s = s / 255;

        var i: number = Math.floor(h / 60) % 6,
            f = (h / 60) - i,
            p = v * (1 - s),
            q = v * (1 - f * s),
            t = v * (1 - (1 - f) * s);

        switch (i) {
            case 0:
                r = v; g = t; b = p; break;
            case 1:
                r = q; g = v; b = p; break;
            case 2:
                r = p; g = v; b = t; break;
            case 3:
                r = p; g = q; b = v; break;
            case 4:
                r = t; g = p; b = v; break;
            case 5:
                r = v; g = p; b = q; break;
        }

        return { 'r': Math.round(r), 'g': Math.round(g), 'b': Math.round(b) };
    }
}

// メインクラス
class Study3DApp {
    private mousePosition: Point;	// マウス位置の初期化
    private phi = 0.30;        		// x軸周りの回転角
    private theta = 0.50;  		    // y軸周りの回転角
    private isDrag: boolean = false;
    private elmWireFrame: HTMLInputElement;
    private elmFill: HTMLInputElement;
    private elmTexture: HTMLInputElement;
    private elmColorful: HTMLInputElement;
    private elmCulling: HTMLInputElement;
    private elmAxis: HTMLInputElement;
    private elmAxisCube: HTMLInputElement;
    private elmSpeed: HTMLInputElement;
    private image: HTMLImageElement;

    private width: number;
    private height: number;
    private center: Point;
    private scale: number;
    private context: CanvasRenderingContext2D;
    private context2: CanvasRenderingContext2D;
    private modelObj: ModelObject;
    private axisCube: AxisCube;
    private axis = [];
    private index = 0;
    private json;
    private frameCount = 0;

    constructor(canvas: HTMLCanvasElement, canvas2: HTMLCanvasElement) {
        this.context = canvas.getContext("2d");
        this.context2 = canvas2.getContext("2d");
        this.width = canvas.width = canvas2.width = 448;    //window.innerWidth;
        this.height = canvas.height = canvas2.height = 448;  //window.innerHeight;

        this.mousePosition = new Point(this.width / 2, this.height / 2);

        this.elmWireFrame = <HTMLInputElement>document.getElementById('wireFrameCheck');
        this.elmFill = <HTMLInputElement>document.getElementById('fillCheck');
        this.elmTexture = <HTMLInputElement>document.getElementById('textureCheck');
        this.elmColorful = <HTMLInputElement>document.getElementById('colorfulCheck');
        this.elmCulling = <HTMLInputElement>document.getElementById('cullingCheck');
        this.elmAxis = <HTMLInputElement>document.getElementById('axisCheck');
        this.elmAxisCube = <HTMLInputElement>document.getElementById('axisCubeCheck');
        this.elmSpeed = <HTMLInputElement>document.getElementById('speed');

        // 中央位置の設定
        this.center = new Point(this.width / 2, this.height / 2);
        // 描画スケールの設定
        this.scale = this.width * 0.6 / 2
        this.modelObj = new ModelObject(this.center, this.scale, 'black');

        canvas.addEventListener("mousemove", (e: MouseEvent) => {
            if (!this.isDrag) return;

            // 回転角の更新
            this.theta += (e.clientX - this.mousePosition.x) * 0.01;
            this.phi += (e.clientY - this.mousePosition.y) * 0.01;

            // x軸周りの回転角に上限を設定
            this.phi = Math.min(this.phi, Math.PI / 2);
            this.phi = Math.max(this.phi, -Math.PI / 2);

            // マウス位置の更新
            this.mousePosition = new Point(e.clientX, e.clientY);
            // 描画
            this.render();
        });

        canvas.addEventListener("mousedown", (e: MouseEvent) => {
            if (e.button == 0) {
                // マウスボタン押下イベント
                this.isDrag = true;
            }
            // マウス位置の更新
            this.mousePosition = new Point(e.offsetX | 0, e.offsetY | 0);
            this.modelObj.mousePoint = this.mousePosition;

        });
        canvas.addEventListener("mouseup", (e: MouseEvent) => {
            // マウスボタン離されたイベント
            this.isDrag = false;
        });

        // 各チェックボックス変更
        this.elmWireFrame.addEventListener("change", () => {
            this.modelObj.isWireFrame = this.elmWireFrame.checked;
            this.render();
        });
        this.elmFill.addEventListener("change", () => {
            this.modelObj.isFill = this.elmFill.checked;
            this.render();
        });
        this.elmTexture.addEventListener("change", () => {
            this.modelObj.isTexture = this.elmTexture.checked;
            this.render();
        });
        this.elmColorful.addEventListener("change", () => {
            this.modelObj.isColorful = this.elmColorful.checked;
            this.render();
        });
        this.elmCulling.addEventListener("change", () => {
            this.modelObj.isCulling = this.elmCulling.checked;
            this.render();
        });
        this.elmAxis.addEventListener("change", () => {
            this.render();
        });
        this.elmAxisCube.addEventListener("change", () => {
            this.render();
        });
        this.elmSpeed.addEventListener("change", () => {
            document.getElementById("speedDisp").innerHTML = this.elmSpeed.value;
        });

        this.axisCube = new AxisCube(this.center, this.scale, 'darkgray');

        // 軸 立方体より少しはみ出すために1.2倍長くする
        this.axis.push(new Axis(this.center, this.scale * 1.2, 'blue', 'x'));
        this.axis.push(new Axis(this.center, this.scale * 1.2, 'green', 'y'));
        this.axis.push(new Axis(this.center, this.scale * 1.2, 'red', 'z'));

        // モデルデータ読込
        this.changeModelData();
    }

    // モデルデータロード完了
    changeModelData() {
        // モデルデータ読込
        this.index = 0;
        this.frameCount = 0;

        //this.JSONLoader("monster.js", (() => this.onJSONLoaded()));
        //this.JSONLoader("robot.js", (() => this.onJSONLoaded()));
        this.JSONLoader("dice41.js", (() => this.onJSONLoaded()));
    }

    // モデルデータロード完了
    onJSONLoaded() {

        // モデルデータの生成
        this.modelObj.createModel(this.json);

        // モデルデータの設定
        this.modelObj.setModelData(this.index);

        this.image = new Image();
        this.image.src = "test.png";
        //this.image.src = "サイコロ2561.png";
        //this.image.src = "monster.jpg";
        //this.image.src = "robot.jpg";
        this.image.onload = (() => this.imageReady(this));
    }

    // イメージ読込完了
    imageReady(that) {
        var ctx = this.context2;
        var w = this.image.width;
        var h = this.image.height;

        // 初期描画および画像データ退避
        ctx.drawImage(this.image, 0, 0, w, h);
        this.modelObj.texture = this.image;

        // 描画
        this.render();
        // タイマー
        setInterval((() => this.onFrame()), 1000 / 30);
    }

    // 情報表示
    drawInfo() {
        var elm = document.getElementById("info");
        elm.innerText = 'theta: ' + this.theta.toFixed(2) + ' / phi: ' + this.phi.toFixed(2) + ' / FaceNo: ' + this.modelObj.faceNo + ' / x,y: ' + this.mousePosition.x + ',' + this.mousePosition.y;
    }

    // 描画クリア
    drawClear(g: CanvasRenderingContext2D) {
        g.beginPath();
        g.fillStyle = 'aliceblue';
        g.fillRect(0, 0, this.width, this.height);
    }

    // 描画
    render() {
        var g: CanvasRenderingContext2D = this.context;

        // 描画クリア
        this.drawClear(g);

        // モデル描画
        this.modelObj.setScreenPosition(this.theta, this.phi);
        this.modelObj.draw(g);

        // 軸立方体描画
        if (this.elmAxisCube.checked) {
            this.axisCube.setScreenPosition(this.theta, this.phi);
            this.axisCube.draw(g);
        }

        if (this.elmAxis.checked) {
            // 軸描画
            for (var i = 0; i < this.axis.length; i++) {
                this.axis[i].setScreenPosition(this.theta, this.phi);
                this.axis[i].draw(g);
            }
        }

        // 情報表示
        this.drawInfo();
    }

    // 毎回フレーム
    onFrame() {
        if ((this.frameCount % (Number(this.elmSpeed.value) * 3)) == 0) {
            this.frameCount = 0;
            // モデルデータの設定
            this.modelObj.setModelData(this.index);
            this.index++;
            this.index %= this.modelObj.animeLength;
        }
        this.render();
        this.frameCount++;
    }

    // モデル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();
    }
}

window.onload = () => {
    var app = new Study3DApp(<HTMLCanvasElement>document.getElementById('content'),
                             <HTMLCanvasElement>document.getElementById('content2'));
};

スポンサーリンク