WebGL & Three.js

【WebGL】GLSLで円や三角形、四角形、その他多角形の図形を描写する方法

この記事の目次
  1. GLSLで円を描写するコード
  2. まず円の周囲に色をつける
  3. 反転させて円に色をつける
  4. canvasと要素の座標の比率を同じにする(座標の正規化)
  5. smoothstep関数で滑らかな円を描く
  6. グラデーションありの円を作成する
  7. グラデーションありの三角形を作成する
  8. グラデーションありの四角形を作成する
  9. グラデーションありの五角形を作成する
  10. グラデーションありの六角形を作成する
  11. グラデーションありの七角形を作成する
  12. グラデーションありの八角形を作成する
  13. 十角形でも十二角形でもどんな多角形でも作成できるコード
  14. タイルっぽく色をつける方法(floor関数)
  15. 二色を横方向にグラデーションさせる(mix関数)
  16. 二色を横方向に(グラデーションさせない)(step関数)
  17. 円に画像を貼り付ける

WebGL(GLSL)で円や三角形、四角形、八角形、十角形といった多角形の図形を描写する方法を紹介します。

基本的なGLSLの計算方法については、GLSLで使う数学(三角関数、ベクトル)を文系エンジニアのために実務でいるところだけ解説を参考にしてみてください。
また定義の方法は、GLSLで定数を定義する方法(define, constとその違いと注意点)を参考にしてみてください。

GLSLで円を描写するコード

今回は色の話になるのでgragment.glslをみていきます。

まず円の周囲に色をつける

precision mediump float;

uniform vec2 uColor;
varying vec2 vVertexPosition;

void main() {
    float circle = length(vVertexPosition);
    vec4 color = vec4(0.0, circle, 0.0, 1.0);
    gl_FragColor = color;
}

上のコードで生成される円は次のようなかたちになります。

vVertexPositionには、それぞれの頂点の座標がわかってきます。
lengthはGLSLの組み込み関数。原点からベクトルの先っぽまでの長さを取得できます。
そのため、原点から遠いところの座標は1に近い値になり、rgbaが(0,1,0,1)と緑色になるため、周りが緑色で、原点に近づくにつれて徐々にRGBaのGの値が小さくなり黒くます。

反転させるためには次のようにします。

反転させて円に色をつける

void main() {
    float circle = 1.0 - length(vVertexPosition);
    // float circle = length(vVertexPosition);
    vec4 color = vec4(0.0, circle, 0.0, 1.0);
    gl_FragColor = color;
}

上のコードで生成される円は次のようなかたちになります。

1.0からlength関数の値を引いてあげることで反転させることができます。

canvasと要素の座標の比率を同じにする(座標の正規化)

void main() {

    vec2 position = vVertexPosition * vec2(2.0, 1.0);
    float circle = 1.0 - length(position);
    // float circle = length(vVertexPosition);
    vec4 color = vec4(0.0, circle, 0.0, 1.0);
    gl_FragColor = color;
}

上のコードで生成される円は次のようなかたちになります。

canvasのデフォルトは幅300px:高さ150pxと2:1になっています。
クリップ座標は-1から1で1:1なので、上のように横が長く描写されます。
クリップ座標はあくまで-1から1までしか表示されませんが、要素の座標は変更することができます。
要素の座標(position)の幅に2倍にしてあげることで、2:1になって比率が合うようになります。
つまり、要素の座標のx軸は-2から2になり、y軸は-1から1となり、渡ってくる値は-1から1なので-1から1の間で色の変化が起きます。
これを座標の正規化といいます。

smoothstep関数で滑らかな円を描く

void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);
    float cSize = length(position) * 1.5; // 円のサイズの調整
    float cLength = smoothstep(0.96, 1.0, cSize);
    float circle = 1.0 - cLength;
    vec4 color = vec4(0.0, circle, 0.0, 1.0);
    gl_FragColor = color;
}

上のコードで生成される円は次のようなかたちになります。

