グラフィックスのための基礎知識とWebGL

AIと会話するのに困らない程度のコンピュータグラフィックスについての基礎知識だけ^^;
勉強しようと思います

コンピュータグラフィックスの基本概念と用語 と、three.jsを使わないブラウザ上でのグラフィックス描画

目次
  1. 基礎知識
    1. ベクトルと行列
    2. 三角関数
    3. アフィン変換
    4. 線形と非線形
    5. 補間(インターポレーション)
    6. 光学とシェーディングの原理
    7. ベクトルと行列に関する高度なトピック
    8. フラクタル図形とノイズ関数(特にペリンノイズ)
  2. Webでのグラフィックス開発
    1. はじめに
    2. WebGLでグラフィックスを描画

基礎知識

ベクトルと行列

ベクトル
ベクトルは大きさと方向を持つ量で、物理学においては速度や力として表れます
数学的には、ベクトルは一列または一行の数の並びで表されます

  • ベクトルの定義
    例: 2次元ベクトル(平面上のある方向と大きさを表します)
    $$\mathbf{v} = (x, y)$$
  • ベクトルの加算と減算
    $$\mathbf{v} + \mathbf{w} = (v_x + w_x, v_y + w_y)$$
    $$\mathbf{v} – \mathbf{w} = (v_x – w_x, v_y – w_y)$$
  • スカラー(大きさのみを持ち、方向を持たない量)との乗算
    $$c \cdot \mathbf{v} = (c \cdot v_x, c \cdot v_y)$$
  • ベクトルの長さ(大きさ)
    始点から終点までの距離(長さはピタゴラスの定理を使って計算)
    $$| \mathbf{v} | = \sqrt{v_x^2 + v_y^2}$$
  • ベクトルの正規化
    方向は同じままで長さを1にします
    $$\hat{\mathbf{v}} = \frac{\mathbf{v}}{| \mathbf{v} |}$$

行列
行列は数や記号、数学的な式を長方形の配列に並べたもので、複数のベクトルを一つの単位として扱うことができます
*行列を使用すると、複数のベクトルに関する情報や演算を一つの表現でまとめることができ、複雑なベクトル計算を効率的に行えます

  • 行列の定義
    例: 2×2行列(2つの2次元ベクトルを列や行として含むことができます)
    $$A = \begin{bmatrix} a & b \\ \ c & d \end{bmatrix}$$
  • 行列の計算(加算、乗算)
    $$A + B = \begin{bmatrix} a_{11} + b_{11} & a_{12} + b_{12} \\ \ a_{21} + b_{21} & a_{22} + b_{22} \end{bmatrix}$$
    行列の乗算では、一方の行列の行ベクトルと他方の列ベクトルのドット積を計算します
    *ドット積:対応する値同士を掛け合わせて、その結果をすべて足し合わせる計算
    $$AB = \begin{bmatrix} a_{11}b_{11} + a_{12}b_{21} & a_{11}b_{12} + a_{12} \\ b_{22} \ a_{21}b_{11} + a_{22}b_{21} & a_{21}b_{12} + a_{22}b_{22} \end{bmatrix}$$
  • 単位行列
    対角線上が1で他が0の正方行列
    $$I = \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}$$
  • 逆行列
    元の行列と乗算すると単位行列になる行列
    $$A^{-1}A = I$$

コンピュータグラフィックスでは、ベクトルを使ってオブジェクトの位置や向きを表現し、行列を使ってこれらのベクトルを変換することで、オブジェクトの変形や動きを表現します
これにより、2Dや3D空間内でのオブジェクトの回転、スケール変更、移動などが可能になります
*行列を使ってこれらのベクトルを変換とは
2次元の点(x,y)や3次元の点(x,y,x)の座標に「変換行列」を乗算することで、新しい位置に点を変更できます
行列に加算を行う場合、単に各要素の数値を調整することに相当します

単位行列は変換の「無」を表し、コンピュータグラフィックスにおいて変換を開始する際の基点や変換をリセットするときに役立ちます
逆行列は変換を元に戻す際に使用し、コンピュータグラフィックスでのオブジェクトの変形を逆操作するのに役立ちます

