Home Manual Reference Source

WebGLの3D描写のデモ

以下で実際を確認できます。

sample2.mjs

// 外部UIライブラリやS3エンジンの読み込み
import GuiBlocks from "./libs/GuiBlocks.min.js";
import InputDetect from "./libs/InputDetect.min.js";
import S3 from "./libs/S3.min.js";

/**
 * WebGLによる3D描画テスト用クラス
 * S3エンジンとGUI部品を組み合わせて動作するデモアプリです。
 */
class S3DGLTest {

    constructor() {
        // S3のWebGLシステム(レンダリング管理)を初期化
        this.s3 = new S3.GLSystem();

        // カメラ操作用コントローラーを初期化
        this.controller = new S3.CameraController();

        // カメラ、メッシュ、モデルの参照を初期化
        this.camera = null;
        this.mesh = null;
        this.model = null;
    }

    /**
     * WebGL描画用Canvasの初期化処理
     * @param {HTMLCanvasElement} canvas - 描画対象のCanvas要素
     */
    initCanvas(canvas) {
        const s3 = this.s3;
        // 背景色を黒に設定
        s3.setBackgroundColor(new S3.Vector(0, 0, 0));
        // WebGLコンテキストをCanvasにセット
        s3.setCanvas(canvas);
        // カメラ操作用のCanvasも登録
        this.controller.setCanvas(canvas);

        // 3Dカメラを生成
        this.camera = s3.createCamera();

        // シェーダープログラム(GLSL)を作成・セット
        const program = s3.createProgram();
        program.setFragmentShader("./libs/S3GL.frag"); // フラグメントシェーダ
        program.setVertexShader("./libs/S3GL.vert");   // バーテックスシェーダ
        s3.setProgram(program);

        // 描画方式などの初期パラメータ
        s3.setSystemMode(S3.SYSTEM_MODE.OPEN_GL);       // OpenGL方式
        s3.setFrontMode(S3.FRONT_FACE.CLOCKWISE);       // 面の表裏判定を時計回りに

        // カメラの視点と注視点を設定
        this.camera.setEye(new S3.Vector(20, 30, 50));  // カメラの位置
        this.camera.setCenter(new S3.Vector(0, 0, 0));  // 注視点(原点)

        // コントローラーにカメラをセット(ユーザー操作反映用)
        this.controller.setCamera(this.camera);
    }

    /**
     * 現在のモデル・メッシュをクリア(削除)する
     * 破棄・参照切り離しでリソースリークを防ぐ
     */
    clearModel() {
        if (this.model) {
            this.model = null;
        }
        if (this.mesh) {
            this.mesh.dispose(); // GLリソース解放
            this.mesh = null;
        }
    }

    /**
     * モデル(3D形状データ)を読み込んでセットする
     * @param {string} url - MQOファイルなどのリソースパス
     */
    setModel(url) {
        const s3 = this.s3;
        // 新しい3Dモデルオブジェクト生成
        const newmodel = s3.createModel();
        // メッシュローダーでファイルからメッシュデータ生成
        const newmesh = S3.MeshLoader.inputData(s3, url);
        // モデルにメッシュを登録
        newmodel.setMesh(newmesh);

        // 既存のモデルをクリア
        this.clearModel();

        // 読み込んだモデル・メッシュを保持
        this.model = newmodel;
        this.mesh = newmesh;
    }

    /**
     * シーンの描画処理
     * カメラ、ライト、モデルをまとめてS3エンジンでレンダリング
     */
    draw() {
        const s3 = this.s3;
        // シーン(Scene)を作成
        const scene = s3.createScene();

        // コントローラーからカメラ取得し、シーンに登録
        scene.setCamera(this.controller.getCamera());

        // モデルがセットされていればシーンに追加し、動きをつける
        if (this.model !== null) {
            scene.addModel(this.model);
            this.model.setScale(5);      // モデルを拡大
            this.model.addRotateY(3);    // モデルをY軸周りに回転
        }

        // 環境に3つのライト(光源)を追加
        // 1. 上方向からの平行光源
        const light_down = s3.createLight();
        light_down.setMode(S3.LIGHT_MODE.DIRECTIONAL_LIGHT);
        light_down.setColor(new S3.Vector(0.6, 0.6, 1.0));
        light_down.setDirection(new S3.Vector(0, -1, 0));
        scene.addLight(light_down);

        // 2. 環境光
        const light_ambient = s3.createLight();
        light_ambient.setMode(S3.LIGHT_MODE.AMBIENT_LIGHT);
        light_ambient.setColor(new S3.Vector(0.0, 0.1, 0.05));
        scene.addLight(light_ambient);

        // 3. ポイントライト(位置指定の点光源)
        const light_point = s3.createLight();
        light_point.setMode(S3.LIGHT_MODE.POINT_LIGHT);
        light_point.setColor(new S3.Vector(0.9, 0.9, 1.0));
        light_point.setPosition(new S3.Vector(100, 0, 0));
        light_point.setRange(200); // 有効範囲
        scene.addLight(light_point);

        // バッファ等の初期化
        s3.clear();

        // シーンを描画
        s3.drawScene(scene);
    }

}

