❇️

GLSLで17の平面繰り返しパターン

2024/11/14に公開

二次元平面上に展開する繰り返しパターンは17のタイプに分類できると言われています。この記事ではGLSLを用いてシェーダーで17の繰り返しパターンをそれぞれ実装をしていきます。

17の平面繰り返しパターン

p1 (単純な並進)

vec2 patternP1(vec2 p, vec2 motifSize, float offset) {
    vec2 q = vec2(
        mod(p.x - motifSize.x * floor(p.y / motifSize.y) * offset, motifSize.x),
        mod(p.y, motifSize.y)
    );
    return q;
}

引数motifSizeは繰り返しパターンのモチーフに使用する四角形の大きさ、引数offsetはX方向のずれの大きさで[0, 1]の範囲の値を指定します。

vec2 q = patternP1(p, vec2(0.5, 1.0), 0.5);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.1, 0.1), vec2(0.4, 0.1), vec2(0.1, 0.9));
gl_FragColor = vec4(vec3(v), 1.0);

pm (一方向の鏡映)

vec2 patternPm(vec2 p, vec2 motifSize) {
    vec2 q = mod(p, motifSize);
    q.x = mod(p.x, motifSize.x * 2.0) < motifSize.x ? q.x : motifSize.x - q.x;
    return q;
}

引数motifSizeは繰り返しパターンのモチーフに使用する四角形の大きさを指定します。

vec2 q = patternPm(p, vec2(0.5, 1.0));
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.1, 0.1), vec2(0.4, 0.1), vec2(0.1, 0.9));
gl_FragColor = vec4(vec3(v), 1.0);

pg (一方向のすべり鏡映)

vec2 patternPg(vec2 p, vec2 motifSize, float glind) {
    vec2 q = p;
    q.x = mod(q.x, motifSize.x);
    if (mod(p.x, motifSize.x * 2.0) >= motifSize.x) {
        q.y += motifSize.y * glind;
        q.x = motifSize.x - q.x;
    }
    q.y = mod(q.y, motifSize.y);
    return q;
}

引数motifSizeは繰り返しパターンのモチーフに使用する四角形の大きさ、引数glindはすべり鏡映の移動量の大きさで[0, 1]の範囲の値を指定します。

vec2 q = patternPg(p, vec2(0.5, 1.0), 0.5);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.1, 0.1), vec2(0.4, 0.1), vec2(0.1, 0.9));    
gl_FragColor = vec4(vec3(v), 1.0);

cm (一方向の対角、他方向に鏡映)

vec2 patternCm(vec2 p, vec2 cellSize) {
    vec2 q = diamondTiling(p, cellSize.x, cellSize.y);
    q.x = q.x < 0.5 * cellSize.x ? 0.5 * cellSize.x - q.x : q.x - 0.5 * cellSize.x;
    return q;
}

引数cellSizeは繰り返しパターンのベースになるひし形格子の大きさを指定します。

vec2 q = patternCm(p, vec2(2.0, 1.0));
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.05, 0.2), vec2(0.8, 0.5), vec2(0.05, 0.8));
gl_FragColor = vec4(vec3(v), 1.0);

p2 (2回割の回転)

vec2 patternP2(vec2 p, vec2 motifSize, float center) {
    vec2 q = p;
    q.x = mod(q.x, motifSize.x);
    if (mod(p.x, motifSize.x * 2.0) >= motifSize.x) {
        q.y += motifSize.y * center;
        q.x = motifSize.x - q.x;
        q.y *= -1;
    }
    q.y = mod(q.y, motifSize.y);
    return q;
}

引数motifSizeは繰り返しパターンのモチーフに使用する四角形の大きさ、引数centerは回転の中心位置で[0, 1]の範囲の値を指定します。

vec2 q = patternP2(p, vec2(0.5, 1.0), 0.5);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.1, 0.1), vec2(0.4, 0.1), vec2(0.1, 0.9));
gl_FragColor = vec4(vec3(v), 1.0);

pmm (二方向の鏡映)

vec2 patternPmm(vec2 p, vec2 motifSize) {
    vec2 q = mod(p, motifSize);
    q.x = mod(p.x, motifSize.x * 2.0) < motifSize.x ? q.x : motifSize.x - q.x;
    q.y = mod(p.y, motifSize.y * 2.0) < motifSize.y ? q.y : motifSize.y - q.y;
    return q;
}

引数motifSizeは繰り返しパターンのモチーフに使用する四角形の大きさを指定します。