三角関数

  • サイン(Sin)関数
    ある角度における直角三角形の対辺(opposite)と斜辺(hypotenuse)の比率
    $$\sin(\theta) = \frac{\text{opposite}}{\text{hypotenuse}}$$
  • コサイン(Cos)関数
    ある角度における直角三角形の隣辺(adjacent)と斜辺の比率
    $$\cos(\theta) = \frac{\text{adjacent}}{\text{hypotenuse}}$$
  • タンジェント(Tan)関数
    ある角度における直角三角形の対辺と隣辺の比率
    $$\tan(\theta) = \frac{\text{opposite}}{\text{adjacent}} = \frac{\sin(\theta)}{\cos(\theta)}$$
  • 単位円(半径が1の円)上の任意の点の座標をθの値に応じて求める
    *θが0のときは点は(1, 0)に、θがπ/2(90度)のときは点は(0, 1)
    $$(x, y) = (\cos(\theta), \sin(\theta))$$
  • 度数法からラジアンへの変換、またはその逆変換には、π(パイ)を使います
    例えば、180度はπラジアンに相当します
    *度数法は角度を360度の円で表す
    *ラジアンは円の半径に基づく測定法で、1ラジアンは約57.3度

サイン関数やコサイン関数は、周期的な動きや波形をモデル化する際に、経過時間に基づいて利用されます
時間が進むにつれて、サインやコサインの値は-1と1の間を周期的に変化し続けます
これは、振動する弦、回転する物体、波の伝播など、時間に連動した周期的な現象を表現するのに理想的です

回転行列
特定の角度で2次元空間での点の回転や、3次元空間でのオブジェクトの回転はサインとコサインを含む回転行列によって記述されます

  • 2次元空間での点の回転
    $$R_{2D}(\theta) = \begin{bmatrix} \cos(\theta) & -\sin(\theta) \\ \ \sin(\theta) & \cos(\theta) \end{bmatrix}$$
  • 3次元空間でのX軸周りの回転
    $$R_x(\theta) = \begin{bmatrix} 1 & 0 & 0 \\ \ 0 & \cos(\theta) & -\sin(\theta) \\ \ 0 & \sin(\theta) & \cos(\theta) \end{bmatrix}$$
  • 3次元空間でのY軸周りの回転
    $$R_y(\theta) = \begin{bmatrix} \cos(\theta) & 0 & \sin(\theta) \\ \ 0 & 1 & 0 \\ \ -\sin(\theta) & 0 & \cos(\theta) \end{bmatrix}$$
  • 3次元空間でのZ軸周りの回転
    $$R_z(\theta) = \begin{bmatrix} \cos(\theta) & -\sin(\theta) & 0 \\ \ \sin(\theta) & \cos(\theta) & 0 \\ \ 0 & 0 & 1 \end{bmatrix}$$

アフィン変換

アフィン変換は、変換行列を使用して幾何学的なオブジェクトの位置、大きさ、形状、方向を変更するために用いられる手法です
平行移動、スケーリング、回転、せん断などが含まれ、これらを組み合わせることで複雑な図形の変換を実現します
例えば、「CSSの transformプロパティ」は、内部的にアフィン変換を利用しています
HTML要素に対して、平行移動(translate)、スケーリング(scale)、回転(rotate)、せん断(skew)などのアフィン変換を適用することで、視覚的な効果を生み出します
*せん断はある軸に沿って傾ける

2Dアフィン変換の行列(3×3の変換行列を使用)

  • 平行移動(Translation)
    $$T_{2D} = \begin{bmatrix} 1 & 0 & t_x \\ \ 0 & 1 & t_y \\ \ 0 & 0 & 1 \end{bmatrix}$$
  • スケーリング(Scaling)
    $$S_{2D} = \begin{bmatrix} s_x & 0 & 0 \\ \ 0 & s_y & 0 \\ \ 0 & 0 & 1 \end{bmatrix}$$
  • 回転(Rotation)
    $$R_{2D} = \begin{bmatrix} \cos(\theta) & -\sin(\theta) & 0 \\ \ \sin(\theta) & \cos(\theta) & 0 \\ \ 0 & 0 & 1 \end{bmatrix}$$
  • せん断(Shearing)
    $$H_{2D} = \begin{bmatrix} 1 & sh_x & 0 \\ \ sh_y & 1 & 0 \\ \ 0 & 0 & 1 \end{bmatrix}$$