// このクラスのインスタンスを生成しグローバル参照
const gl = new S3DGLTest();

/**
 * WebGL描画パネル(Canvasと連携したUI部品)の生成
 * - 画面上に1280x720のWebGLキャンバスを配置
 * - テスト用の3Dモデル(teapot.mqo)を初期表示
 * - 50msごとに描画を更新
 */
const createWebGLPanel2 = function() {
    const panel = new GuiBlocks.Canvas();
    panel.putMe("webglpanel", GuiBlocks.PUT_TYPE.IN);
    panel.setUnit(GuiBlocks.UNIT_TYPE.PX);
    panel.setPixelSize(1280, 720);

    const canvas = panel.getCanvas();

    gl.initCanvas(canvas); // WebGLセットアップ
    gl.setModel("./resource/teapot.mqo"); // ティーポット3Dモデル読み込み

    const redraw = function() {
        gl.draw(); // 画面更新
    };

    // 描画ループ(50ms間隔)
    setInterval(redraw, 50);
    // ※setTimeoutによる単発描画も可能
};

/**
 * 操作用パネルの生成
 * - ファイル選択用のコンボボックス(mqoモデルのリスト)
 * - 「load」ボタンでモデルの切り替えが可能
 */
const createOperationPanel = function() {
    const filepanel = new GuiBlocks.Panel("ファイル");
    filepanel.putMe("operationpanel", GuiBlocks.PUT_TYPE.IN);

    const filebox = new GuiBlocks.ComboBox([
        "./resource/teapot.mqo",
        "./resource/bumptest.mqo"
    ]);
    filebox.putMe(filepanel, GuiBlocks.PUT_TYPE.IN);

    const loadbutton = new GuiBlocks.Button("load");
    loadbutton.putMe(filebox, GuiBlocks.PUT_TYPE.NEWLINE);
    loadbutton.addListener(function () {
        const filename = filebox.getSelectedItem();
        console.log(filename);
        gl.setModel(filename); // 新しいモデルに切り替え
    });
};

/**
 * メイン処理
 * - コンソール出力
 * - スクロール抑止
 * - WebGLパネルと操作パネルのセットアップ
 */
const main = function() {
    console.log("S3DGL クラスのサンプル");

    // スマホ等でのスクロールを抑制
    InputDetect.noScroll();

    // WebGL描画パネル生成
    createWebGLPanel2();
    // 操作パネル生成
    createOperationPanel();
};

// アプリ起動
main();

S3GL.vert

// ========================================================================
// バーテックス(頂点)シェーダ
// このシェーダは、3Dオブジェクトの各頂点ごとに呼び出され、
// 頂点位置の座標変換や、法線・テクスチャ座標などの各種属性を
// フラグメントシェーダ(ピクセルシェーダ)に渡します。
// ========================================================================

// ---------- 頂点属性(JavaScriptからバインドされる) -------------------

// 頂点法線ベクトル(照明計算や法線マップで使う)
attribute vec3 vertexNormal;

// 接線・従法線(タンジェント/バイノーマル)
// 法線マッピング(ノーマルマップ)で必要
attribute vec3 vertexBinormal;
attribute vec3 vertexTangent;

// 頂点の3D空間位置(ワールド空間での頂点座標)
attribute vec3 vertexPosition;

// テクスチャのUV座標(2次元、0.0~1.0など)
attribute vec2 vertexTextureCoord;