現在の頂点の位置を取得し、x座標を2倍、y座標をそのままに変更します。
現在の位置から原点までの距離を計算し、1.5倍します。
smoothstep関数を用いて、cSizeが0.96に近づくにつれて滑らかに値を変化させます。
0.96という値は実際には見ながら調整していきます。
ここでは入力値が同じため、cLengthは常に0.0になります。
1.0からcLengthを引いて円の形状を作成します。
ただし、cLengthが常に0.0なので、この行は常にcircle = 1.0となります。
色を設定します。赤成分は0.0、緑成分はcircle(常に1.0)、青成分は0.0、アルファ値は1.0です。

スポンサーリンク

グラデーションありの円を作成する

void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);
    float cSize = length(position) * 1.5; // 円のサイズの調整
    float cLength = smoothstep(0.96, 1.0, cSize);
    float circle = 1.0 - cLength;
       グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * circle;

    vec4 color = vec4(0.0,uv, 1.0);
    // vec4 color = vec4(uv.y,uv, 1.0);
    gl_FragColor = color;
}

基本は上と同じなのですが、色をグラデーションにするには、VVertexPositionで線形補間されてくる値をcircleに掛け合わせることで実現できます。
その値をRGBのどこかの2次元に当てはめることでグラデーションになります。

グラデーションありの三角形を作成する

// 三角形
void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);

    // 三角形の頂点を定義
    vec2 p0 = vec2(0.0, 1.0); // 上部の頂点
    vec2 p1 = vec2(-1.0, -1.0); // 左下の頂点
    vec2 p2 = vec2(1.0, -1.0); // 右下の頂点

    // バリセントリック座標を計算
    float alpha = ((p1.y - p2.y)*(position.x - p2.x) + (p2.x - p1.x)*(position.y - p2.y)) / ((p1.y - p2.y)*(p0.x - p2.x) + (p2.x - p1.x)*(p0.y - p2.y));
    float beta = ((p2.y - p0.y)*(position.x - p2.x) + (p0.x - p2.x)*(position.y - p2.y)) / ((p1.y - p2.y)*(p0.x - p2.x) + (p2.x - p1.x)*(p0.y - p2.y));
    float gamma = 1.0 - alpha - beta;

    // バリセントリック座標を用いて三角形の内部を判断
    float triangleShape = 0.0;
    if (alpha > 0.0 && beta > 0.0 && gamma > 0.0) {
        triangleShape = 1.0; // 点は三角形の内部にある
    }

    // グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * triangleShape;

    vec4 color = vec4(0.0, uv, 1.0);
    gl_FragColor = color;
}

三角形の形状を定義するには、フラグメントシェーダー内で特定の距離関数を使用することが一般的です。
たとえば、画面上の各ピクセルが三角形の内部にあるかどうかを判断するために、バリセントリック座標を使用する方法があります。

バリセントリック座標は、三角形内の点が三角形の頂点に対してどの位置にあるかを示すために使用されます。
これをフラグメントシェーダーで計算するためには、まず三角形の各頂点の位置を定義する必要があります。
ここでは、シンプルな等辺三角形を考えます。

上のコードは等辺三角形の形状を定義するためのGLSLコードの例です。

三角形の形状を定義し、与えられた点が三角形の内部にあるかどうかを判断するためのものです。
点が三角形の内部にある場合、triangleShapeは1.0になり、
そうでない場合は0.0になります。
その結果、三角形の内部のピクセルのみが色付けされ、残りの部分は透明になります。

グラデーションありの四角形を作成する