3Dアフィン変換の行列(4×4の変換行列を使用)

  • 平行移動(Translation)
    $$T_{3D} = \begin{bmatrix} 1 & 0 & 0 & t_x \\ \ 0 & 1 & 0 & t_y \\ \ 0 & 0 & 1 & t_z \\ \ 0 & 0 & 0 & 1 \end{bmatrix}$$
  • スケーリング(Scaling)
    $$S_{3D} = \begin{bmatrix} s_x & 0 & 0 & 0 \\ \ 0 & s_y & 0 & 0 \\ \ 0 & 0 & s_z & 0 \\ \ 0 & 0 & 0 & 1 \end{bmatrix}$$
  • X軸周りの回転
    $$R_{3Dx} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ \ 0 & \cos(\theta) & -\sin(\theta) & 0 \\ \ 0 & \sin(\theta) & \cos(\theta) & 0 \\ \ 0 & 0 & 0 & 1 \end{bmatrix}$$
  • Y軸周りの回転
    $$R_{3Dy} = \begin{bmatrix} \cos(\theta) & 0 & \sin(\theta) & 0 \\ \ 0 & 1 & 0 & 0 \\ \ -\sin(\theta) & 0 & \cos(\theta) & 0 \\ \ 0 & 0 & 0 & 1 \end{bmatrix}$$
  • Z軸周りの回転
    $$R_{3Dz} = \begin{bmatrix} \cos(\theta) & -\sin(\theta) & 0 & 0 \\ \ \sin(\theta) & \cos(\theta) & 0 & 0 \\ \ 0 & 0 & 1 & 0 \\ \ 0 & 0 & 0 & 1 \end{bmatrix}$$

シンプルなスケーリング(大きさの変更)や回転などは、ベクトルにスカラーを掛けたり基本的な回転行列を使用したりすることで実現できます
より複雑な変換(例えば、一度にスケーリングと回転と平行移動を行うなど)を行う場合には、アフィン変換を利用すると便利です

線形と非線形

  • 線形関数は計算が単純で、予測しやすい動作をします
    その変数間の関係が一次方程式(直線的関係)で表されるものです
  • 非線形関数はより複雑で、予測が難しく、多様な動作をします
    線形でない関係、つまり変数間の関係が非一次方程式で表されるものです

コンピュータグラフィックスにおける線形と非線形
線形プロセスは計算が単純で予測しやすい一方、非線形プロセスは現実世界の光や色の挙動をより正確にモデル化するために使用されます

  • 色空間とガンマ補正
    人間の目は非線形的に光を感知しますが、コンピュータシステムは、光の強度を線形に処理しますが、最終的な画像は非線形的な色空間(例:sRGB)で表示されます
    このため、線形色空間でレンダリングされた画像には、表示前にガンマ補正(非線形処理)が適用されることがあります
  • シェーディングと光の表現
    リアルタイムグラフィックス(例:ビデオゲーム)では、光の計算にしばしば線形アプローチが使われます
    一方で、映画や高品質なレンダリングでは、より現実的な光の表現のために、非線形な光の伝播モデルを使用することがあります
  • HDR(High Dynamic Range)画像
    HDRでは、非線形的に光の範囲を拡張し、よりリアルな光の表現を目指します
    これにより、実際の環境に近い光の強度とコントラストがシミュレートされます

補間(インターポレーション)

補間は既知のデータポイント間で新しいデータポイントを生成するための一連の手法です

  • 線形補間
    2点間を直線で結び、シンプルで予測しやすい補間を提供します
  • 多項式補間
    複数の点を通る高次の曲線を生成し、より複雑なパターンを捉える能力を持ちます
  • スプライン補間
    複数の点を通る滑らかな曲線を生成し、自然で美しい遷移を実現します
  • ベジェ曲線
    制御点を基に曲線を形成し、直感的で柔軟な曲線の制御を可能にします

*GLSLや他のシェーディング言語において、様々な場面で内部的で自動的に補間が行われます
特に、頂点シェーダーからフラグメントシェーダーへのデータ伝達において補間が顕著になります

光学とシェーディングの原理

物理学に基づいた光の基本的な挙動
反射と屈折、光の減衰と色のブレンド

  • 反射(Reflection)
    光が物体に当たって跳ね返る現象
    入射角$\theta_i$と反射角$\theta_r$は等しくなります
    $$\theta_i = \theta_r$$
  • 屈折(Refraction)
    光が異なる媒質に進入する際に方向を変える現象
    スネルの法則
    $n_1$、$n_2$は媒質の屈折率、$\theta_t$は屈折角
    $$n_1 \sin(\theta_i) = n_2 \sin(\theta_t)$$
  • 光の減衰(Light Attenuation)
    距離が増加するにつれて光の強度が減少する現象
    一般的な減衰モデル
    $I_0$は初期強度、$d$は距離
    $$I = \frac{I_0}{d^2}$$
  • 色のブレンド
    異なる色の光が混ざり合って新しい色を生成するプロセス
    $C_1$$C_2$は色、$\alpha$はブレンド係数
    $$C = \alpha C_1 + (1 – \alpha) C_2$$