// マテリアル(材質)のインデックス番号(float型で持つ)
// 面ごとに異なるマテリアル情報を持つ場合に利用
attribute float vertexMaterialFloat;

// ---------- 行列(uniform: 全頂点で共通の定数値) ----------------------

// モデル座標→カメラ→射影空間への変換行列(最終的にgl_Positionに使う)
uniform mat4 matrixLocalToPerspective4;

// モデル座標→ワールド空間への変換行列
uniform mat4 matrixLocalToWorld4;

// ---------- シェーダ間の受け渡し(varying: 頂点→フラグメント) --------

// マテリアル番号
varying float interpolationMaterialFloat;

// 法線・接線・従法線(各頂点のものをフラグメントに渡す)
varying vec3 interpolationNormal;
varying vec3 interpolationBinormal;
varying vec3 interpolationTangent;

// ワールド空間での頂点位置(ピクセルシェーダのライティングなどに使う)
varying vec3 interpolationPosition;

// テクスチャ座標(フラグメントシェーダでピクセルごとのUV計算に使う)
varying vec2 interpolationTextureCoord;

// ---------- メイン関数 ----------------------------------------------

void main(void) {
    // マテリアル番号をそのまま渡す(int化はfsで行う)
    interpolationMaterialFloat = vertexMaterialFloat;

    // 各種ベクトルもフラグメントシェーダへ伝達
    interpolationNormal   = vertexNormal;
    interpolationBinormal = vertexBinormal;
    interpolationTangent  = vertexTangent;

    // ワールド空間の頂点位置を計算して渡す(照明/反射などで使う)
    interpolationPosition = (matrixLocalToWorld4 * vec4(vertexPosition, 1.0)).xyz;

    // UV座標
    interpolationTextureCoord = vertexTextureCoord;

    // 頂点を最終的にどこに描画するか計算(射影空間座標)
    gl_Position = matrixLocalToPerspective4 * vec4(vertexPosition, 1.0);

    // ※このgl_Positionの値が、画面上の頂点の位置になる
}

S3GL.frag

// ========================================================================
// フラグメント(ピクセル)シェーダ
// このシェーダは、各ピクセル(画面上の1点)ごとに実行されます。
// ライティング、テクスチャ、マテリアル、ノーマルマップなどの情報を使って
// 最終的な色(gl_FragColor)を決定します。
// ========================================================================

// ---------- 精度の指定(必須:ESのGLSL) ------------------------------
precision mediump float;

// ---------- 材質(マテリアル)関連のuniform変数 -------------------------
// JavaScript側から各マテリアルごとにセットされる(最大4つまで)

#define MATERIALS_MAX 4

uniform vec4 materialsColorAndDiffuse[MATERIALS_MAX];
//  [R, G, B, 拡散率](カラー+ディフューズ)

uniform vec4 materialsSpecularAndPower[MATERIALS_MAX];
//  [R, G, B, 光沢度](スペキュラ+パワー)

uniform vec3 materialsEmission[MATERIALS_MAX];
//  発光色(エミッシブ)

uniform vec4 materialsAmbientAndReflect[MATERIALS_MAX];
//  [R, G, B, 反射率](アンビエント+リフレクト)

uniform vec2 materialsTextureExist[MATERIALS_MAX];
//  [カラーテクスチャの有無, ノーマルテクスチャの有無](1:あり, 0:なし)

uniform sampler2D materialsTextureColor[MATERIALS_MAX];
//  カラーテクスチャ(画像)

uniform sampler2D materialsTextureNormal[MATERIALS_MAX];
//  ノーマルマップ(法線マップ用画像)

// ---------- 変換行列のuniform(主に法線変換で使う) ----------------------
uniform mat4 matrixWorldToLocal4; // ワールド→ローカル変換(逆行列など)
uniform mat3 matrixLocalToWorld3; // ローカル→ワールド変換(回転のみ3x3行列)

// ---------- ライト(光源)情報のuniform ---------------------------------

#define LIGHTS_MAX 4
#define LIGHT_MODE_NONE        0
#define LIGHT_MODE_AMBIENT     1
#define LIGHT_MODE_DIRECTIONAL 2
#define LIGHT_MODE_POINT       3

