Three.js/シェーダー(GLSL)その3

目次
  1. 座標系について
  2. エッジに近い部分の視覚効果
  3. ライト

座標系について

オブジェクトにテクスチャやパターンをどのように適用するか?

今までは、平面にUV座標を利用して描画していました

  • UV座標は、オブジェクトの表面にテクスチャやパターンを「マッピング(適用)」するために使われる2D座標系です
    この方法では、パターンはオブジェクトの表面に直接「貼り付け」られるため、オブジェクトが移動、回転、スケールしても、パターンはオブジェクトの表面に沿って一緒に動きます
    *オブジェクトの「皮」にデザインを描くようなものです
  • モデル座標系(modelPosition)を利用した描画
    モデル座標系では、オブジェクトの各頂点の位置情報を直接使用してシェーディングやパターンを生成します
    • ワールド空間での操作(modelPosition.xyzをvPositionに割り当てること)により、オブジェクトがワールド内での絶対的な位置に基づいて描画されるため、オブジェクトが移動、回転、またはスケールしても、パターンはワールド空間内で静止して見えます
      *カメラや他のオブジェクトの動きに関わらず、パターンの位置は変わりません
    • ローカル空間での操作(position.xyzをvPositionに割り当てること)により、パターンがオブジェクトに固定されるため、オブジェクトの移動、回転、スケールに合わせてパターンも一緒に動きます
      *パターンがオブジェクトに付随して動く

vPositionをフラグメントシェーダーに渡す

//頂点シェーダー
varying vec3 vPosition;

void main(){
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    // ...
  //ワールド空間での操作
    vPosition = modelPosition.xyz;
}

備考:4次元ベクトルとw成分
3Dグラフィックスにおいては、位置や方向を表すために3次元ベクトルが一般的に使われます
しかし、3D空間内での回転やスケーリング、移動(平行移動)などの変換を一つの行列で表現するためには、4次元ベクトル(x, y, z, w)を使用
*wは通常
位置を表すベクトルでは1
方向を表すベクトル(法線ベクトルなど)では0

エッジに近い部分の視覚効果

フレネル効果とは、光が異なる媒体の境界で反射する際に、入射角に応じて反射率が変わる現象です
特に物体の端や角に近づくと反射が強まる特性を持ちます
たとえば、窓ガラスを斜めから見たとき、ガラスの透過性よりも反射性が大きくなります

フレネル効果を数学的にモデル化するには、以下の要素を考慮する必要があります

  • 法線(Normal):物体表面から垂直に伸びるベクトルです
    これは表面の向きを表し、光の反射や屈折を計算する際の基準点
  • ビュー角(View Angle): 視点(例えばカメラや目)からのオブジェクトの表面への方向

フレネル効果においては
光が物体の表面に対してほぼ平行に入射する(ビュー角が大きい)時、反射率は高い
逆に、光が表面にほぼ垂直に入射する(ビュー角が小さい)時、反射率は低い

これを定量化するために、法線ベクトルとビュー方向ベクトルのドット積を計算します
ドット積が1に近い場合、ベクトルはほぼ同じ方向を指しており(ビュー角が小さい)、反射率は低い
ドット積が0に近い場合(ベクトルが直角に近い)、ビュー角が大きく、反射率が高い

エッジに近い部分で特定の視覚効果を出したい場合、フレネル効果に似た効果を利用する
*フレネル効果は、物体の表面と視線がほぼ平行になる部分、つまりエッジ部分で反射率が高まる物理現象です

オブジェクトがワールド空間でどのように回転しても、法線ベクトルはオブジェクトの表面の正しい方向を維持するように

必要なのは法線ベクトルの「向き」情報であり、物体の「位置」情報を変更することは目的としていません
法線ベクトルに変換を適用する際にw成分を0に設定することで、回転やスケールは影響を受けるものの、物体の移動(平行移動)は影響を受けないようにします

 //オブジェクトが変形しても正しい方向の法線ベクトルを送る
 vec4 modelNormal = modelMatrix * vec4(normal, 0.0);
 vNormal = modelNormal.xyz;