コンピュータグラフィックスにおける光学モデル(シェーディングモデル
*シェーディングモデルは、3Dコンピュータグラフィックスにおいて、光と物体表面の相互作用をシミュレートし、どのように見えるかを計算するための数学的モデルです

  • ランバート反射モデル(Lambertian Reflection)
    物体表面が完全に拡散反射体(マットな表面)であると仮定します
    入射光はすべての方向に均等に散乱されるため、視点の位置に関係なく同じ色に見えます
  • フォン反射モデル(Phong Reflection Model)
    拡散反射と鏡面反射の両方を考慮します
    鏡面反射は視点の位置によって変わり、光沢のあるハイライトを生成します
  • ブリン-フォン反射モデル(Blinn-Phong Reflection Model)
    フォンモデルの改良版で、計算効率とリアリズムのバランスが改善されています
    ハーフベクトルを使用して鏡面反射を計算します
  • クック-トランス反射モデル(Cook-Torrance Model)
    物理ベースのレンダリングに用いられる、より複雑でリアリスティックなモデル
    表面のマイクロファセット構造を考慮し、光の散乱と鏡面反射をより正確にシミュレートします

シェーディングモデルを使った高度なシェーディング

「シェーディング」という用語は、特定のピクセルや頂点における色や明るさを計算するプロセスを指します
例えば、Blinn-Phongシェーディングは、Blinn-Phongモデルを基にして、3Dオブジェクトの各ピクセルまたは頂点に色を割り当てる方法です

  • Phong Shading
    物体の表面の各点で光の反射を計算します
    拡散反射、鏡面反射、環境光の3つの成分から成ります。
  • Blinn-Phong Shading
    ブリン・フォン・モデルは、フォンモデルを改良したもので、鏡面反射の計算を簡略化しています
  • 環境マッピング(Environment Mapping)
    物体の周囲の環境を反映して、リアルタイムで反射をシミュレートします
    キューブマップや球面マップを使って、周囲の環境をテクスチャとして保存し、これを反射計算に使用します
  • 陰影処理(Shadowing)
    物体が光を遮ることによって影が生じる現象をシミュレートします
    シャドウマップ、シャドウボリュームなどの技術を用いて、影の存在と形状を計算します

Three.jsなどの高レベルの3Dグラフィックスライブラリは、これらの複雑な計算を抽象化し、開発者に使いやすいインターフェースを提供します。
GLSLを使ってカスタムシェーダーを書く場合、シェーディングを自分で実装する必要があります

ベクトルと行列に関する高度なトピック

  • ドット積
    二つのベクトルの成分をそれぞれ掛け合わせて全ての積を足し合わせる操作
    ベクトルの向きの関係を理解するのに役立ちます
    *例えば、2つのベクトルがどの程度同じ方向を向いているかを計算するのに使われます
    光の反射や陰影の計算にも使われ、シェーディングの計算に不可欠です
    $$\mathbf{a} \cdot \mathbf{b} = a_x b_x + a_y b_y + a_z b_z$$
    ドット積はベクトルの大きさと角度に依存し、二つのベクトル間の角度を使っても計算できます
    $$\mathbf{a} \cdot \mathbf{b} = |\mathbf{a}| |\mathbf{b}| \cos(\theta)$$
  • クロス積
    二つのベクトルに垂直な新しいベクトルを生成する操作
    3D空間において面の法線を計算(光の当たり方や影の形成)や回転軸の計算するのに使われます
    $$\mathbf{a} \times \mathbf{b} = (a_y b_z – a_z b_y, a_z b_x – a_x b_z, a_x b_y – a_y b_x)$$
  • 転置行列
    行列の行と列を入れ替える操作
    行列の操作、特に座標変換において重要です。
    カメラ変換やオブジェクトの変換など、3D空間でのオブジェクトの位置や方向を変更するのに使用されます
    行列$A$の転置は$A^T$で表される
    $$(A^T)^T = A$$
    $$(AB)^T = B^T A^T$$

フラクタル図形とノイズ関数(特にペリンノイズ)

フラクタル図形やペリンノイズは、通常はアルゴリズムやプログラムコードを用いて表現されます。

  • フラクタル図形
    フラクタルは自己相似性の特徴を持ち、どのスケールで見ても同じようなパターンが現れます
    単純な形から始まり、反復的なプロセスによって複雑な形状に発展します
  • ペリンノイズ
    ペリンノイズは、自然界のランダムさを模倣した滑らかなノイズ関数です
    一連のグリッド上のランダムベクトルを生成し、それらの間で補間を行います
    結果として得られるノイズは、自然界の現象(例:雲、山など)の外観に似ています

Webでのグラフィックス開発

はじめに

WebGL」はウェブブラウザでグラフィックスをcanvas要素上にレンダリングするための技術で、「WebGPU」は、WebGLよりも進んだ次世代の高性能グラフィックスAPIで、より複雑なビジュアルと計算を扱います

WebGLでは描画処理には「GLSL」というシェーダー言語が使用され、「GLSL」で書いた頂点シェーダーとフラグメントシェーダーをまとめて「シェーダー」と呼びます
*「GLSL」はOpenGLおよびWebGLのためのシェーダー言語
WebGPUでは「WGSL」という新しいシェーダー言語が使用されます

「three.js」のようなライブラリは、これらの技術を簡単に扱えるようにし、開発プロセスを大幅に簡略化できます

WebGLでグラフィックスを描画

バッファの準備
頂点シェーダーは3D空間内の頂点の位置を計算、
その情報をフラグメントシェーダーに渡す役割を果たし、
バッファの準備は、これらの頂点データをGPUに転送し、シェーダーがアクセスできるようにする

頂点シェーダー (vsSource)
頂点シェーダーは、各頂点に対して一度実行されるプログラムです。このプログラムは、頂点の位置を変換し、他の情報(例えば色やテクスチャ座標)をフラグメントシェーダーに渡す

フラグメントシェーダーは、画面上の各ピクセルに対して実行されるプログラムです。フラグメントシェーダーは、ピクセルの色を決定するために使用される

バッファの準備
バッファは、頂点に関連するデータ(位置、色、テクスチャ座標など)をGPUに渡すためのメカニズムです
これらのデータを含むバッファを作成し、設定します

備考
uModelViewMatrix はシーン内のオブジェクトの位置とカメラの位置を扱い
uProjectionMatrix は3Dシーンを2D画面にどのように投影するかを決定します

バッファの準備が完了した後

  1. シェーダープログラムの使用
    まず、作成したシェーダープログラム(頂点シェーダーとフラグメントシェーダーを組み合わせたもの)をアクティブにしますgl.useProgram
    これにより、WebGLはどのシェーダープログラムを使って描画するかを知ります。
  2. バッファのバインド
    描画に必要なバッファ(頂点位置バッファ、頂点色バッファなど)をバインド
    gl.bindBuffer
    これにより、シェーダープログラムがこれらのバッファからデータを取得できるようになります。
  3. 属性の設定
    頂点シェーダーの属性(attribute)にバッファのデータを関連付ける
    gl.vertexAttribPointer
    gl.enableVertexAttribArray
    シェーダープログラムが各頂点の位置や色などのデータを正しく読み取れるようになる
  4. ユニフォーム変数の設定
    シェーダープログラムのユニフォーム変数(uniform)に値を設定
    gl.uniformMatrix4fv
    モデルビュー行列や投影行列など、シェーダーが必要とするグローバル情報が含まれます
  5. 描画コマンドの発行
    gl.drawArrays
    gl.drawElements
    このコマンドにより、設定されたシェーダープログラムが使用され、バッファのデータが頂点シェーダー、そしてフラグメントシェーダーを通過して最終的なピクセルの色が決定され、画面に描画される
  • 1:WebGLコンテキストの取得
  • 2:シェーダの準備
  • 3:シェーダプログラムの作成
    シェーダのソースコードからシェーダオブジェクトを作成し、それらをリンクしてシェーダプログラムを作成します
  • 4:バッファの準備
    描画するオブジェクトの頂点データ(位置、色など)を保持するバッファを作成します
  • 5:ユニフォーム変数の設定
    必要な属性やユニフォーム(uniform)変数を設定します。
    カメラやオブジェクトの変換行列(モデルビュー行列やプロジェクション行列)を計算し、シェーダに渡します
  • 6 :描画の実行
    描画コマンドを実行します。これにより、頂点シェーダとフラグメントシェーダが各頂点、ピクセルに対して実行され、最終的な画像が生成される
  • 7:アニメーション

2D正方形の描画

コードを見る

<canvas id="webgl-canvas"></canvas>
<!--   行列操作用のライブラリ  -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>  
main();
function main() {
// 1. WebGLコンテキストの取得
  const canvas = document.querySelector("#webgl-canvas");
  const gl = canvas.getContext("webgl",{ alpha: true });
  canvas.width = window.innerWidth * window.devicePixelRatio;
  canvas.height = window.innerHeight * window.devicePixelRatio;
  gl.viewport(0, 0, canvas.width, canvas.height);
  // WebGL が使用可能で動作している場合にのみ続行
  if (gl === null) {
    alert(
      "WebGLに対応していない可能性があります。",
    );
    return;
  }
 
// 2. シェーダの準備
/*
頂点シェーダー
aVertexPositionは、各頂点の位置を示すデータを受け取るためのもの
uModelViewMatrixは、オブジェクトのモデルビュー変換行列を表し、オブジェクトの位置や向きなどを表します。
uProjectionMatrixは、3Dシーンを2Dビューポートに投影するための行列です
*/
const vsSource = `
    attribute vec4 aVertexPosition;
    attribute vec4 aVertexColor;
    uniform mat4 uModelViewMatrix;
    uniform mat4 uProjectionMatrix;
    varying lowp vec4 vColor;
    void main(void) {
      gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
      vColor = aVertexColor;
    }
  `;
//フラグメントシェーダー  
const fsSource = `
    varying lowp vec4 vColor;
    void main(void) {
      gl_FragColor = vColor;
    }
  `;
  
// 3. シェーダプログラムの作成
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);  
  
// 4. バッファの準備
const buffers = initBuffers(gl);
  
/*  
5. 描画の準備
シェーダープログラムを使用するために必要な頂点シェーダーの情報をオブジェクトで管理
後の描画処理で利用できるように整理
*/
const programInfo = {
  program: shaderProgram,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
    vertexColor: gl.getAttribLocation(shaderProgram, "aVertexColor"),
  },
  uniformLocations: {
    projectionMatrix: gl.getUniformLocation(shaderProgram, "uProjectionMatrix"),
    modelViewMatrix: gl.getUniformLocation(shaderProgram, "uModelViewMatrix"),
  },
};  