uniform int lightsLength;                 // 実際に使う光源の数
uniform vec4 lightsData1[LIGHTS_MAX];     // 光源の種類・レンジ・方向/位置(XY)
uniform vec4 lightsData2[LIGHTS_MAX];     // 方向/位置(Z), 光源色(RGB)

// ---------- カメラ情報(視線ベクトル) ---------------------------------
uniform vec3 eyeWorldDirection;

// ---------- 頂点シェーダから渡された補間値 ------------------------------
varying float interpolationMaterialFloat;    // マテリアル番号(float型)
varying vec3 interpolationNormal;           // 法線ベクトル
varying vec3 interpolationBinormal;         // バイノーマル
varying vec3 interpolationTangent;          // タンジェント
varying vec3 interpolationPosition;         // ワールド空間での頂点位置
varying vec2 interpolationTextureCoord;     // テクスチャUV座標

// ========================================================================
// メイン処理開始
// ========================================================================
void main(void) {

    // ----- 定数定義(色やノーマルの基準値) ----------------------------
    const vec4 ZERO      = vec4(0.0, 0.0, 0.0, 0.0);
    const vec4 ONE       = vec4(1.0, 1.0, 1.0, 1.0);
    const vec4 WHITE     = ONE;
    const vec3 NORMALTOP = vec3(0.5, 0.5, 1.0); // 法線マップのデフォルト

    // ----- 1. 頂点シェーダから受け取った値の初期処理 ------------------

    int   vertexMaterial = int(interpolationMaterialFloat); // マテリアル番号をintに変換
    vec3  vertexNormal   = normalize(interpolationNormal);
    vec3  vertexBinormal = normalize(interpolationBinormal);
    vec3  vertexTangent  = normalize(interpolationTangent);

    // ----- 2. 面ごとのマテリアル情報(配列から抽出) ------------------

    // 各種マテリアル属性値(色、反射率、テクスチャ有無など)を取得
    vec3  materialColor;
    float materialDiffuse;
    vec3  materialSpecular;
    float materialPower;
    vec3  materialEmission;
    vec3  materialAmbient;
    float materialReflect;
    float materialRoughness;
    vec4  materialTextureColor;
    vec3  materialTextureNormal;
    bool  materialIsSetNormal;

    {
        // 材質配列(最大4つ)から該当インデックスを選択
        if (vertexMaterial < 4) {
            // 材質番号ごとに各種パラメータを取り出す
            // 下記のような分岐は、配列に対してインデックスでアクセスしやすくするため
            if(vertexMaterial < 2) {
                if(vertexMaterial == 0) {
                    materialColor         = materialsColorAndDiffuse[0].xyz;
                    materialDiffuse       = materialsColorAndDiffuse[0].z;
                    materialSpecular      = materialsSpecularAndPower[0].xyz;
                    materialPower         = materialsSpecularAndPower[0].w;
                    materialEmission      = materialsEmission[0];
                    materialAmbient       = materialsAmbientAndReflect[0].xyz;
                    materialReflect       = materialsAmbientAndReflect[0].w;
                    materialTextureColor  = materialsTextureExist[0].x > 0.5 ?
                        texture2D(materialsTextureColor[0], interpolationTextureCoord) : WHITE;
                    materialIsSetNormal   = materialsTextureExist[0].y > 0.5;
                    materialTextureNormal = materialIsSetNormal ?
                        texture2D(materialsTextureNormal[0], interpolationTextureCoord).xyz : NORMALTOP;
                }
                else {
                    // 1番のマテリアル
                    materialColor         = materialsColorAndDiffuse[1].xyz;
                    materialDiffuse       = materialsColorAndDiffuse[1].z;
                    materialSpecular      = materialsSpecularAndPower[1].xyz;
                    materialPower         = materialsSpecularAndPower[1].w;
                    materialEmission      = materialsEmission[1];
                    materialAmbient       = materialsAmbientAndReflect[1].xyz;
                    materialReflect       = materialsAmbientAndReflect[1].w;
                    materialTextureColor  = materialsTextureExist[1].x > 0.5 ?
                        texture2D(materialsTextureColor[1], interpolationTextureCoord) : WHITE;
                    materialIsSetNormal   = materialsTextureExist[1].y > 0.5;
                    materialTextureNormal = materialIsSetNormal ?
                        texture2D(materialsTextureNormal[1], interpolationTextureCoord).xyz : NORMALTOP;
                }
            }
            else {
                if(vertexMaterial == 2) {
                    // 2番のマテリアル
                    materialColor         = materialsColorAndDiffuse[2].xyz;
                    materialDiffuse       = materialsColorAndDiffuse[2].z;
                    materialSpecular      = materialsSpecularAndPower[2].xyz;
                    materialPower         = materialsSpecularAndPower[2].w;
                    materialEmission      = materialsEmission[2];
                    materialAmbient       = materialsAmbientAndReflect[2].xyz;
                    materialReflect       = materialsAmbientAndReflect[2].w;
                    materialTextureColor  = materialsTextureExist[2].x > 0.5 ?
                        texture2D(materialsTextureColor[2], interpolationTextureCoord) : WHITE;
                    materialIsSetNormal   = materialsTextureExist[2].y > 0.5;
                    materialTextureNormal = materialIsSetNormal ?
                        texture2D(materialsTextureNormal[2], interpolationTextureCoord).xyz : NORMALTOP;
                }
                else {
                    // 3番のマテリアル
                    materialColor         = materialsColorAndDiffuse[3].xyz;
                    materialDiffuse       = materialsColorAndDiffuse[3].z;
                    materialSpecular      = materialsSpecularAndPower[3].xyz;
                    materialPower         = materialsSpecularAndPower[3].w;
                    materialEmission      = materialsEmission[3];
                    materialAmbient       = materialsAmbientAndReflect[3].xyz;
                    materialReflect       = materialsAmbientAndReflect[3].w;
                    materialTextureColor  = materialsTextureExist[3].x > 0.5 ?
                        texture2D(materialsTextureColor[3], interpolationTextureCoord) : WHITE;
                    materialIsSetNormal   = materialsTextureExist[3].y > 0.5;
                    materialTextureNormal = materialIsSetNormal ?
                        texture2D(materialsTextureNormal[3], interpolationTextureCoord).xyz : NORMALTOP;
                }
            }
        }
        // ラフネス(ざらざら感)は光沢度から計算
        // ラフネス値(0...1)を暫定計算(大きいほどざらざらしている)
        materialRoughness = (100.0 - materialPower) * 0.01;
    }

    // ----- 3. テクスチャ反映・ノーマルマッピング ------------------------
    {
        // カラーテクスチャで色を掛ける
        materialColor *= materialTextureColor.xyz;

        // ノーマルマップ(法線テクスチャ)を使う場合
        if(materialIsSetNormal) {
            // [0,1] -> [-1,1]の範囲へ変換
            materialTextureNormal = (materialTextureNormal * 2.0 - 1.0);
            // 接線空間からワールド空間に変換
            vertexNormal = normalize(
                -materialTextureNormal.x * vertexTangent +
                materialTextureNormal.y * vertexBinormal +
                materialTextureNormal.z * vertexNormal);
        }
    }

    // ----- 4. 反射ベクトルや視線方向の計算 ------------------------------

    // 反射ベクトル
    vec3 vertexReflectVector = reflect(eyeWorldDirection, normalize(matrixLocalToWorld3 * vertexNormal));

    // カメラが向いている方向を取得
    vec3 eyeDirection = normalize(matrixWorldToLocal4 * vec4(eyeWorldDirection, 0.0)).xyz;

    // ----- 5. 出力色の初期化 --------------------------------------------
    vec3 destDiffuse  = materialColor * materialEmission;
    vec3 destSpecular = ZERO.xyz;
    vec3 destAmbient  = materialAmbient * 0.2;

    // 光源全体の平均色(反射表現などに使う)
    vec3 averageLightsColor = ZERO.xyz;

    // ----- 6. ライティング計算 ------------------------------------------
    {
        // 全ライトについて計算
        for(int i = 0; i < LIGHTS_MAX; i++) {
            int   lightMode   = int(lightsData1[i].x);
            float lightRange  = lightsData1[i].y;
            vec3  lightVector = vec3(lightsData1[i].zw, lightsData2[i].x);
            vec3  lightColor  = lightsData2[i].yzw;

            // 平行光源・点光源
            if((lightMode == LIGHT_MODE_DIRECTIONAL) || (lightMode == LIGHT_MODE_POINT)) {
                bool is_direction = lightMode == LIGHT_MODE_DIRECTIONAL;
                // 光源の向きや位置を求める
                // 光源の種類によって、ピクセルと光への方向ベクトルの計算を変える
                // lightsVector は、点光源なら位置を、平行光源なら方向を指す値
                vec3 lightDirection = is_direction ?
                    normalize(matrixWorldToLocal4 * vec4(lightVector, 0.0)).xyz :
                    normalize(matrixWorldToLocal4 * vec4(interpolationPosition - lightVector, 1.0)).xyz;
                float d = is_direction ? -1.0 : length(lightVector - interpolationPosition);

                if(d < lightRange) {
                    // 距離減衰
                    float rate = is_direction ? 1.0 : pow(1.0 - (d / lightRange), 0.5);
                    // 拡散反射(ランバート反射)
                    float diffuse = clamp(((dot(vertexNormal, lightDirection) * 0.9) + 0.1) * materialDiffuse, 0.0, 1.0);
                    destDiffuse  += lightColor * materialColor.xyz * diffuse * rate;
                    // 鏡面反射(ブリン・フォン)
                    vec3  halfLightEye = normalize(lightDirection + eyeDirection);
                    float specular     = pow(clamp(dot(vertexNormal, halfLightEye), 0.0, 1.0), materialPower);
                    destSpecular      += lightColor * materialSpecular.xyz * specular * rate;
                }
            }
            // アンビエントライト(環境光)
            else if(lightMode == LIGHT_MODE_AMBIENT) {
                destDiffuse += lightColor * materialColor.xyz;
                destAmbient += lightColor * materialAmbient.xyz;
            }

            // 光の平均色(反射・環境エフェクト用)
            averageLightsColor += lightColor;

            // 実際に使う光源数分だけループ
            if(i == lightsLength) {
                break;
            }
        }
        // 平均色を正規化
        if(0 < lightsLength) {
            averageLightsColor /= vec3(lightsLength, lightsLength, lightsLength);
        }
    }

    // ----- 7. 反射表現(リフレクションの模様作成)-----------------------
    vec3 destColor = ZERO.xyz;

    {
        // アンビエント光
        destColor += destAmbient;

        // 拡散反射(反射率が高いほど弱くする)
        destColor += clamp(destDiffuse * (1.0 - materialReflect * 0.8), 0.0, 1.0);

        // 鏡面反射
        destColor += destSpecular;

        // 反射のエフェクト(物体に周囲の色が写り込む)
        if(materialReflect > 0.0001) {
            // リフレクト用の簡易ノイズ模様を計算(ラフネスで模様のボケ方を制御)
            float x = vertexReflectVector.y;
            float x1 = mix(-0.1, -0.5, materialRoughness);
            float x2 = mix(0.01,  0.5, materialRoughness);
            float c1 = mix(-0.3,  0.5, materialRoughness);
            float c2 = mix( 0.9,  0.6, materialRoughness);
            float c3 = mix( 0.3,  0.4, materialRoughness);
            float c4 = mix( 1.2,  0.8, materialRoughness);
            float c5 = mix( 0.3,  0.5, materialRoughness);

            x = x < x1 ? mix( c1, c2, (x + 1.0) * (1.0 / (1.0 + x1))) :
                x < 0.0 ? mix( c2, c3, (x - x1) * (1.0 / -x1)) :
                x < x2  ? mix( c3, c4, x * (1.0 / x2)) :
                          mix( c4, c5, (x - x2) * (1.0 / (1.0 - x2))) ;

            // 光沢・ラフネスの影響も反映した色模様
            //  リフレクトが大きいほどはっきり強くうつりこむ
            //  映り込む模様は、周りの光の色に影響する
            vec3 reflectColor = vec3(x, x, x) * materialReflect * averageLightsColor;

            // 映り込む模様の色
            //  ラフネスが大きいほど、物体の色で映り込む
            //  ラフネスが小さいほど、スペキュラの色で映り込む
            reflectColor *= materialRoughness * materialColor + (1.0 - materialRoughness) * materialSpecular;

            destColor += reflectColor;
        }
    }

    // ----- 8. 出力(ピクセル色の最終決定)-------------------------------
    gl_FragColor = vec4(destColor, 1.0);
}