視野ベクトル(viewDirection)は、視点(カメラ)からフラグメント(ピクセルの位置に相当する3D空間の点)への方向を示します
これは、フラグメントの位置(vPosition)からカメラの位置(cameraPosition)を引くことで計算され、視野ベクトルを正規化して長さを1にします
*Three.jsを使用してシェーダーを書く場合、cameraPositionはシェーダーに渡されるuniform変数の中に含まれており、これを利用してカメラのワールド空間における位置を取得することができます

 vec3 viewDirection = normalize(vPosition - cameraPosition);
// 補間された法線ベクトルの長さが1からずれる可能性があるので、法線を再正規化
 vec3 normal = normalize(vNormal); 
//fresnel:視野ベクトル(viewDirection)と法線ベクトル(vNormal)の間の関係を数値化
//2つのベクトルのドット積を計算
 float fresnel = dot(viewDirection, normal);

dot(viewDirection, normal)の計算結果は-1から1の範囲になりますが、これを直接フラグメントの色や透明度(この場合はアルファ値)に適用すると、意図した効果が得られない可能性があります
特に、オブジェクトの輪郭に近い部分だけを強調表示したい場合には、以下のような調整が必要です

-1から1の範囲であるため、この値を0から1の範囲に正規化する

 float fresnel = dot(viewDirection, normal) + 1.0;
 fresnel = (fresnel) / 2.0;

マテリアル設定において、side: THREE.DoubleSideオプションを使用する場合、ドット積の結果に1.0を加えたため、背面の反射が非常に明るく表示されてしまいます
フラグメントシェーダー内で、gl_FrontFacingという組み込み変数を利用して、現在描画しているフラグメントがカメラに面しているか(前面)、それとも背面かを判断します
gl_FrontFacingがfalseであれば、そのフラグメントは背面にあることを意味します
背面の場合、法線ベクトルに-1.0を乗じて反転させ、Fresnel効果が自然に見えるようにします

 if(!gl_FrontFacing) normal *= - 1.0;

オブジェクトの端に近づくにつれて、アルファが1.0から0.0へと滑らかに低下し、オブジェクトの端がより自然に見えるように

 float falloff = smoothstep(0.8, 0.0, fresnel);
 fresnel *= falloff;

頂点シェーダーで時間の経過に伴って頂点を動かすことで、オブジェクトの視覚的な表現を変化させることができる
*頂点を時間に応じて動かすことで、フレネル効果が0に近い部分がオブジェクト上で動くようになり、それに伴って色が表示される部分も変動

ライト

物体の色(反射する光の種類)と照明の色(物体に当たる光の色)を乗算することにより、私たちが最終的に見る色が決定される

  • 物体の色
    物体がどの色を反射するかは、その物体の表面の物質特性によって決まります
    例えば、完全に赤い物体は赤い光を反射し、他の色(緑や青など)の光は吸収します
  • 照明の色
    照明の色は、その光源が放出する光の色です
    自然光は白(1,1,1)に近く、すべての色を含んでいるため、様々な物体に当たると、それぞれの物体固有の色を反射します
    しかし、例えば照明が赤色の場合、つまり照明のRGB値が(1,0,0)である場合、赤以外の成分(緑と青、つまりGB)が0です
    これは、照明が赤い光の成分のみを含んでいることを意味します。したがって、物体が赤い光を反射する能力(つまり、物体が赤色を含んでいる場合)がある場合にのみ、その光は私たちの目に反射して届きます
//物体の色
vec3 color = uColor;

//光情報を格納するための変数light(すべての成分が0のベクトルで初期化)
 vec3 light = vec3(0.0);

//アンビエントライトの効果をlightに加算 
light += ambientLight(...);
//ディレクショナルライトの効果をlightに加算 
light += directionalLight(...);
//ポイントライトの効果をlightに加算 
light += pointLight(...);

//物体の色とライト(光)の色を乗算することで最終的な色を決定
 color *= light;

 gl_FragColor = vec4(color, 1.0);

アンビエントライト(環境光)
すべての表面に均等に適用されるため、物体の向きに関係なく、一定の光が当たっているように見せます