// 四角形
void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);

    // 四角形の頂点を定義
    vec2 p0 = vec2(-1.0, 1.0); // 左上
    vec2 p1 = vec2(-1.0, -1.0); // 左下
    vec2 p2 = vec2(1.0, -1.0); // 右下
    vec2 p3 = vec2(1.0, 1.0); // 右上

    // 二つの三角形に分割してそれぞれのバリセントリック座標を計算
    float alpha1 = ((p1.y - p2.y)*(position.x - p2.x) + (p2.x - p1.x)*(position.y - p2.y)) / ((p1.y - p2.y)*(p0.x - p2.x) + (p2.x - p1.x)*(p0.y - p2.y));
    float beta1 = ((p2.y - p0.y)*(position.x - p2.x) + (p0.x - p2.x)*(position.y - p2.y)) / ((p1.y - p2.y)*(p0.x - p2.x) + (p2.x - p1.x)*(p0.y - p2.y));
    float gamma1 = 1.0 - alpha1 - beta1;

    float alpha2 = ((p0.y - p3.y)*(position.x - p3.x) + (p3.x - p0.x)*(position.y - p3.y)) / ((p0.y - p3.y)*(p2.x - p3.x) + (p3.x - p0.x)*(p2.y - p3.y));
    float beta2 = ((p3.y - p2.y)*(position.x - p3.x) + (p2.x - p3.x)*(position.y - p3.y)) / ((p0.y - p3.y)*(p2.x - p3.x) + (p3.x - p0.x)*(p2.y - p3.y));
    float gamma2 = 1.0 - alpha2 - beta2;

    // 二つの三角形のいずれかに点が含まれているか判断
    float quadShape = 0.0;
    if ((alpha1 > 0.0 && beta1 > 0.0 && gamma1 > 0.0) || (alpha2 > 0.0 && beta2 > 0.0 && gamma2 > 0.0)) {
        quadShape = 1.0; // 点は四角形の内部にある
    }

    // グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * quadShape;

    vec4 color = vec4(0.0, uv, 1.0);
    gl_FragColor = color;
}

四角形を描画するためのGLSLコードは、三角形の場合と比較して少し異なりますが、基本的なアイデアは同じです。
四角形を描画するには、その四辺に対して点が内側にあるかどうかを判断する必要があります。

一般的な方法は、四角形を二つの三角形に分割し、それぞれに対してバリセントリック座標を計算するか、
あるいは直接的に各辺との関係を計算する方法です。

矩形を左上から右下にかけての対角線で二つの三角形に分割しています。
そして、それぞれの三角形に対してバリセントリック座標を計算し、点が四角形の内部にあるかどうかを判断しています。
この方法で四角形の内部のピクセルを特定し、グラデーションを適用しています。

グラデーションありの五角形を作成する

// 五角形
void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);

    // 五角形の頂点を定義
    vec2 p0 = vec2(0.0, 1.0); // 上部
    vec2 p1 = vec2(-0.951, 0.309); // 左上
    vec2 p2 = vec2(-0.588, -0.809); // 左下
    vec2 p3 = vec2(0.588, -0.809); // 右下
    vec2 p4 = vec2(0.951, 0.309); // 右上

    // 五角形を三つの三角形に分割して、それぞれのバリセントリック座標を計算
    // 三角形1: p0, p1, p2
    float alpha1 = ((p1.y - p2.y)*(position.x - p2.x) + (p2.x - p1.x)*(position.y - p2.y)) / ((p1.y - p2.y)*(p0.x - p2.x) + (p2.x - p1.x)*(p0.y - p2.y));
    float beta1 = ((p2.y - p0.y)*(position.x - p2.x) + (p0.x - p2.x)*(position.y - p2.y)) / ((p1.y - p2.y)*(p0.x - p2.x) + (p2.x - p1.x)*(p0.y - p2.y));
    float gamma1 = 1.0 - alpha1 - beta1;

    // 三角形2: p0, p2, p3
    float alpha2 = ((p2.y - p3.y)*(position.x - p3.x) + (p3.x - p2.x)*(position.y - p3.y)) / ((p2.y - p3.y)*(p0.x - p3.x) + (p3.x - p2.x)*(p0.y - p3.y));
    float beta2 = ((p3.y - p0.y)*(position.x - p3.x) + (p0.x - p3.x)*(position.y - p3.y)) / ((p2.y - p3.y)*(p0.x - p3.x) + (p3.x - p2.x)*(p0.y - p3.y));
    float gamma2 = 1.0 - alpha2 - beta2;

    // 三角形3: p0, p3, p4
    float alpha3 = ((p3.y - p4.y)*(position.x - p4.x) + (p4.x - p3.x)*(position.y - p4.y)) / ((p3.y - p4.y)*(p0.x - p4.x) + (p4.x - p3.x)*(p0.y - p4.y));
    float beta3 = ((p4.y - p0.y)*(position.x - p4.x) + (p0.x - p4.x)*(position.y - p4.y)) / ((p3.y - p4.y)*(p0.x - p4.x) + (p4.x - p3.x)*(p0.y - p4.y));
    float gamma3 = 1.0 - alpha3 - beta3;

    // 五角形の内部に点が含まれているか判断
    float pentagonShape = 0.0;
    if ((alpha1 > 0.0 && beta1 > 0.0 && gamma1 > 0.0) ||
        (alpha2 > 0.0 && beta2 > 0.0 && gamma2 > 0.0) ||
        (alpha3 > 0.0 && beta3 > 0.0 && gamma3 > 0.0)) {
        pentagonShape = 1.0; // 点は五角形の内部にある
    }

    // グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * pentagonShape;

    vec4 color = vec4(0.0, uv, 1.0);
    gl_FragColor = color;
}

