STUDIO TAMA


thumbnail

投稿日:2024-09-28

【Grasshopper Tutorial】 Custom Clipping with GHGL

  • #Grasshopper

  • #Plugin

今回は、Grasshopper のプラグインである GhGl を使用して、Grasshopper 上で GLSL(OpenGL Shading Language)を活用する方法を紹介します。具体的には、fragment shader を用いて、モデルをボックスや球体でクリップする実装を行います。

動画化もしていますので、実際に動きを見ながら学びたい方はぜひ以下のリンクから動画をご覧ください。

GLSL について

GLSL(OpenGL Shading Language)は、GPU による並列処理を活用して、リアルタイムのグラフィック処理を行うためのシェーダー言語で、多くの頂点やピクセルに対して同時に処理を行うことができるため、非常に高速で複雑なビジュアルエフェクトを実現できます。WebGL やゲームエンジンを触っている方ならなじみのある方も多いかもしれません。 私も Three.js でカスタムシェーダーを使いますが、いくつか記事のせているのでご興味あればぜひ見てください。

thumbnail

STUDIO TAMA

Rhinoceros・Grasshopper・Three.jsなどを用いたモデリングや役立つTipsを発信しています。

他にも

shaderToy

とかは狂った(いい意味で)作品をたくさん見ることができるので楽しいかもです。

環境

  • Rhinoceros8
  • GhGl v8.0.0

GhGl のインストール

thumbnail

GhGl をインストールします。PackageManager からインストールできますのでそちらからインストールします。 私は Rhino8 を使用しているので GhGl のバージョンは v8.0.0 ですが、Rhino7 の方はもしかしたら Version を落とさないといけないかもしれないのでご注意ください。

モデルの準備

thumbnail
  • モデルの準備をします。今回私は適当な建物の 1 つの Mesh モデルを使用します。サイズは約 30*30*30 程度です。各々好きなモデルを用意してもらって大丈夫ですし、Brep でも可能ですし単一要素である必要もありません。用意したら Grasshopper の Mesh コンポーネント(Brep の方は Brep コンポーネント)にセットします。
  • 次に、今回カットに使用する Box や Sphere の中心点を点要素で Rhino 側に用意します。用意したら Grasshopper の Point コンポーネントにセットします。

Grasshopper にセットしたら Rhino 側のモデルは非表示にしましょう。点はそのまま表示しておきます。

GhGl の用意

thumbnail

GL Mesh Shader コンポーネントを用意します。Display => Preview の中にあります。 先ほど用意したモデルをセットします。画像のように紫色に代わるかと思います。 コンポーネントをダブルクリックすると glsl の Editor が開くかと思います。3 つタブがあり、Vertex / Geometry / fragment の 3 つがあるかと思います。

  • vertex shader は、モデルの各頂点を操作するためのシェーダーです。今回は使用しますが、大きな変更は加えません。
  • geometry shader は、メッシュのジオメトリ(形状)を操作するためのシェーダーです。今回は全く使用しません。
  • fragment shader は、入力されたオブジェクトが画面上で占めるピクセルに対して実行されるシェーダーで、今回はこれがメインとなります。

デフォルトで書いてあるコードを軽く解説すると

【vertex に関して】

  • #version 330 は GLSL のバージョンを表しています。
  • layout の行では、頂点の位置情報 (_meshVertex) と法線情報 (_meshNormal) を受け取っています。
  • _worldToClip はワールド座標からクリップ座標に変換するための 4x4 行列で、この行列を使って 3D 空間の頂点位置をスクリーン上に表示できる形式に変換しています。
  • out vec3 normal は_meshNormal(各頂点位置での法線ベクトル) を fragment shader に渡すために定義しています。ライトの計算などは法線情報を使用して計算されるので、fragment に渡しています。
  • void main() がエントリーポイントで、normal に _meshNormal を代入しています。
  • gl_Position の行は、各頂点の最終的な位置を計算して設定する部分です。この位置情報をもとに、頂点が画面上のどこに描画されるかが決まります。
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 に関して】

  • fragment_color は最終的に各ピクセルの色を決定します。
  • uniform vec3 _lightDirection[4]; は、GHGL 側で事前に定義された 4 つの光源の方向を表すベクトルです。このコードでは、最初の光源(_lightDirection[0])を使用しています。
  • main()の最初の 3 行で、オブジェクトの各位置に対して、0 番目の光源ベクトルからの光の強度(intensity)を計算しています。光源ベクトルと各頂点の法線ベクトルとの内積により、光の強さを計算します。この計算によって、光の当たる角度に応じて明るさが変わり、オブジェクトが立体的に見える効果を生み出します。(たとえば、球体に対して光が垂直に当たる位置では内積が 1.0 となり、光の強さが最大になります。一方、光源と面が 90 度で接する位置では内積が 0 になり、光の強さも 0 になります。)
  • diffuse の行でオブジェクトの色を設定しています。vec4 型で RGBA の順番になっており、ここでは R1.0, G0.0, B1.0, Alpha(不透明度)1.0 なので、オブジェクトは紫色で表示されます。
  • ambient の行では環境光(ambient light)を設定しています。diffuse の色に対して強度 0.1 の環境光を加えています。
  • c には最終的に描画される色を計算しており、環境光の影響(ambient light)と光源の影響(directional light)を組み合わせています。