vec3 ambientLight(vec3 lightColor, float lightIntensity){
    return lightColor * lightIntensity;
}

ディレクショナルライト(指向性光)
光源からの光が一定の方向にのみ進むと仮定する光
光源の位置は考慮されず、「光源の方向」が使用されます
*スペキュラー効果を考慮

  • 面が光源に向かっている(面の法線が光の方向と同じ方向を指している)場合、その面は光の全強度を受けます
  • 面が光の方向に対して90度の角度である場合、その面は光を全く受けません
  • 面の向きと光の方向がその間の角度にある場合、受ける光の強度は角度に応じて線形に減少

スペキュラー効果:物体の表面からの光の反射がどのように観測者の目に入るか(鏡面反射)
スペキュラー成分は、視線ベクトル反射ベクトルドット積を計算
*反射ベクトルが視線ベクトルに一致している場合、目に入る反射光が最大

反射ベクトル:光が理想的に滑らかな表面で反射したときの方向
入射ベクトルに対して法線ベクトルを軸として対称の位置(入射角=反射角)

reflect関数は、入射ベクトルを基にした反射ベクトルを計算するために使用されます
I:入射ベクトル(光源から物体の表面へ向かうベクトル)
通常、このベクトルは正規化されている状態で使用
*reflect関数は、入射ベクトルが物体に向かっていると仮定して反射ベクトルを計算します
そのため光の方向を渡す場合は、その方向を反転させる必要があります
N:表面の正規化された法線ベクトル

 vec3 lightReflection = reflect(vec3 I, vec3 N);
//ディレクショナルライト
vec3 directionalLight(vec3 lightColor, float lightIntensity, vec3 normal, vec3 lightPosition, vec3 viewDirection, float specularPower){ 
     vec3 lightDirection = normalize(lightPosition); //方向を使用
     vec3 lightReflection = reflect(- lightDirection, normal);
  /*
  法線(面の向き)と光の方向のドット積
  同じ方向を指している場合に1
  90度の角度で交差している場合に0
  ベクトルが反対方向を向いている場合にマイナスの値の場合ありなので
 最大値を0.0にmax(0.0, shading)
 */
   float shading = dot(normal, lightDirection);
   shading = max(0.0, shading);
  
 // Specular
  float specular = - dot(lightReflection, viewDirection);
  specular = max(0.0, specular); //powを適用する前に、値を最大値0.0にクランプする
  specular = pow(specular, specularPower);//specularPower乗して。ピンポイントの効果
  return lightColor * lightIntensity * (shading + specular);
 }

ポイントライト(点光源)
ある特定の位置から全方向に光を放つ光源
光源の位置が重要であり、フラグメントの位置から光源へのベクトルを基に計算
*距離による減衰も考慮

//ポイントライト
vec3 pointLight(vec3 lightColor, float lightIntensity, vec3 normal, vec3 lightPosition, vec3 viewDirection, float specularPower, vec3 position, float lightDecay){ 
    vec3 lightDelta = lightPosition - position;////フラグメントの位置から光源へのベクトルを基に計算
    vec3 lightDirection = normalize(lightDelta);
    vec3 lightReflection = reflect(- lightDirection, normal);
  
    float shading = dot(normal, lightDirection);
    shading = max(0.0, shading);
  /*
    lightDistanceは光源からフラグメントまでの距離
    距離が0のときdecay = 1.0
    距離が1単位増加すると光の強度は0になり距離がわずかに増加するだけで光が完全に消えてしまう
    距離に乗じる係数lightDecayを導入して減衰率を調整
*/ 
    float lightDistance = length(lightDelta);
    float decay = 1.0 - lightDistance * lightDecay;
    decay = max(0.0, decay);
  
    // Specular
    float specular = - dot(lightReflection, viewDirection);
    specular = max(0.0, specular); //pow を適用する前に、値を最大値で 0.0 にクランプする
    specular = pow(specular, specularPower);//specularPower乗して。ピンポイントの鋭い効果
  
    return lightColor * lightIntensity * decay * (shading + specular);
   // return vec3(decay);
}