五角形を描画するためのGLSLコードは、三角形や四角形よりも少し複雑です。
五角形は単純な形状でないため、直接的なバリセントリック座標や簡単な距離計算で描画するのは難しいです。
一般的な方法は、五角形を複数の三角形に分割し、それぞれの三角形に対して点が内側にあるかを判断することです。

五角形を描画するには、まず五角形の頂点を定義し、これらの頂点を使って五角形を複数の三角形に分割します。
次に、これらの三角形それぞれについて、フラグメントシェーダーでバリセントリック座標を計算し、点が三角形の内部にあるかどうかを判断します。

五角形を三つの三角形に分割し、それぞれに対してバリセントリック座標を計算しています。
各三角形について点が内部に含まれるかを判断し、少なくとも一つの三角形の内部に点がある場合、その点は五角形の内部と判断されます。
その結果、五角形の内部のピクセルのみが色付けされ、残りの部分は透明になります。

グラデーションありの六角形を作成する

void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);

    // 六角形の頂点を定義
    vec2 p0 = vec2(0.0, 1.0); // 中心
    vec2 p1 = vec2(-0.866, 0.5); // 左上
    vec2 p2 = vec2(-0.866, -0.5); // 左下
    vec2 p3 = vec2(0.0, -1.0); // 下部
    vec2 p4 = vec2(0.866, -0.5); // 右下
    vec2 p5 = vec2(0.866, 0.5); // 右上

    // 六角形を六つの三角形に分割して、それぞれのバリセントリック座標を計算
    // 三角形1: p0, p1, p2
    float alpha1 = ((p1.y - p2.y)*(position.x - p2.x) + (p2.x - p1.x)*(position.y - p2.y)) / ((p1.y - p2.y)*(p0.x - p2.x) + (p2.x - p1.x)*(p0.y - p2.y));
    float beta1 = ((p2.y - p0.y)*(position.x - p2.x) + (p0.x - p2.x)*(position.y - p2.y)) / ((p1.y - p2.y)*(p0.x - p2.x) + (p2.x - p1.x)*(p0.y - p2.y));
    float gamma1 = 1.0 - alpha1 - beta1;

    // 三角形2: p0, p2, p3
    float alpha2 = ((p2.y - p3.y)*(position.x - p3.x) + (p3.x - p2.x)*(position.y - p3.y)) / ((p2.y - p3.y)*(p0.x - p3.x) + (p3.x - p2.x)*(p0.y - p3.y));
    float beta2 = ((p3.y - p0.y)*(position.x - p3.x) + (p0.x - p3.x)*(position.y - p3.y)) / ((p2.y - p3.y)*(p0.x - p3.x) + (p3.x - p2.x)*(p0.y - p3.y));
    float gamma2 = 1.0 - alpha2 - beta2;

    // 三角形3: p0, p3, p4
    float alpha3 = ((p3.y - p4.y)*(position.x - p4.x) + (p4.x - p3.x)*(position.y - p4.y)) / ((p3.y - p4.y)*(p0.x - p4.x) + (p4.x - p3.x)*(p0.y - p4.y));
    float beta3 = ((p4.y - p0.y)*(position.x - p4.x) + (p0.x - p4.x)*(position.y - p4.y)) / ((p3.y - p4.y)*(p0.x - p4.x) + (p4.x - p3.x)*(p0.y - p4.y));
    float gamma3 = 1.0 - alpha3 - beta3;

    // 三角形4: p0, p4, p5
    float alpha4 = ((p4.y - p5.y)*(position.x - p5.x) + (p5.x - p4.x)*(position.y - p5.y)) / ((p4.y - p5.y)*(p0.x - p5.x) + (p5.x - p4.x)*(p0.y - p5.y));
    float beta4 = ((p5.y - p0.y)*(position.x - p5.x) + (p0.x - p5.x)*(position.y - p5.y)) / ((p4.y - p5.y)*(p0.x - p5.x) + (p5.x - p4.x)*(p0.y - p5.y));
    float gamma4 = 1.0 - alpha4 - beta4;

    // 三角形5: p0, p5, p1
    float alpha5 = ((p5.y - p1.y)*(position.x - p1.x) + (p1.x - p5.x)*(position.y - p1.y)) / ((p5.y - p1.y)*(p0.x - p1.x) + (p1.x - p5.x)*(p0.y - p1.y));
    float beta5 = ((p1.y - p0.y)*(position.x - p1.x) + (p0.x - p1.x)*(position.y - p1.y)) / ((p5.y - p1.y)*(p0.x - p1.x) + (p1.x - p5.x)*(p0.y - p1.y));
    float gamma5 = 1.0 - alpha5 - beta5;

    // 三角形6: p0, p1, p2(再び)
    // この三角形は既に計算済みなので、再度計算する必要はありません。

    // 六角形の内部に点が含まれているか判断
    float hexagonShape = 0.0;
    if ((alpha1 > 0.0 && beta1 > 0.0 && gamma1 > 0.0) ||
        (alpha2 > 0.0 && beta2 > 0.0 && gamma2 > 0.0) ||
        (alpha3 > 0.0 && beta3 > 0.0 && gamma3 > 0.0) ||
        (alpha4 > 0.0 && beta4 > 0.0 && gamma4 > 0.0) ||
        (alpha5 > 0.0 && beta5 > 0.0 && gamma5 > 0.0)) {
        hexagonShape = 1.0; // 点は六角形の内部にある
    }

    // グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * hexagonShape;

    vec4 color = vec4(0.0, uv, 1.0);
    gl_FragColor = color;
}