vec2 q = patternPmm(p, vec2(0.5, 1.0));
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.1, 0.1), vec2(0.4, 0.1), vec2(0.1, 0.9));
gl_FragColor = vec4(vec3(v), 1.0);

cmm (2本の対角線の両方に鏡映)

vec2 patternCmm(vec2 p, vec2 cellSize) {
    vec2 q = diamondTiling(p, cellSize.x, cellSize.y);
    q.x = q.x < 0.5 * cellSize.x ? 0.5 * cellSize.x - q.x : q.x - 0.5 * cellSize.x;
    q.y = q.y < 0.5 * cellSize.y ? 0.5 * cellSize.y - q.y : q.y - 0.5 * cellSize.y;
    return q;
}

引数cellSizeは繰り返しパターンのベースになるひし形格子の大きさを指定します。

vec2 q = patternCmm(p, vec2(2.0, 1.0));
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.05, 0.05), vec2(0.8, 0.05), vec2(0.05, 0.3));
gl_FragColor = vec4(vec3(v), 1.0);

pgg (二方向のすべり鏡映)

vec2 patternPgg(vec2 p, vec2 motifSize, float glind) {
    vec2 q = p;
    if (mod(p.y, motifSize.y * 2.0) >= motifSize.y) {
        q.x -= motifSize.x * glind * 2.0;
        q.y = motifSize.y - q.y;
    }
    vec2 r = q;
    q.x = mod(q.x, motifSize.x);
    if (mod(r.x, motifSize.x * 2.0) >= motifSize.x) {
        q.x = motifSize.x - q.x;
        q.y *= -1;
    }
    q.y = mod(q.y, motifSize.y);
    return q;
}

引数motifSizeは繰り返しパターンのモチーフに使用する四角形の大きさ、引数glindはすべり鏡映の移動量の大きさで[0, 1]の範囲の値を指定します。

vec2 q = patternPgg(p, vec2(0.5, 1.0), 0.5);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.1, 0.1), vec2(0.4, 0.1), vec2(0.1, 0.9));
gl_FragColor = vec4(vec3(v), 1.0);

pmg (一方向の鏡映、他方向にすべり鏡映)

vec2 patternPmg(vec2 p, vec2 motifSize, float glind) {
    vec2 q = p;
    q.y = mod(p.y, motifSize.y);
    if (mod(p.y, motifSize.y * 2.0) >= motifSize.y) {
        q.y = motifSize.y - q.y;
        q.x -= motifSize.x * glind * 2.0;
    }
    vec2 r = q;
    r.x = mod(q.x, motifSize.x);
    if (mod(q.x, motifSize.x * 2.0) >= motifSize.x) {
        r.x = motifSize.x - r.x;
    }
    return r;
}

引数motifSizeは繰り返しパターンのモチーフに使用する四角形の大きさ、引数glindはすべり鏡映の移動量の大きさで[0, 1]の範囲の値を指定します。

vec2 q = patternPmg(p, vec2(0.5, 1.0), 0.5);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.1, 0.1), vec2(0.4, 0.1), vec2(0.1, 0.9));
gl_FragColor = vec4(vec3(v), 1.0);

p3 (3回割の回転)

vec2 patternP3(vec2 p, float cellSize) {
    const float angleStep = 2.0 * PI / 3.0;
    const vec2 centerHexagon = vec2(sqrt(3) / 2, 1.0);
    vec2 q = equilateralHexagonTiling(p / cellSize);
    q -= centerHexagon;
    float a = atan(q.x, q.y);
    a = floor(a / angleStep) * angleStep;
    q *= rotate(-a);
    q *= cellSize;
    return q;
}

引数cellSizeは繰り返しパターンのベースになる六角形格子の一辺の大きさを指定します。

vec2 q = patternP3(p, 1.0);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.05, 0.0), vec2(0.8, -0), vec2(0.1, 0.8));
gl_FragColor = vec4(vec3(v), 1.0);

p31m (3回割の回転と鏡映)

vec2 patternP31m(vec2 p, float cellSize) {
    const float angleStep = 2.0 * PI / 3.0;
    const vec2 centerHexagon = vec2(sqrt(3) / 2, 1.0);
    vec2 q = equilateralHexagonTiling(p.yx / cellSize);
    q -= centerHexagon;
    float a = atan(q.x, q.y) + PI / 6;
    q = length(q) * vec2(sin(a), cos(a));
    a = floor(a / angleStep) * angleStep;
    q *= rotate(-a);
    q *= cellSize;
    float b = atan(q.x, q.y);
    float r = length(q);
    b = b < 0.5 * angleStep ? b : angleStep - b;
    q = r * vec2(sin(b), cos(b));
    return q;   
}