※余談ですが 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 を詳しく勉強してみると表現の幅が広がったりどのように光の効果を表現できるか学べて楽しいかもです。

Box Clipping の実装(Grasshopper で Box の準備)

thumbnail

Grasshopper で Box を準備します。

  • 最初に用意した点を中心に CenterBox コンポーネントで Box を作ります。とりあえずサイズは 5*5*5 にしておきます。
  • fragment で各辺の長さを使用しますので multiple コンポーネントで 2 倍して辺の長さの値を用意しておきます。
  • 画像の赤矢印箇所をクリックして preview を wireframe にしておいてください。

Box Clipping の実装(変数の受け取り)

では最初に Box を使ってモデルを切り取っていきます。 まず初めに頂点の座標を fragment に渡すために vertex / fragment を以下のように編集します。

  • fragment に渡すための vPosition を vec3 型で定義し、_meshVertex を代入しています。
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
thumbnail

次に変数を 4 つ渡します。コンポーネントを拡大すると+マークが出てくると思うので 4 つ追加してそれぞれ以下のような名前にして接続してください。

  • uBoxCenter : Box の中心点
  • uBoxLengthX / uBoxLengthY / uBoxLengthZ : Box の各辺の長さ

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 Clipping の実装(くり抜き)

thumbnail

それではこの 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
  • distanceX で各頂点の X 座標から Box の中心点の X 座標までの X 方向の距離を算出しています。Y/Z 方向も同様に行います。
  • insideBox で各頂点が Box の内側かどうかを判断しています。distanceX/Y/Z がそれぞれの Box の辺の長さの半分より小さかったら Box の内側となるので上記のような式となっています。
  • if 文で頂点が内側だった場合、discard することで該当するピクセルのレンダリングを行わないようにしています。Box の外側だった場合には fragment_color を指定してレンダリングしています。

画像のようにくり貫かれるかと思います。Rhino 側でポイントを移動させたり、number slider で Box サイズを変更したりすればくり抜き範囲も変わります。

※コードの間違いがないのにうまくいかない方は入力端子をつなぎなおしたり、再起動してみてください。たまに変な挙動をします。

Box Clipping の実装(切断位置の色付け)

thumbnail

切断位置の色付けを行います。切断された境界から一定の小さい距離だけ外側を色付けすることで切断面に色がついているように見せていきます。

まずは uniform で 2 つの変数を渡します。

  • uEdgeThreshold: 切断位置からどれくらいの距離色付けするか。とりあえず numberSlider コンポーネントで 0.3 を指定して GL Mesh Shader コンポーネントにつなぎます。
  • uEdgeColor:切断された箇所の色。colour Picker コンポーネントで好きな色を指定してつなぎます。

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
  • uniform で uEdgeThreshold を float 型で受け取ります。
  • uEdgeColor も受け取ります。カラーは vec4 型(RGBA)となります。
  • nearBox で色付け範囲なのかどうかを判断しています。
  • if 文に条件を追加して insideBox ではなくかつ nearBox だった場合は uEdgeColor を fragment_color に渡します。

Rhino 側で点を移動させてみると画像のように切断位置の色付けが反映されるかと思います。

Box Clipping の実装(切断部分の反転)

thumbnail

最後に切断部分を反転させる機能を加えます。 変数を 1 つ渡します。

  • uFlip: Toggle コンポーネントでフラグを渡します。False で接続しておき、True にしたら反転させます。

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}
  • uniform で uFlip を bool 型で受け取ります。
  • if 文を書き換えます。uFlip が false の場合は先ほどと同様で Box 内部がくりぬかれます。true の場合は内部のみが残り、Box の外側はレンダリングされません。

以上で Box による Clipping は完成です。

Box Clipping の最終コード

thumbnail
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}

Sphere Clipping

thumbnail

次に球体での 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

終わり

以上になります。 以下参考文献です。

目 次