基本的なアイデアは同じです。六角形を複数の三角形に分割し、それぞれの三角形について点が内側にあるかどうかを判断します。
六角形を描画するために、中心を共有する六つの三角形に分割し、それぞれのバリセントリック座標を計算します。

六角形を六つの三角形に分割し、それぞれに対してバリセントリック座標を計算しています。
各三角形について点が内部に含まれるかを判断し、少なくとも一つの三角形の内部に点がある場合、その点は六角形の内部と判断されます。
その結果、六角形の内部のピクセルのみが色付けされ、残りの部分は透明になります。

グラデーションありの七角形を作成する

// 七角形
void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);

    // 中心点
    vec2 p0 = vec2(0.0, 1.0);

    // 七角形の内部に点が含まれているか判断
    float heptagonShape = 0.0;
    float angle, angleNext;
    vec2 p, pNext;

    for(int i = 0; i < 7; ++i) {
        // 現在の角度と次の角度を計算
        angle = 2.0 * 3.14159265 * float(i) / 7.0;
        angleNext = 2.0 * 3.14159265 * float(i + 1) / 7.0;

        // 現在の頂点と次の頂点を計算
        p = vec2(sin(angle), cos(angle));
        pNext = vec2(sin(angleNext), cos(angleNext));

        // バリセントリック座標を計算
        float alpha = ((p.y - pNext.y)*(position.x - pNext.x) + (pNext.x - p.x)*(position.y - pNext.y)) / ((p.y - pNext.y)*(p0.x - pNext.x) + (pNext.x - p.x)*(p0.y - pNext.y));
        float beta = ((pNext.y - p0.y)*(position.x - pNext.x) + (p0.x - pNext.x)*(position.y - pNext.y)) / ((p.y - pNext.y)*(p0.x - pNext.x) + (pNext.x - p.x)*(p0.y - pNext.y));
        float gamma = 1.0 - alpha - beta;

        // この三角形の内部に点が含まれているか判断
        if (alpha > 0.0 && beta > 0.0 && gamma > 0.0) {
            heptagonShape = 1.0;
            break;
        }
    }

    // グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * heptagonShape;

    vec4 color = vec4(0.0, uv, 1.0);
    gl_FragColor = color;
}

