投稿日:2024-09-28
#Grasshopper
#Plugin
今回は、Grasshopper のプラグインである GhGl を使用して、Grasshopper 上で GLSL(OpenGL Shading Language)を活用する方法を紹介します。具体的には、fragment shader を用いて、モデルをボックスや球体でクリップする実装を行います。
動画化もしていますので、実際に動きを見ながら学びたい方はぜひ以下のリンクから動画をご覧ください。
GLSL(OpenGL Shading Language)は、GPU による並列処理を活用して、リアルタイムのグラフィック処理を行うためのシェーダー言語で、多くの頂点やピクセルに対して同時に処理を行うことができるため、非常に高速で複雑なビジュアルエフェクトを実現できます。WebGL やゲームエンジンを触っている方ならなじみのある方も多いかもしれません。 私も Three.js でカスタムシェーダーを使いますが、いくつか記事のせているのでご興味あればぜひ見てください。
他にも
とかは狂った(いい意味で)作品をたくさん見ることができるので楽しいかもです。
GhGl をインストールします。PackageManager からインストールできますのでそちらからインストールします。 私は Rhino8 を使用しているので GhGl のバージョンは v8.0.0 ですが、Rhino7 の方はもしかしたら Version を落とさないといけないかもしれないのでご注意ください。
Grasshopper にセットしたら Rhino 側のモデルは非表示にしましょう。点はそのまま表示しておきます。
GL Mesh Shader コンポーネントを用意します。Display => Preview の中にあります。 先ほど用意したモデルをセットします。画像のように紫色に代わるかと思います。 コンポーネントをダブルクリックすると glsl の Editor が開くかと思います。3 つタブがあり、Vertex / Geometry / fragment の 3 つがあるかと思います。
デフォルトで書いてあるコードを軽く解説すると
【vertex に関して】
1#version 330 2 3layout(location = 0) in vec3 _meshVertex; 4layout(location = 1) in vec3 _meshNormal; 5 6uniform mat4 _worldToClip; 7out vec3 normal; 8 9void main() { 10 normal = _meshNormal; 11 gl_Position = _worldToClip * vec4(_meshVertex , 1.0); 12}
【fragment に関して】
※余談ですが c を算出している行で、directionalLight の効果に abs(intensity) を使って絶対値をとっているため、光の影響を受けない位置にも光が当たるようになっていますが、これは球体の裏側のように光源から直接光が当たらない場所にも光が届くことを意味します。たぶん、オブジェクトをはっきり見せるためだと思いますが、もし光源の影響を現実的に表現したい場合は、max(intensity, 0.0) として負の値を 0 にすることで、純粋に光の当たる位置のみが明るくなります。
1#version 330 2 3uniform vec3 _lightDirection[4]; 4uniform mat3 _worldToCameraNormal; 5 6in vec3 normal; 7out vec4 fragment_color; 8 9void main() { 10 vec3 l = normalize(_lightDirection[0]); 11 vec3 camNormal = _worldToCameraNormal * normal; 12 float intensity = dot(l, normalize(camNormal.xyz)); 13 vec4 diffuse = vec4(1.0, 0.0, 1.0, 1.0); 14 15 vec3 ambient = vec3(0.1, 0.1, 0.1) * diffuse.rgb; 16 vec3 c = ambient + diffuse.rgb * abs(intensity); 17 fragment_color = vec4(c, diffuse.a); 18} 19
詳しくは説明しませんでしたが、ご興味あれば glsl を詳しく勉強してみると表現の幅が広がったりどのように光の効果を表現できるか学べて楽しいかもです。
Grasshopper で Box を準備します。
では最初に Box を使ってモデルを切り取っていきます。 まず初めに頂点の座標を fragment に渡すために vertex / fragment を以下のように編集します。
1#version 330 2 3layout(location = 0) in vec3 _meshVertex; 4layout(location = 1) in vec3 _meshNormal; 5 6uniform mat4 _worldToClip; 7out vec3 normal; 8out vec3 vPosition; 9 10void main() { 11 normal = _meshNormal; 12 gl_Position = _worldToClip * vec4(_meshVertex , 1.0); 13 vPosition = _meshVertex; 14}
1#version 330 2 3uniform vec3 _lightDirection[4]; 4uniform mat3 _worldToCameraNormal; 5 6in vec3 normal; 7in vec3 vPosition; //vertexShaderから受け取る 8out vec4 fragment_color; 9 10void main() { 11 vec3 l = normalize(_lightDirection[0]); 12 vec3 camNormal = _worldToCameraNormal * normal; 13 float intensity = dot(l, normalize(camNormal.xyz)); 14 vec4 diffuse = vec4(1.0, 0.0, 1.0, 1.0); 15 16 vec3 ambient = vec3(0.1, 0.1, 0.1) * diffuse.rgb; 17 vec3 c = ambient + diffuse.rgb * abs(intensity); 18 fragment_color = vec4(c, diffuse.a); 19} 20
次に変数を 4 つ渡します。コンポーネントを拡大すると+マークが出てくると思うので 4 つ追加してそれぞれ以下のような名前にして接続してください。
fragment を以下のように修正して変数を受け取ります。 ついでにモデルの色を白くして ambientLight の効果を少し強くしています。
1#version 330 2 3uniform vec3 _lightDirection[4]; 4uniform mat3 _worldToCameraNormal; 5 6uniform vec3 uBoxCenter; //uniformで変数を受け取る 7uniform int uBoxLengthX; //uniformで変数を受け取る 8uniform int uBoxLengthY; //uniformで変数を受け取る 9uniform int uBoxLengthZ; //uniformで変数を受け取る 10 11in vec3 normal; 12in vec3 vPosition; 13out vec4 fragment_color; 14 15void main() { 16 vec3 l = normalize(_lightDirection[0]); 17 vec3 camNormal = _worldToCameraNormal * normal; 18 float intensity = dot(l, normalize(camNormal.xyz)); 19 vec4 diffuse = vec4(1.0, 1.0, 1.0, 1.0); //モデルの色をホワイトに変更 20 21 vec3 ambient = vec3(0.3, 0.3, 0.3) * diffuse.rgb; //ambientLightを少し強く 22 vec3 c = ambient + diffuse.rgb * abs(intensity); 23 fragment_color = vec4(c, diffuse.a); 24}
それではこの Box を使ってモデルをくり抜いていきます。
1#version 330 2 3uniform vec3 _lightDirection[4]; 4uniform mat3 _worldToCameraNormal; 5uniform vec3 uBoxCenter; 6uniform int uBoxLengthX; 7uniform int uBoxLengthY; 8uniform int uBoxLengthZ; 9 10in vec3 normal; 11out vec4 fragment_color; 12in vec3 vPosition; 13 14void main() { 15 vec3 l = normalize(_lightDirection[0]); 16 vec3 camNormal = _worldToCameraNormal * normal; 17 float intensity = dot(l, normalize(camNormal.xyz)); 18 vec4 diffuse = vec4(1.0, 1.0, 1.0, 1.0); 19 20 vec3 ambient = vec3(0.3, 0.3, 0.3) * diffuse.rgb; 21 vec3 c = ambient + diffuse.rgb * abs(intensity); 22 23 float distanceX = abs(vPosition.x - uBoxCenter.x); 24 float distanceY = abs(vPosition.y - uBoxCenter.y); 25 float distanceZ = abs(vPosition.z - uBoxCenter.z); 26 27 bool insideBox = ( 28 distanceX < uBoxLengthX * 0.5 29 && distanceY < uBoxLengthY * 0.5 30 && distanceZ < uBoxLengthZ * 0.5 31 ); 32 33 if(insideBox){ 34 discard; 35 }else{ 36 fragment_color = vec4(c,diffuse.a); 37 } 38} 39
画像のようにくり貫かれるかと思います。Rhino 側でポイントを移動させたり、number slider で Box サイズを変更したりすればくり抜き範囲も変わります。
※コードの間違いがないのにうまくいかない方は入力端子をつなぎなおしたり、再起動してみてください。たまに変な挙動をします。
切断位置の色付けを行います。切断された境界から一定の小さい距離だけ外側を色付けすることで切断面に色がついているように見せていきます。
まずは uniform で 2 つの変数を渡します。
fragment を以下のように直します。
1#version 330 2 3uniform vec3 _lightDirection[4]; 4uniform mat3 _worldToCameraNormal; 5uniform vec3 uBoxCenter; 6uniform int uBoxLengthX; 7uniform int uBoxLengthY; 8uniform int uBoxLengthZ; 9uniform float uEdgeThreshold; 10uniform vec4 uEdgeColor; 11 12in vec3 normal; 13in vec3 vPosition; 14out vec4 fragment_color; 15 16void main() { 17 vec3 l = normalize(_lightDirection[0]); 18 vec3 camNormal = _worldToCameraNormal * normal; 19 float intensity = dot(l, normalize(camNormal.xyz)); 20 vec4 diffuse = vec4(1.0, 1.0, 1.0, 1.0); 21 22 vec3 ambient = vec3(0.3, 0.3, 0.3) * diffuse.rgb; 23 vec3 c = ambient + diffuse.rgb * abs(intensity); 24 25 float distanceX = abs(vPosition.x - uBoxCenter.x); 26 float distanceY = abs(vPosition.y - uBoxCenter.y); 27 float distanceZ = abs(vPosition.z - uBoxCenter.z); 28 29 bool insideBox = ( 30 distanceX < uBoxLengthX * 0.5 31 && distanceY < uBoxLengthY * 0.5 32 && distanceZ < uBoxLengthZ * 0.5 33 ); 34 35 bool nearBox = ( 36 distanceX < uBoxLengthX * 0.5 + uEdgeThreshold 37 && distanceY < uBoxLengthY * 0.5 + uEdgeThreshold 38 && distanceZ < uBoxLengthZ * 0.5 + uEdgeThreshold 39 ); 40 41 if(insideBox){ 42 discard; 43 }else if(nearBox){ 44 fragment_color = uEdgeColor; 45 }else{ 46 fragment_color = vec4(c,diffuse.a); 47 } 48} 49
Rhino 側で点を移動させてみると画像のように切断位置の色付けが反映されるかと思います。
最後に切断部分を反転させる機能を加えます。 変数を 1 つ渡します。
fragment shader を編集します。
1#version 330 2 3uniform vec3 _lightDirection[4]; 4uniform mat3 _worldToCameraNormal; 5uniform vec3 uBoxCenter; 6uniform int uBoxLengthX; 7uniform int uBoxLengthY; 8uniform int uBoxLengthZ; 9uniform float uEdgeThreshold; 10uniform vec4 uEdgeColor; 11uniform bool uFlip; 12 13in vec3 normal; 14in vec3 vPosition; 15out vec4 fragment_color; 16 17void main() { 18 vec3 l = normalize(_lightDirection[0]); 19 vec3 camNormal = _worldToCameraNormal * normal; 20 float intensity = dot(l, normalize(camNormal.xyz)); 21 vec4 diffuse = vec4(1.0, 1.0, 1.0, 1.0); 22 23 vec3 ambient = vec3(0.3, 0.3, 0.3) * diffuse.rgb; 24 vec3 c = ambient + diffuse.rgb * abs(intensity); 25 26 float distanceX = abs(vPosition.x - uBoxCenter.x); 27 float distanceY = abs(vPosition.y - uBoxCenter.y); 28 float distanceZ = abs(vPosition.z - uBoxCenter.z); 29 30 bool insideBox = ( 31 distanceX < uBoxLengthX * 0.5 32 && distanceY < uBoxLengthY * 0.5 33 && distanceZ < uBoxLengthZ * 0.5 34 ); 35 36 bool nearBox = ( 37 distanceX < uBoxLengthX * 0.5 + uEdgeThreshold 38 && distanceY < uBoxLengthY * 0.5 + uEdgeThreshold 39 && distanceZ < uBoxLengthZ * 0.5 + uEdgeThreshold 40 ); 41 42 if(!uFlip){ 43 if(insideBox){ 44 discard; 45 }else if(nearBox){ 46 fragment_color = uEdgeColor; 47 }else{ 48 fragment_color = vec4(c,diffuse.a); 49 } 50 }else{ 51 if(insideBox){ 52 fragment_color = vec4(c,diffuse.a); 53 }else if(nearBox){ 54 fragment_color = uEdgeColor; 55 }else{ 56 discard; 57 } 58 } 59}
以上で Box による Clipping は完成です。
1#version 330 2 3layout(location = 0) in vec3 _meshVertex; 4layout(location = 1) in vec3 _meshNormal; 5 6uniform mat4 _worldToClip; 7out vec3 normal; 8out vec3 vPosition; 9 10void main() { 11 normal = _meshNormal; 12 gl_Position = _worldToClip * vec4(_meshVertex , 1.0); 13 vPosition = _meshVertex; 14} 15
1#version 330 2 3uniform vec3 _lightDirection[4]; 4uniform mat3 _worldToCameraNormal; 5uniform vec3 uBoxCenter; 6uniform int uBoxLengthX; 7uniform int uBoxLengthY; 8uniform int uBoxLengthZ; 9uniform float uEdgeThreshold; 10uniform vec4 uEdgeColor; 11uniform bool uFlip; 12 13in vec3 normal; 14in vec3 vPosition; 15out vec4 fragment_color; 16 17void main() { 18 vec3 l = normalize(_lightDirection[0]); 19 vec3 camNormal = _worldToCameraNormal * normal; 20 float intensity = dot(l, normalize(camNormal.xyz)); 21 vec4 diffuse = vec4(1.0, 1.0, 1.0, 1.0); 22 23 vec3 ambient = vec3(0.3, 0.3, 0.3) * diffuse.rgb; 24 vec3 c = ambient + diffuse.rgb * abs(intensity); 25 26 float distanceX = abs(vPosition.x - uBoxCenter.x); 27 float distanceY = abs(vPosition.y - uBoxCenter.y); 28 float distanceZ = abs(vPosition.z - uBoxCenter.z); 29 30 bool insideBox = ( 31 distanceX < uBoxLengthX * 0.5 32 && distanceY < uBoxLengthY * 0.5 33 && distanceZ < uBoxLengthZ * 0.5 34 ); 35 36 bool nearBox = ( 37 distanceX < uBoxLengthX * 0.5 + uEdgeThreshold 38 && distanceY < uBoxLengthY * 0.5 + uEdgeThreshold 39 && distanceZ < uBoxLengthZ * 0.5 + uEdgeThreshold 40 ); 41 42 if(!uFlip){ 43 if(insideBox){ 44 discard; 45 }else if(nearBox){ 46 fragment_color = uEdgeColor; 47 }else{ 48 fragment_color = vec4(c,diffuse.a); 49 } 50 }else{ 51 if(insideBox){ 52 fragment_color = vec4(c,diffuse.a); 53 }else if(nearBox){ 54 fragment_color = uEdgeColor; 55 }else{ 56 discard; 57 } 58 } 59}
次に球体での Clipping を行いますが、Box より簡単なので解説は割愛します。最終コードだけ載せておきますが、考え方は各頂点の位置と球体の中心までの距離が半径以下かどうかを判断しています。
1#version 330 2 3layout(location = 0) in vec3 _meshVertex; 4layout(location = 1) in vec3 _meshNormal; 5 6uniform mat4 _worldToClip; 7out vec3 normal; 8out vec3 vPosition; 9 10void main() { 11 normal = _meshNormal; 12 gl_Position = _worldToClip * vec4(_meshVertex , 1.0); 13 vPosition = _meshVertex; 14}
1#version 330 2 3uniform vec3 _lightDirection[4]; 4uniform mat3 _worldToCameraNormal; 5uniform vec3 uSphereCenter; 6uniform float uSphereRadius; 7uniform float uEdgeThreshold; 8uniform vec4 uEdgeColor; 9uniform bool uFlip; 10 11in vec3 normal; 12out vec4 fragment_color; 13in vec3 vPosition; 14 15void main() { 16 vec3 l = normalize(_lightDirection[0]); 17 vec3 camNormal = _worldToCameraNormal * normal; 18 float intensity = dot(l, normalize(camNormal.xyz)); 19 vec4 diffuse = vec4(1.0, 1.0, 1.0, 1.0); 20 21 vec3 ambient = vec3(0.3, 0.3, 0.3) * diffuse.rgb; 22 vec3 c = ambient + diffuse.rgb * abs(intensity); 23 24 bool insideSphere = length(vPosition - uSphereCenter) < uSphereRadius; 25 bool nearSphere = length(vPosition - uSphereCenter) < uSphereRadius + uEdgeThreshold; 26 27 if(!uFlip){ 28 if(insideSphere){ 29 discard; 30 }else if(nearSphere){ 31 fragment_color = uEdgeColor; 32 }else{ 33 fragment_color = vec4(c, diffuse.a); 34 } 35 }else{ 36 if(insideSphere){ 37 fragment_color = vec4(c, diffuse.a); 38 }else if(nearSphere){ 39 fragment_color = uEdgeColor; 40 }else{ 41 discard; 42 } 43 } 44} 45
以上になります。 以下参考文献です。