// 6. 描画の実行
//drawScene(gl, programInfo, buffers);
  
// 7. アニメーション(繰り返しシーンを描画)
let then = 0;
  function render(now) {
    now *= 0.001; // nowパラメータは、requestAnimationFrameから渡され、ページが読み込まれてからのミリ秒単位の時刻です。これを秒単位に変換して使用
    deltaTime = now - then;
    then = now;  
    gl.clearColor(0.0, 0.0, 0.0, 0.0);  // 完全に透明
    gl.clear(gl.COLOR_BUFFER_BIT);
    drawScene(gl, programInfo, buffers, squareRotation);
    squareRotation += deltaTime;
    requestAnimationFrame(render);
  }
requestAnimationFrame(render);  
  
}  
//ここまでmain関数
  
/*
 3. シェーダプログラムの作成
   シェーダープログラムを初期化し、WebGL にデータの描画方法を教えるための関数
*/
function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    alert(
      `シェーダープログラムを初期化できません: ${gl.getProgramInfoLog(
        shaderProgram,
      )}`,
    );
    return null;
  }
  return shaderProgram;
}
  /*
   3. シェーダプログラムの作成用のinitShaderProgramで使用する関数
    指定された種類のシェーダーを作成し、ソースをアップロードしてコンパイルする
  */