以下のようなエラーが出る場合、GLSL ES 3.00未満の環境で動作するように、七角形の描画コードを修正します。モジュロ演算子(%)の代わりにmod関数を使用します。

ERROR: 0:202: '%' : integer modulus operator supported in GLSL ES 3.00 and above only

また、以下のようなエラーが出る場合は、GLSL ES 2.00では、配列のインデックスとしてループ変数を使用することが許可されていないため、このエラーが発生しています。配列のインデックスにはコンパイル時に既知の定数値を使用する必要があります。

ERROR: 0:203: '[]' : Index expression must be constant

グラデーションありの八角形を作成する

// 八角形
void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);

    // 八角形の中心
    vec2 center = vec2(0.0, 0.0);

    // 八角形の内部に点が含まれているか判断
    float octagonShape = 0.0;
    float angle, angleNext;
    vec2 p, pNext;

    for(int i = 0; i < 8; ++i) {
        // 現在の角度と次の角度を計算
        angle = 2.0 * 3.14159265 * float(i) / 8.0;
        angleNext = 2.0 * 3.14159265 * float(i + 1) / 8.0;

        // 現在の頂点と次の頂点を計算
        p = center + vec2(cos(angle), sin(angle));
        pNext = center + vec2(cos(angleNext), sin(angleNext));

        // バリセントリック座標を計算
        float alpha = ((p.y - pNext.y)*(position.x - pNext.x) + (pNext.x - p.x)*(position.y - pNext.y)) / ((p.y - pNext.y)*(center.x - pNext.x) + (pNext.x - p.x)*(center.y - pNext.y));
        float beta = ((pNext.y - center.y)*(position.x - pNext.x) + (center.x - pNext.x)*(position.y - pNext.y)) / ((p.y - pNext.y)*(center.x - pNext.x) + (pNext.x - p.x)*(center.y - pNext.y));
        float gamma = 1.0 - alpha - beta;

        // この三角形の内部に点が含まれているか判断
        if (alpha > 0.0 && beta > 0.0 && gamma > 0.0) {
            octagonShape = 1.0;
            break;
        }
    }

    // グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * octagonShape;

    vec4 color = vec4(0.0, uv, 1.0);
    gl_FragColor = color;
}

八角形を中心から放射状に八つの三角形に分割する際、各三角形は八角形の中心と隣接する二つの頂点で構成されます。
これらの三角形の内部に現在のフラグメント(ピクセル)が含まれるかどうかをチェックすることで、八角形の形状を作り出します。
八角形の中心点をvec2(0.0, 0.0)とし、各頂点は中心から放射状に配置されるように計算しています。

十角形でも十二角形でもどんな多角形でも作成できるコード


上のコードであとは同じ繰り返しというのがわかるので、
以下のように実際の多角形の頂点数と最大超点数の数値を変えるだけでいろんな多角形を作成できるコードが出来上がります。

