From 114c2ef739b8d1ef68ccdc3efef1e9730ac18ca2 Mon Sep 17 00:00:00 2001 From: neozhaoliang Date: Thu, 21 Jan 2021 15:49:31 +0800 Subject: [PATCH] fix bug in polytope models.py and update escher tiling fragment code --- src/aperiodic-tilings/glsl/escher.frag | 391 +++++++++++++++---------- src/polytopes/polytopes/models.py | 5 +- 2 files changed, 242 insertions(+), 154 deletions(-) diff --git a/src/aperiodic-tilings/glsl/escher.frag b/src/aperiodic-tilings/glsl/escher.frag index a2131a5..cfc8b21 100644 --- a/src/aperiodic-tilings/glsl/escher.frag +++ b/src/aperiodic-tilings/glsl/escher.frag @@ -6,30 +6,124 @@ uniform float iTime; out vec4 FinalColor; -#define PI 3.141592653 -#define BIGF 100000.0 +/* +==================================================== + +Apeiodic tiling using de Bruijn's algebraic approach + + by Zhao Liang +==================================================== + +This shader is motivated by Greg Egan's javascript applet at + + http://gregegan.net/APPLETS/02/02.html + +Thanks Egan for explaining his idea to me! (and the comments in his code :) + +Also I learned a lot from Shane's wonderful examples. + +You can do whatever you want to this program. + +Below I'll give a brief sketch of the procedure used in this program, +for a detailed explanation please refer to + +"Algebraic theory of Penrose's non-periodic tilings of the plane". + N.G. de Bruijn. + +Also I found the book + +"Aperiodic Order, Volume 1", by Baake M., Grimm U., Penrose R. + +very helpful. + +Main steps: (use N=5 as example) + +1. We choose the five fifth roots of unity as grid directions. + Each direction will have a family of grid lines orthogonal to it and + has unit spacing between adjacent lines. + +2. We also choose five reals to shift each grid along its direction. + +3. Any intersection point P of two grid lines can be identified with four integers + (r, s, kr, ks), which means P is the intersection of the kr-th line in the r-th + grid and the ks-th line in the s-th grid. It must hold 0 <= r < s < 5. +4. Each intersection point P correspondes to an unique rhombus in the final tiling. Note + this rhombus does not necessarily contain P but a transformed version of P. -#define SOME_SOLID_FACES +5. For each pixel uv, we do a bit lengthy computation to find which rhombus its + transformed position lies in. +6. Color the rhombus acoording to its shape, the position, ... whatever you want. -// iq's hash function -float hash21(vec2 p) +7. We also draw a tunnel in each face and flip the tunnel randomly to make some cubes + look impossible. (a cube is formed by three rhombus meeting at an obtuse vertex) + Note this is different from Egan's applet, in there he carefully chose fixed flips + for each rhombus to make **every** cube look impossible. Our random flips here only + make **some** cubes look impossible. + +All suggestions are welcomed. +*/ + +// if you want some faces are closed and draw a cross bar on them +#define SOME_CLOSED_FACES + +// dimension of the grids +const int N = 5; + +// N real numbers for the shifts of the grids. +float[N] shifts; + +// directions of the grids, will be initialized in the beginning of the main function +vec2[N] grids; + +struct Rhombus { - return fract(sin(dot(p, vec2(141.13, 289.97))) * 43758.5453); -} + // r, s for the r-th and s-th grids. + int r; + int s; + // kr, ks for the lines in the two grids + float kr; + float ks; -// 2d cross product of two vectors. -// This is useful when we want to solve t for points p,q,v that p+t*q is parellel to -// v. Just use the fact ccross(p+tq, v) = 0 -> t = -ccross(p, v) / ccross(q, v) -float ccross(vec2 z, vec2 w) + // center and vertices of the rhombus + vec2 cen; + vec2[4] verts; + + // the vertices of the tunnels. Each tunnel contains two pieces. + vec2[4] inset1; + vec2[4] inset2; +}; + +#define PI 3.141592653 + +// init the grid directions, we choose the five fifth roots of unity +void init_grids() { - return z.x * w.y - z.y * w.x; + float theta; + for(int k=0; k t = -ccross(p, v) / ccross(q, v) +float ccross(vec2 p, vec2 q) { return p.x * q.y - p.y * q.x; } -// distance from a 2d point p to a 2d segment (a, b) +// iq's hash function, for randomly flip the tunnels and open/closed faces +float hash21(vec2 p) { return fract(sin(dot(p, vec2(141.13, 289.97))) * 43758.5453); } + +// distance from a 2d point p to a 2d segment (a, b) float dseg(vec2 p, vec2 a, vec2 b) { vec2 v = b - a; @@ -38,23 +132,21 @@ float dseg(vec2 p, vec2 a, vec2 b) return length(p - t * v); } - -// check if a point p is in the convex cone t*a+s*b where t and s are both non-negative. -// again we use cross product to do this +// check if a point p is in the convex cone C = { s*a+t*b | s,t >= 0 }. +// we assume a, b are not parallel: ccross(a, b) != 0. +// again we use cross product to do this. bool incone(vec2 p, vec2 a, vec2 b) { float cpa = ccross(p, a); - if (cpa == 0.0) return true; float cpb = ccross(p, b); - if (cpb == 0.0) return true; float cab = ccross(a, b); - return (sign(cpb) == sign(cab)) && (sign(cpa) == sign(-cab)); + float s = -cpa / cab, t = cpb / cab; + return (s >= 0.) && (t >= 0.); } - - -// check if a 2d point p is inside a convex quad with vertices stored in a -// array of 2d vectors. Here we assume the vertices are aranged in order, -// either in clockwise order of anti-clockwise order. + +// check if a 2d point p is inside a convex quad with vertices stored in an +// array of four 2d vectors. Here we assume the vertices are aranged in order: +// A--B--C--D, either clockwise or anti-clockwise. // we simple check if p-A is in the cone (B-A, D-A) and p-C is in the cone (B-C, D-C). bool inquad(vec2 p, vec2[4] verts) { @@ -62,9 +154,9 @@ bool inquad(vec2 p, vec2[4] verts) vec2 a = verts[1] - verts[0]; vec2 b = verts[3] - verts[0]; bool inab = incone(q, a, b); - + if (!inab) return false; - + q = p - verts[2]; a = verts[1] - verts[2]; b = verts[3] - verts[2]; @@ -72,8 +164,7 @@ bool inquad(vec2 p, vec2[4] verts) return inab; } - -// distance function from a 2d point p to a rhombus +// distance function from a 2d point p to a rhombus. // this is a signed distance version, points inside will return negative distances. float dquad(vec2 p, vec2[4] verts) { @@ -84,39 +175,16 @@ float dquad(vec2 p, vec2[4] verts) return inquad(p, verts) ? -dmin : dmin; } - -// shists of the grids -const float[5] shifts = float[5](0.5, 0.5, 0.5, 0.5, 0.5); - -// directions of the grids, will be initialized in the beginning of the main function -vec2[5] grids; - - -// init the grid directions, we choose the fifth roots of unity -void init_grids() -{ - float theta; - for(int k=0; k<5; k++) - { - theta = 2.0 * PI / 5.0 * float(k); - grids[k] = vec2(cos(theta), sin(theta)); - } -} - // project a vector p to the k-th grid, note each grid line is shifted so we need // to add the corresponding shifts. -float project_point_grid(vec2 p, int k) -{ - return dot(p, grids[k]) + shifts[k]; -} +float project_point_grid(vec2 p, int k) { return dot(p, grids[k]) + shifts[k]; } -// for a point p and for each 0<=k<5, p must lie between the (m,m+1)-th lines in the k-th -// grid for some integer m. -// we use a bool param `lower` to choose either return m or m+1. -float[5] get_point_index(in vec2 p, bool lower) +// for a point p and for each 0 <= k < 5, p must lie between the (m, m+1)-th lines in +// the k-th grid for some integer m. we use a bool param `lower` to choose return m or m+1. +float[N] get_point_index(in vec2 p, bool lower) { - float[5] index; - for(int k=0; k<5; k++) + float[N] index; + for(int k=0; k q, -// which rhombus q lies in. we simply iterate over all possible combinations. +// a bit lengthy computation to find after the transformation p --> q, +// which rhombus q lies in. we simply iterate over all possible combinations: +// for each pair 0 <= r < s < 5, we find (kr, ks) so that p lies in the (kr, kr+1) +// strip in the r-th grid and (ks, ks+1) strip in the s-th grid, and check which of +// the four rhombus (r, s, kr, ks), (r, s, kr, ks+1), (r, s, kr+1, ks), (r, s, kr+1, ks+1) +// contains q. +// Sadly due to float rounding errors, we have to search from (r, s, kr-1, ks-1). Rhombus get_mapped_rhombus(vec2 p, out vec2 q) { q = debruijn_transform(p); - float[5] pindex = get_point_index(p, true); + float[N] pindex = get_point_index(p, true); Rhombus rb; float kr, ks; vec2[4] verts; - for(int r=0; r<4; r++) + for(int r=0; r .5) + + if(rnd > .2) { q.xy = -q.xy; // randomly flip the tunnels to make some cubes look impossible col *= min(1.45 - dcen, .75); } else { col *= min(dcen + 0.75, .9); } - + // distance to the boundary of the face float dface = dquad(p, rb.verts); - // distance to the tunnel of the face - float tunnel = min(dquad(q, rb.inset1), dquad(q, rb.inset2)); - tunnel = max(tunnel, -(tunnel + 0.1)); + + // distance to the face border + float dborder = max(dface, -(dface + 0.006)); + + // 5 to the tunnel of the face + float dtunnel = min(dquad(q, rb.inset1), dquad(q, rb.inset2)); + // distance to the hole of the face, // we choose the size of the hole to half the width/height of the rhombus. // note each rhombus has unit side length, so sA is twice the distance // from the center to its four edges. - float hole = dface + sA / 4.0; + float dhole = dface + sA / 4.0; float dcross = 1e5; - -#ifdef SOME_SOLIDS_FACES + +#ifdef SOME_CLOSED_FACES + // really dirty, maybe should put into a function if(abs(rnd - 0.5) > .48) { - hole += 1e5; tunnel += 1e5; - vec2 vA = (rb.verts[0] + rb.cen) / 2.; - vec2 vB = (rb.verts[1] + rb.cen) / 2.; - vec2 vC = (rb.verts[2] + rb.cen) / 2.; - vec2 vD = (rb.verts[3] + rb.cen) / 2.; - dcross = dseg(p, vA, vB); - dcross = min(dcross, dseg(p, vB, vC)); - dcross = min(dcross, dseg(p, vC, vD)); - dcross = min(dcross, dseg(p, vD, vA)); - dcross = min(dcross, dseg(p, vA, vC)); - dcross = min(dcross, dseg(p, vB, vD)); + dhole += 1e5; dtunnel += 1e5; + dcross = min(dcross, getCross(p, rb)); } #endif - - // combine all distances - float dborder = max(dface, -(dface + 0.006)); - dborder = min(dborder, min(tunnel, hole)); - + // shade the tunnels by the type of the rhombus float shade; + float id = floor(hash21(vec2(float(rb.r), float(rb.s))) * 3.); + // qd is our shading direction - float qd = dot(q, rb.cen - rb.verts[2]); + int ind = cA >= 0. ? 0 : 1; + float qd = dot(q, rb.cen - rb.verts[ind]); if(id == 0.) shade = .8 - step(0., -sign(rnd - .5)*qd) * .3; else if(id == 1.) shade = .6 - step(0., -sign(rnd - .5)*qd) * .4; else - shade = .7 - step(0., -sign(rnd - .5)*qd) * .2; + shade = .7 - step(0., -sign(rnd - .5)*qd) * .4; + - col = mix(col, vec3(0.), (1. - smoothstep(0., .01, hole)) * .8); - col = mix(col, vec3(0.), (1. - smoothstep(0., .01, dborder)) * .95); + // draw the black hole + col = mix(col, vec3(0.), (1. - smoothstep(0., sf, dhole))); + + // draw the face border + col = mix(col, vec3(0.), (1. - smoothstep(0., sf*2., dborder)) * .95); + // shade the tunnels - col = mix(col, vec3(1.), (1. - smoothstep(0., .01, tunnel)) *shade); - // highlighting the edges - col = mix(col, col * 2., (1. - smoothstep(0., .02, dborder - .02))*.3); - // highlight crosses on solid faces - col = mix(col, vec3(0.), (1. - smoothstep(0., .01, dcross-0.005)) * 0.5); + col = mix(col, vec3(1.), (1. - smoothstep(0., sf, dtunnel)) * shade); + + // highlight the edges + col = mix(col, col * 2., (1. - smoothstep(0., sf*2., dborder-sf*2.)) * .3); + + // draw the crosses on solid faces + col = mix(col, vec3(0.), (1. - smoothstep(0., sf, dcross-sf/2.)) * .5); + + // adjust luminance of faces by their types col *= min(id / 2. + .5, 1.25); + // draw hatch lines on the face to add some decorate pattern. + // we get line direction first vec2 diag = (rb.verts[0] - rb.cen); float dd = cA < 0. ? dot(q, diag) : dot(q, vec2(-diag.y, diag.x)); - // some hatch lines to make the faces look like pencil work - float hatch = clamp(sin(dd * 60.*PI) * 2. + .5, 0., 1.); - float hrnd = hash21(floor(q * 400.* PI) + 0.73); + float hatch = clamp(sin(dd * 60. * PI) * 2. + .5, 0., 1.); + float hrnd = hash21(floor(q * 400. * PI) + 0.73); if (hrnd > 0.66) hatch = hrnd; + // we dont't want the hatch lines to show on top of the hole and tunnel - if (tunnel < 0.0 || hole < 0.0) hatch = 1.0; + if (dtunnel < 0.0 || dhole < 0.0) hatch = 1.0; col *= hatch *.1 + .85; - // some thin bouding box between the hole and the face border - col = mix(col, vec3(0.), (1. - smoothstep(0., .01, max(dface+0.1, -(dface+.1))))*.5); - uv = gl_FragCoord.xy / iResolution.xy; + // add a thin bounding box around the hole + col = mix(col, vec3(0.), (1. - smoothstep(0., sf*1.5, abs(dface + sA/8.)))*.5); + uv = gl_FragCoord.xy / iResolution.xy; col *= pow(16. * uv.x * uv.y * (1. - uv.x) * (1. - uv.y), .125) * .75 + .25; FinalColor = vec4(sqrt(max(col, 0.0)), 1.0); } diff --git a/src/polytopes/polytopes/models.py b/src/polytopes/polytopes/models.py index 3e2aded..b463637 100644 --- a/src/polytopes/polytopes/models.py +++ b/src/polytopes/polytopes/models.py @@ -286,7 +286,7 @@ class Snub(Polyhedra): def __init__(self, coxeter_diagram, init_dist=(1.0, 1.0, 1.0), extra_relations=()): super().__init__(coxeter_diagram, init_dist, extra_relations) - # the representaion is not in the form of a Coxeter group, + # the representation is not in the form of a Coxeter group, # we must overwrite the relations. # four generators (r, r^-1, s, s^-1) @@ -308,6 +308,9 @@ def __init__(self, coxeter_diagram, init_dist=(1.0, 1.0, 1.0), extra_relations=( # map the extra_relations expressed by reflections # into relations by (r, s). for extra_rel in extra_relations: + if len(extra_rel) % 2 == 1: + extra_rel += extra_rel + snub_rel = [] for x, y in zip(extra_rel[:-1:2], extra_rel[1::2]): snub_rel.extend(