引数cellSizeは繰り返しパターンのベースになる六角形格子の一辺の大きさを指定します。

vec2 q = patternP31m(p, 1.0);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.05, 0.05), vec2(0.5, 0.4), vec2(0.1, 0.8));
gl_FragColor = vec4(vec3(v), 1.0);

p3m1 (3回割の回転と鏡映)

vec2 patternP3m1(vec2 p, float cellSize) {
    const float angleStep = 2.0 * PI / 3.0;
    const vec2 centerHexagon = vec2(sqrt(3) / 2, 1.0);
    vec2 q = equilateralHexagonTiling(p / cellSize);
    q -= centerHexagon;
    float a = atan(q.x, q.y);
    a = floor(a / angleStep) * angleStep;
    q *= rotate(-a);
    q *= cellSize;
    float b = atan(q.x, q.y);
    float r = length(q);
    b = b < 0.5 * angleStep ? b : angleStep - b;
    q = r * vec2(sin(b), cos(b));
    return q;   
}

引数cellSizeは繰り返しパターンのベースになる六角形格子の一辺の大きさを指定します。

vec2 q = patternP3m1(p, 1.0);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.05, 0.05), vec2(0.4, 0.4), vec2(0.1, 0.8));
gl_FragColor = vec4(vec3(v), 1.0);

p4 (4回割の回転)

vec2 patternP4(vec2 p, float cellSize) {
    const float angleStep = PI / 2.0;
    vec2 q = mod(p, cellSize);
    q -= 0.5 * cellSize;
    float a = atan(q.y, q.x);
    a = floor(a / angleStep) * angleStep;
    q *= rotate(a);
    return q;
}

引数cellSizeは繰り返しパターンのベースになる四角形格子の一辺の大きさを指定します。

vec2 q = patternP4(p, 1.0);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.05, 0.05), vec2(0.5, 0.05), vec2(0.05, 0.3));
gl_FragColor = vec4(vec3(v), 1.0);

p4m (4回割の回転と鏡映)

vec2 patternP4m(vec2 p, float cellSize) {
    const float angleStep = PI / 2.0;
    vec2 q = mod(p, cellSize);
    q -= 0.5 * cellSize;
    float a = atan(q.y, q.x);
    a = floor(a / (2.0 * PI / 4)) * (2.0 * PI / 4);
    q *= rotate(a);
    float b = atan(q.x, q.y);
    float r = length(q);
    b = b < 0.5 * angleStep ? b : angleStep - b;
    q = r * vec2(sin(b), cos(b));
    return q;
}

引数cellSizeは繰り返しパターンのベースになる四角形格子の一辺の大きさを指定します。

vec2 q = patternP4m(p, 1.0);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.0, 0.0), vec2(0.4, 0.45), vec2(0.1, 0.35));
gl_FragColor = vec4(vec3(v), 1.0);

p4g (4回割の回転とすべり鏡映)

vec2 patternP4g(vec2 p, float cellSize) {
    const float angleStep = PI / 2.0;
    vec2 q = mod(p, cellSize);
    q.x = mod(p.x, cellSize * 2.0) < cellSize ? q.x : cellSize - q.x;
    q.y = mod(p.y, cellSize * 2.0) < cellSize ? q.y : cellSize - q.y;
    q -= 0.5 * cellSize;
    float a = atan(q.y, q.x);
    a = floor(a / angleStep) * angleStep;
    q *= rotate(a);
    return q;
}

引数cellSizeは繰り返しパターンのベースになる四角形格子の一辺の大きさを指定します。

vec2 q = patternP4g(p, 1.0);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.05, 0.05), vec2(0.5, 0.05), vec2(0.05, 0.3));
gl_FragColor = vec4(vec3(v), 1.0);

p6 (6回割の回転)

vec2 patternP6(vec2 p, float cellSize) {
    const float angleStep = PI / 3.0;
    const vec2 centerHexagon = vec2(sqrt(3) / 2, 1.0);
    vec2 q = equilateralHexagonTiling(p / cellSize);
    q -= centerHexagon;
    float a = atan(q.x, q.y);
    a = floor(a / angleStep) * angleStep;
    q *= rotate(-a);
    q *= cellSize;
    return q;
}

引数cellSizeは繰り返しパターンのベースになる六角形格子の一辺の大きさを指定します。