void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);

    // 中心点
    vec2 center = vec2(0.0, 0.0);

    // 多角形の最大頂点数(たとえば20とする)
    const int MAX_SIDES = 20;

    // 実際の多角形の頂点数(例として9を設定)
    int numSides = 9;

    // 多角形の内部に点が含まれているか判断
    float polygonShape = 0.0;
    float angle, angleNext;
    vec2 p, pNext;

    for(int i = 0; i < MAX_SIDES; ++i) {
        if(i < numSides) {
            // 現在の角度と次の角度を計算
            angle = 2.0 * 3.14159265 * float(i) / float(numSides);
            angleNext = 2.0 * 3.14159265 * float(i + 1) / float(numSides);

            // 現在の頂点と次の頂点を計算
            p = center + vec2(cos(angle), sin(angle));
            pNext = center + vec2(cos(angleNext), sin(angleNext));

            // バリセントリック座標を計算
            float alpha = ((p.y - pNext.y)*(position.x - pNext.x) + (pNext.x - p.x)*(position.y - pNext.y)) / ((p.y - pNext.y)*(center.x - pNext.x) + (pNext.x - p.x)*(center.y - pNext.y));
            float beta = ((pNext.y - center.y)*(position.x - pNext.x) + (center.x - pNext.x)*(position.y - pNext.y)) / ((p.y - pNext.y)*(center.x - pNext.x) + (pNext.x - p.x)*(center.y - pNext.y));
            float gamma = 1.0 - alpha - beta;

            // この三角形の内部に点が含まれているか判断
            if (alpha > 0.0 && beta > 0.0 && gamma > 0.0) {
                polygonShape = 1.0;
                break;
            }
        }
    }

    // グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * polygonShape;

    vec4 color = vec4(0.0, uv, 1.0);
    gl_FragColor = color;
}

例のコードでは九角形です。
GLSL ES 2.00 では、ループ内でのインデックス i を非定数表現と比較することが許可されていないため、エラーが発生しています。

ERROR: 0:293: 'i' : Loop index cannot be compared with non-constant expression

この問題を解決するためには、ループの限界をコンパイル時に既知の定数値に設定する必要があります。

多角形の頂点数を動的に設定するには、最大の頂点数を定数として定義し、実際の頂点数がこの最大値以下になるようにします。
この方法では、実際の頂点数が最大値より少ない場合、余分な繰り返しが行われますが、これは条件分岐で処理することができます。

タイルっぽく色をつける方法(floor関数)

void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);
    float cSize = length(position) * 1.5; // 円のサイズの調整
    float cLength = smoothstep(0.96, 1.0, cSize);
    float circle = 1.0 - cLength;
    //    グラデーdションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * circle;
    uv = floor(uv * 10.0) / 10.0; // floor小数点切り捨て
    vec4 color = vec4(0.0,uv, 1.0);
    // vec4 color = vec4(uv.y,uv, 1.0);
    gl_FragColor = color;
}

floor()という組み込み関数を使うと、小数点を切り捨てることができます。
そのため、0.8や0.6など1未満の数字はすべて0になります。
それだと線形補間された値のほとんどが0になるため、floor()に渡る前に、掛け算してあげます。
そして、同じ数値でやると、一定のの数値で切り捨て、切り上げを繰り返すことが可能になります。
それがタイルのような模様を実現できます。

基本的なGLSLの計算方法については、GLSLで使う数学(三角関数、ベクトル)を文系エンジニアのために実務でいるところだけ解説を参考にしてみてください。

二色を横方向にグラデーションさせる(mix関数)

// 円(二色横グラデーション模様)
void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);
    float cSize = length(position) * 1.5; // 円のサイズの調整
    float cLength = smoothstep(0.96, 1.0, cSize);
    float circle = 1.0 - cLength;

    // 色設定
    vec4 green = vec4(0.0,1.0,0.0, 1.0); // 緑
    vec4 blue = vec4(0.0,0.0,1.0, 1.0); // 青
    vec4 black = vec4(0.0, 0.0, 0.0, 1.0); // 黒

    //    グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * circle;
    // uv.x = 0の
    vec4 color = mix(green, blue, uv.x);

    // 背景が黒色になるように調整
    gl_FragColor = mix(black, color, circle);
}

使いたい色を指定して、mix()という関数を使います。
mixの具体的な値をそれぞれ0、0.5、1のときにみると以下のような計算になります。