function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    alert(
      `シェーダーのコンパイル時にエラーが発生しました: ${gl.getShaderInfoLog(
        shader,
      )}`,
    );
    gl.deleteShader(shader);
    return null;
  }

  return shader;
}

/*
4.バッファの準備用の関数
データをGPUに送信し  
*/
function initBuffers(gl) {
  const positionBuffer = initPositionBuffer(gl);
  const colorBuffer = initColorBuffer(gl);

  return {
   position: positionBuffer, //頂点データ
   color: colorBuffer, //カラーデータ 
 };
}
  //頂点データ
  function initPositionBuffer(gl) {
    const positionBuffer = gl.createBuffer();
     // 正方形の頂点座標の配列を作成する
    const positions = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0];
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); 
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    return positionBuffer;
  }  
  //カラーデータ 
  function initColorBuffer(gl) {
    const colors = [
      1.0,  1.0,  1.0,  1.0,    // 白
      1.0,  0.0,  0.0,  1.0,    // 赤
      0.0,  1.0,  0.0,  1.0,    // 緑
      0.0,  0.0,  1.0,  1.0,    // 青
    ];
    const colorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
    return colorBuffer;
  }
  
/*
6.描画の実行用の関数
WebGLを使用して3Dシーンをレンダリングする
*/   
let squareRotation = 0.0;
let deltaTime = 0;  
function drawScene(gl, programInfo, buffers, squareRotation) {
  gl.clearDepth(1.0); // 全てをクリア
  gl.enable(gl.DEPTH_TEST); // 深度テストを有効化
  gl.depthFunc(gl.LEQUAL); // 奥にあるものは隠れるようにする

  // 描写を行う前にキャンバスをクリアする
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  //プロジェクションマトリックスの設定
  const fieldOfView = (45 * Math.PI) / 180; // ラジアンにする
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const zNear = 0.1;
  const zFar = 100.0;
  //mat4はglMatrixライブラリの一部 行列を作成
  const projectionMatrix = mat4.create();
  // メモ: glMatrixライブラリは常に第一引数として行列を受け取る
  mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);

  //モデルビューマトリックスの設定(オブジェクトの位置)
  const modelViewMatrix = mat4.create();
  mat4.translate(
    modelViewMatrix, // 変換結果の格納先
    modelViewMatrix, // 変換する行列
    [-0.0, 0.0, -6.0],
  ); 
  //アニメーション
  mat4.rotate(
    modelViewMatrix,  // 出力先の行列
    modelViewMatrix,  // 回転する行列
    squareRotation,   // ラジアンでの回転量
    [0, 0, 1],        // 回転軸 (Z軸)
  );

  setPositionAttribute(gl, buffers, programInfo);  
  setColorAttribute(gl, buffers, programInfo);
  
  gl.useProgram(programInfo.program);
  gl.uniformMatrix4fv(
    programInfo.uniformLocations.projectionMatrix,
    false,
    projectionMatrix,
  );
  gl.uniformMatrix4fv(
    programInfo.uniformLocations.modelViewMatrix,
    false,
    modelViewMatrix,
  );
  { //オブジェクトの描画、変数はブロック内でのみ有効にするためブロックで囲っている
    const offset = 0;
    const vertexCount = 4; //四角形なので4
    /*
    描画コマンドを実行
    参考
    gl.drawArraysは、頂点データが重複しない単純な形状に適しています。
    gl.drawElementsは、多くの頂点が複数のプリミティブで共有されるより複雑な形状に適しています。    
    */
    gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount);
  }
}
  
    /*
    drawScene関数内で使用
    位置バッファからシェーダーの頂点位置属性(vertexPosition)にデータをどのように送るかをWebGLに指示するための関数
    */
    function setPositionAttribute(gl, buffers, programInfo) {
      const numComponents = 2; // 反復処理ごとに 2つの値を取り出す
      const type = gl.FLOAT; // バッファ内のデータは 32 ビット浮動小数点数
      const normalize = false; // 正規化なし
      const stride = 0; // 一組の値から次の値まで何バイトで移動するか(0 = 上記の type と numComponents を使用)
      const offset = 0; // バッファー内の何バイト目から開始するか
      gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
      gl.vertexAttribPointer( //バッファのデータ構造を指定
        programInfo.attribLocations.vertexPosition,
        numComponents,
        type,
        normalize,
        stride,
        offset,
      );
      gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);//属性を有効
    }  
    /*
    drawScene関数内で使用
    カラーバッファーから vertexColor属性に色を取り出す方法をWebGLに指示します。
    */ 
    function setColorAttribute(gl, buffers, programInfo) {
      const numComponents = 4;
      const type = gl.FLOAT;
      const normalize = false;
      const stride = 0;
      const offset = 0;
      gl.bindBuffer(gl.ARRAY_BUFFER, buffers.color);
      gl.vertexAttribPointer(
        programInfo.attribLocations.vertexColor,
        numComponents,
        type,
        normalize,
        stride,
        offset,
      );
      gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
    }  

