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

アラフィフプログラマーが数学と物理を基礎からやり直す

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'));
};