uv.x = 0;
green * 1.0 + blue * 0.0
uv.x = 0.5;
green * 0.5 + blue * 0.5
uv.x = 1;
green * 0.0 + blue * 1.0

ちなみに縦方向に変更したい場合は、mixの最後の引数部分をyに変更すればOKです。

    vec4 color = mix(green, blue, uv.y);

二色を横方向に(グラデーションさせない)(step関数)

void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);
    float cSize = length(position) * 1.5; // 円のサイズの調整
    float cLength = smoothstep(0.999, 1.0, cSize);
    float circle = 1.0 - cLength;

    // 色設定
    vec4 green = vec4(0.0,1.0,0.0, 1.0); // 緑
    vec4 blue = vec4(0.0,0.0,1.0, 1.0); // 青
    vec4 black = vec4(0.0, 0.0, 0.0, 1.0); // 黒

    //    グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * circle;
    uv.x = step(0.5, uv.x);
    vec4 color = mix(green, blue, uv.x);

    // 背景が黒色になるように調整
    gl_FragColor = mix(black, color, circle);
}

step関数を使うことで、段階的に変化させることができます。上の場合、0.5を指定しているのでちょうど真ん中で色が変わります。

ここをたとえばy部分も変化させると4色になります。
背景を赤にして見てみると以下のようになります。

// 4色
void main() {
    vec2 position = vVertexPosition * vec2(2.0, 1.0);
    float cSize = length(position) * 1.5; // 円のサイズの調整
    float cLength = smoothstep(0.999, 1.0, cSize);
    float circle = 1.0 - cLength;

    // 色設定
    vec4 green = vec4(0.0,1.0,0.0, 1.0); // 緑
    vec4 blue = vec4(0.0,0.0,1.0, 1.0); // 青
    vec4 black = vec4(1.0, 0.0, 0.0, 1.0); // 赤

    //    グラデーションの調整
    vec2 uv = vVertexPosition / 2.0 + 0.5;
    uv = uv * circle;
    uv = step(0.5, uv);
    vec4 color = vec4(0.0, uv.xy, 1.0);

    // 背景が黒色になるように調整
    gl_FragColor = mix(black, color, circle);
}

円に画像を貼り付ける

   const geometry = new THREE.PlaneGeometry(20, 20);
   const material = new THREE.ShaderMaterial({
     uniforms: {
      //  uTex: { value: await loadTex('/img/original/rec1.jpg') }
       uTex: { value: await loadTex('/img/original/square.jpg') }
     },
     vertexShader: `
       varying vec2 vUv;

       void main() {
         vUv = uv;
         gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
       }
       `,
     fragmentShader: `
       varying vec2 vUv;
       uniform sampler2D uTex;

       void main() {
         vec2 centerPosition = vec2(0.5, 0.5);
         vec2 position = vUv;

         // centerPosition中央位置からの距離
         float distance = distance(position, centerPosition) * 4.5; // 円のサイズの調整
         float circle = 1.0 - smoothstep(0.96, 1.0, distance);
         vec4 texColor = texture(uTex, vUv);

         // 緑の円
         // vec4 color = vec4(0.0, circle, 0.0, 1.0);
         // 画像
         vec4 color = circle * texColor;

         gl_FragColor = color;
       }

       `,
     transparent: true,
   });

Three.jsのShaderMaterialを使ってGLSLを記述した画像の貼り方が知りたい場合は、【WebGL】Three.jsのShaderMaterialを使って画像や動画を出力する方法(デモ付き)でuniformsやUV座標など詳しく解説しているので参考にしてみてください。

UV座標は(0,0)から(1,1)のため、中央を設定して、それからdistance関数(中央からの距離を取得)で円のポジションを決めてあげます。
あとは、circleという変数が円のエリアになるので、それとそれぞれの座標の色情報をもっているtexColorをかけてあげれば円の部分の画像の色情報が決定できます。
あとはgl_FragColorにセットすればそれぞれのピクセルの位置ごとに画像の色情報がセットされることになり、画像が描写されます。

スポンサーリンク