vec2 q = patternP6(p, 1.0);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.1, 0.1), vec2(0.4, 0.4), vec2(0.1, 0.9));
gl_FragColor = vec4(vec3(v), 1.0);

p6m (6回割の回転と鏡映方向の対角、他方向に鏡映)

vec2 patternP6m(vec2 p, float cellSize) {
    const float angleStep = PI / 3.0;
    const vec2 centerHexagon = vec2(sqrt(3) / 2, 1.0);
    vec2 q = equilateralHexagonTiling(p / cellSize);
    q -= centerHexagon;
    float a = atan(q.x, q.y);
    a = floor(a / angleStep) * angleStep;
    q *= rotate(-a);
    q *= cellSize;
    float b = atan(q.x, q.y);
    float r = length(q);
    b = b < 0.5 * angleStep ? b : angleStep - b;
    q = r * vec2(sin(b), cos(b));
    return q;
}

引数cellSizeは繰り返しパターンのベースになる六角形格子の一辺の大きさを指定します。

vec2 q = patternP6m(p, 1.0);
gl_FragColor = vec4(q, 0.0, 1.0);

float v = drawTriangle(q, vec2(0.02, 0.1), vec2(0.2, 0.45), vec2(0.05, 0.9));
gl_FragColor = vec4(vec3(v), 1.0);

補足

繰り返しパターンの実装で使用しているマクロ変数や独自定義した関数です。

#define PI 3.14159265359

mat2 rotate(float r) {
    float c = cos(r);
    float s = sin(r);
    return mat2(c, s, -s, c);
}

// https://iquilezles.org/articles/distfunctions2d/
float sdTriangle( in vec2 p, in vec2 p0, in vec2 p1, in vec2 p2 )
{
    vec2 e0 = p1-p0, e1 = p2-p1, e2 = p0-p2;
    vec2 v0 = p -p0, v1 = p -p1, v2 = p -p2;
    vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 );
    vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 );
    vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 );
    float s = sign( e0.x*e2.y - e0.y*e2.x );
    vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
                     vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
                     vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
    return -sqrt(d.x)*sign(d.y);
}

float drawTriangle(vec2 p, vec2 p0, vec2 p1, vec2 p2) {
    float d = sdTriangle(p, p0, p1, p2);
    return step(0.0, d);   
}

vec2 diamondTiling(vec2 p, float width, float height) {
    float h = 0.5 * height;
    vec2 q = p;
    float skew = 0.5 * width / h;
    q.x -= q.y * skew;
    q = vec2(mod(q.x, width), mod(q.y, h));
    if (q.x / width + q.y / h >= 1.0) {
        q.x -= 0.5 * width;
    } else {
        q.y += h;
    }
    q.x += mod(q.y, h) * skew;
    return q;
}

vec2 equilateralHexagonTiling(vec2 p) {
    const float WIDTH = sqrt(3);
    const float HALF_WIDTH = 0.5 * WIDTH;
    const float TRIANGLE_HEIGHT = 0.5;
    const float SIDE_LENGTH = 1.0;
    const float ROW_HEIGHT = SIDE_LENGTH + TRIANGLE_HEIGHT;
    const float TRIANGLE_SKEW = sqrt(3);

    vec2 q = p;
    bool isEvenRow = int(mod(floor(q.y / ROW_HEIGHT), 2.0)) == 0;
    q.y = mod(q.y, ROW_HEIGHT);
    
    if (q.y < TRIANGLE_HEIGHT) { // top and bottom triangles
        q.x += isEvenRow ? HALF_WIDTH : 0;
        q.x -= TRIANGLE_SKEW * q.y;
        q.x = mod(q.x, WIDTH);
        if (q.x / WIDTH + q.y / TRIANGLE_HEIGHT < 1.0) { // top triangle
            q.y += ROW_HEIGHT;
        } else { // bottom triangle
            q.x -= HALF_WIDTH;
        }
        q.x += TRIANGLE_SKEW * mod(q.y, ROW_HEIGHT);
    } else { // center rectangle
        q.x -= !isEvenRow ? HALF_WIDTH : 0;
        q.x = mod(q.x, WIDTH);
    }

    return q;
}

diamondTiling関数、equilateralHexagonTiling関数はそれぞれ別記事にしてますので、そちらを参照ください。
https://zenn.dev/aadebdeb/articles/a80d91ec889e07
https://zenn.dev/aadebdeb/articles/8b6cc2543073a7

参考

Discussion