立方体を描画する際に挙げられたポイント

  • 頂点の数は合計で24個
    一つの面を描画するためには最低でも4つで6面
    *立方体を描画するとき、頂点を共有しない方法を選択するとテクスチャマッピングやライティングの計算が容易になりますが、メモリ使用量が多くなります
    共有される頂点を持つ立方体を描画する場合は、頂点の数を8個に減らすことができますが、テクスチャ座標や法線ベクトルを面ごとに異ならせることはできなくなります
  • それぞれの頂点は3次元空間内での位置を示すx、y、zの3つの座標で特定されるので、vertexPosition属性のnumComponentsは3
  • 色の配列: 通常、各頂点には色データが関連付けられます。立方体の各頂点に色を割り当てる場合、24個の頂点それぞれに対して色情報(たとえばRGBA形式ならば4つの値)を提供する配列を作成します
  • インデックスバッファの使用
    各面を構成する頂点を指定するために、インデックスバッファを使用します
    これにより、頂点バッファ内のどの頂点が各三角形を形成するのかをWebGLに指示します
  • drawElementsによる描画: gl.drawElements()を使用して、インデックスバッファを通じて立方体を構成する三角形を描画します

立方体にテクスチャを適応する

  • テクスチャのロード
    • テクスチャの生成: gl.createTexture()を呼び出してテクスチャオブジェクトを生成します
    • テクスチャユニットのアクティブ化: gl.activeTexture()によって使用するテクスチャユニットを指定します
    • テクスチャのバインド: gl.bindTexture()を使用してテクスチャオブジェクトをアクティブなテクスチャユニットにバインド
    • テクスチャパラメータの設定: gl.texParameteri()でテクスチャのラッピングやフィルタリング方法を設定します
    • 画像のアップロード: gl.texImage2D()を使用して画像データをテクスチャとしてGPUにアップロード
  • テクスチャ座標のバッファを準備
    テクスチャ座標に関するデータを含むバッファオブジェクトを作成し、バインド
  • シェーダーでのテクスチャ座標の使用
    頂点シェーダーでテクスチャ座標を受け取り、フラグメントシェーダーに渡す
  • シェーダーにテクスチャユニットを伝える: gl.uniform1i()を使って、フラグメントシェーダー内の対応するuniform変数にテクスチャユニットのインデックスを伝える

