ShaderとThree.jsの研究

こんにちは、B4魚田です。風鈴の音が心地よくなる季節になりました。

今回は、私の制作に関連する、ShaderとThree.jsでのグラフィックの制作の過程の一部をまとめてみます。(前回の予告ではP5.jsを使う予定でしたが、変更しました。申し訳ございません。)

Shaderと Three.jsとは?

Shader

Shaderとは3DCGの描画処理プログラムのことです。オブジェクトに陰影をつけたり、表面の質感や凹凸の設定、各画素の色を決定してくれます。それぞれの機能を、オブジェクトの頂点の位置を決定するVertex Shaderと面を構成する各画素の色を決定するFragment Shaderの2つに分けて記述します。

Three.js

Three.jsとは、3DCGを作成するためのJavaScriptライブラリです。Web上でコンピュータグラフィックを描画する低レベルAPIであるWebGLを、JavaScriptで記述できるように開発されています。シーン、カメラ、オブジェクト、ライトの要素から3DCGを描画し、アニメーションやインタラクティブ機能を追加することも可能です。

グラフィックの生成

  1. 単色の球

はじめにシンプルな球を描画してみます。

まず、球の位置を決めます。Vertex Shaderは3D オブジェクトの頂点の位置を計算するためのプログラムです。

vertexShader: `
 void main() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,

gl_Position = ... の部分が頂点の最終的な位置を決定します。それぞれの値はvec4(X座標、Y座標、Z座標、同次座標)となっています

次に、球の色を決めます。 このFragment Shaderは、3Dオブジェクトの表面の色を決定する役割を果たします。

fragmentShader: `
 precision mediump float;

 void main() {
  // オレンジ色のRGB値を定義 (1.0, 0.5, 0.0)
  vec3 orangeColor = vec3(1.0, 0.5, 0.0);
                
  // 最終的に出力される色に代入
  gl_FragColor = vec4(orangeColor, 1.0);
}
`,

gl_FragColor = ... の部分が最終的な色を決定します。それぞれの値はvec4(赤、緑、青、透明度)となっています。

このシェーダーは3Dオブジェクトの表面全体を均一なオレンジ色で塗りつぶします。

2.グラデーション

次に、球にグラデーションを付けてみます。 Vertex Shaderでは、uv(テクスチャ座標)をFragment Shaderに渡せるようにします。

fragmentShader: `
 //varyingは頂点シェーダーからフラグメントシェーダーにuv座標の値を渡すための変数。
varying vec2 vUv;

void main() {
//vUvにuv座標を代入する。
vUv = uv;

gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,

Fragment Shaderでは、vUvを受け取り、gradient(グラデーションの値)として代入しています。gradientは0.0~1.0の値をとるので、gl_FragColorにそのまま代入して使うことができます。

fragmentShader: `
 precision mediump float;

 //vUvを受け取る。
 varying vec2 vUv;

 void main() {
  //グラデーションの方向を決める
  float gradient = vUv.y;
    
  //gradientは0.0~1.0の値をとるので、そのまま代入する。
  gl_FragColor = vec4(1.0, gradient, 0.0, 1.0);
`

このように書き替えることでy軸方向のグラデーションを描画できます。

また、mix関数を使っても、グラデーションを描画することができます。

fragmentShader: `
 precision mediump float;

 varying vec2 vUv;

 void main() {
  //グラデーションの方向を決める
  float gradient = vUv.y;

  // オレンジと青の色味を決める
  vec3 blueColor = vec3(0.3, 0.3, 1.0);
  vec3 orangeColor = vec3(1.0, 0.5, 0.0);
  //グラデーションの色を決める
  vec3 finalColor = mix(orangeColor, blueColor, gradient);

  gl_FragColor = vec4(finalColor,1.0) 
`

このコードだと以下のような青→オレンジのグラデーションになります。

3.ノイズ

ノイズ的な表現を加えることもできます。Fragment Shaderへの書き加えることで実装します。

fragmentShader: `
 precision mediump float;

 varying vec2 vUv;

 //2つの2次元ベクトルの内積、三角関数とマジックナンバーを使ったノイズ関数
 float noise(vec2 st) {
  return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
 }

 void main() {
  //グラデーションの方向を決める
  float gradient = vUv.y;


  vec3 blueColor = vec3(0.3, 0.3, 1.0);
  vec3 orangeColor = vec3(1.0, 0.5, 0.0);

  vec3 finalColor = mix(orangeColor, blueColor, gradient);

  //ノイズ関数を使って、表現を付け加える。
  float noiseValue = noise(vUv * 10.0);
  //以下の式はfinalColor各要素にnoiseValueを加えている。
  finalColor += noiseValue;

  gl_FragColor = vec4(finalColor,1.0) 
`

このコードでは、球の表面に白いノイズを重ねることができます。

4.照明効果

Three.jsでは予め、オブジェクトにあてるライトの設定ができますが、Shaderを使って、照明を追加することもできます。 まず、Vertex Shaderからは、頂点の位置と法線のベクトルを渡します。

vertexShader: `
 varying vec2 vUv;

//頂点の位置を取得
 varying vec3 vNormal;
 varying vec3 vPosition;

 void main() {
  //uv座標を頂点シェーダーに渡す
  vUv = uv;
  vNormal = normalize(normalMatrix * normal);
  vPosition = vec3(modelViewMatrix * vec4(position, 1.0));

  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,

Fragment Shaderでは、vNormalvPositionを受け取り、照明の位置(lightPosition)、 照明の色(lightColor)を設定します。finalColordiffuseを乗算することで、照明をあてたfinalColorを描画することができます。

fragmentShader: `
precision mediump float;

varying vec2 vUv;

 varying vec3 vNormal;
 varying vec3 vPosition;
 const vec3 lightPosition = vec3(200.0, 200.0, 200.0);
 const vec3 lightColor = vec3(1.0, 1.0, 1.0);

 void main() {
  //照明計算
  vec3 lightDir = normalize(lightPosition - vPosition);
  vec3 normal = normalize(vNormal);   
  float diff = max(dot(normal,lightDir),0.0);
  vec3 diffuse = lightColor * diff;    

  float gradient = vUv.y;

  vec3 blueColor = vec3(0.3, 0.3, 1.0);
  vec3 orangeColor = vec3(1.0, 0.5, 0.0);

  vec3 finalColor = mix(orangeColor, blueColor, gradient);

  //照明効果を乗算する。
  finalColor = finalColor * (diffuse);

  gl_FragColor = vec4(finalColor,1.0) 
`

その他の照明効果として、明るい部分をより明るく、暗い部分をより暗くするグロー効果を実装することもできます。((0.299, 0.587, 0.114)はグロースケールという定数)

// 輝度効果
float luminance = dot(finalColor, vec3(0.299, 0.587, 0.114));
float glowStrength = 5.0;
vec3 glow = vec3(1.0, 0.7, 0.3) * pow(luminance, 3.0) * glowStrength;   

gl_FragColor = vec4(finalColor + glow, 1.0);

おわりに

ここまで、Shaderでできる、グラデーション、ノイズ、照明などの描画処理をまとめました。Vertex ShaderとFragment Shaderの組み合わせによっては、さらに複雑な表現ができます。機会があれば、そういった表現もまとめたいと思います。 ご高覧頂き、ありがとうございました。