備考:キャンバスサイズとピクセル比

  • WebGLでは、キャンバスのサイズ: WebGLでは、canva要素のwidthとheight属性が、WebGLのビューポートのサイズを直接決定します
    現代のデバイスでは、物理ピクセルとCSSピクセルの比率(ピクセル比)が異なる場合があります
    例えば、高解像度のディスプレイでは、ピクセル比が2以上になることがあります。このピクセル比を考慮してキャンバスのサイズを設定することで、高解像度ディスプレイ上でのシャープなレンダリングが可能になります
  • Three.jsでは、renderer.setSize: Three.jsでは、renderer.setSize関数を使用してキャンバスのサイズを設定します(この関数は内部的にキャンバスのサイズとデバイスのピクセル比を考慮して適切なサイズ設定を行います)
    renderer.setPixelRatio: renderer.setPixelRatio(window.devicePixelRatio)を使うことで、デバイスのピクセル比に合わせた高解像度レンダリングが可能になり

備考2:アニメーションの実装について
WebGLにおけるアニメーションは、オブジェクトの物理的な動きと視覚的な効果とを分けて考え、それぞれ適切な場所(JavaScript/頂点シェーダー、フラグメントシェーダー)で処理するのが一般的です

  • オブジェクトの移動・変換アニメーション(JavaScript & 頂点シェーダー)
    行列変換: オブジェクトの位置、回転、スケールなどの変換は、モデルビュー行列や投影行列を用いてJavaScriptで計算されます
    頂点シェーダーの役割: 計算された変換行列は頂点シェーダーに渡され、頂点データに適用されます頂点シェーダーはこれらの変換を各頂点に対して実行し、最終的な位置を計算します
  • 色や視覚効果のアニメーション(フラグメントシェーダー
    フラグメントシェーダーでの計算: 色の変化やテクスチャエフェクトなど、ピクセル単位での詳細な視覚効果はフラグメントシェーダーで計算されます
    これには、時間に基づく色の変化、光の効果、パーティクルエフェクトなどが含まれます

ライティング
WebGLは自動的なライティング機能を持たないので、シェーダーを通じて手動で実装する必要がある

  • 基本的なライティング(指向性光源と環境光)の場合
    法線データの準備
    3Dオブジェクトの各頂点に対応する法線ベクトル(ある面に垂直なベクトル)を定義
  • 光源の方向ベクトルの定義
    光がどの方向から来ているかを示すベクトルを設定
  • 正規行列の計算
    モデルビュー行列から正規行列を計算します
    モデルがアニメーションしている場合でも、正規行列を用いて法線ベクトルを常に再計算することで、ライティングが常に正確になります
    *正規行列の計算は、最初にモデルビュー行列の逆行列を計算し、その逆行列を転置する
  • シェーダーでのライティング計算
    頂点シェーダーまたはフラグメントシェーダー内で、定義された法線、光源の方向、および環境光の影響を考慮して色を計算

*複雑なライティング(点光源やスペキュラーハイライト)の場合、より複雑な計算が必要です
点光源では、光源の位置と減衰(距離による光の弱まり)の情報が必要
スペキュラーハイライトでは、視点の方向、光の反射特性(光沢度)、光源の色などの追加情報が必要

*法線ベクトルは物体の形状と向きの情報を提供しするもので、シェーディングモデルはその情報を使用して物体の見た目を計算します