|
| 1 | +// From https://openprocessing.org/sketch/2308573 |
| 2 | + |
| 3 | +import '../../types/global' |
| 4 | +// Fake matter.js import |
| 5 | +declare const Matter: any |
| 6 | + |
| 7 | +let engine: any |
| 8 | +let blobs: Blob[] = [] |
| 9 | +let metaballShader: p5.Shader |
| 10 | +let spheremap: p5.Image |
| 11 | +let renderer: p5.RendererGL |
| 12 | + |
| 13 | +async function setup() { |
| 14 | + spheremap = await loadImage('https://deckard.openprocessing.org/user67809/visual2181338/h987a85d77bacbc3b232fb87ce6fe440a/dusseldorf_bridge.jpg') |
| 15 | + renderer = createCanvas(600, 600, WEBGL) |
| 16 | + metaballShader = createShader(vert, frag) |
| 17 | + setupScene() |
| 18 | + blobs.push(new Blob(random(-1,1)*100, 50, 100, '#f3e17e')) |
| 19 | + blobs.push(new Blob(random(-1,1)*100, -150, 100, '#dd483c')) |
| 20 | + blobs.push(new Blob(random(-1,1)*100, -350, 50, '#4b8a5f')) |
| 21 | + blobs.push(new Blob(random(-1,1)*100, -550, 50, '#0d150b')) |
| 22 | +} |
| 23 | + |
| 24 | +function setupScene() { |
| 25 | + engine = Matter.Engine.create() |
| 26 | + |
| 27 | + const ground = Matter.Bodies.rectangle(0, height / 2 + 30, width, 60, { |
| 28 | + isStatic: true, |
| 29 | + }) |
| 30 | + const wallLeft = Matter.Bodies.rectangle(-width/2 - 30, 0, 60, 3 * height, { |
| 31 | + isStatic: true, |
| 32 | + }) |
| 33 | + const wallRight = Matter.Bodies.rectangle(width/2 + 30, 0, 60, 3 * height, { |
| 34 | + isStatic: true, |
| 35 | + }) |
| 36 | + Matter.World.add(engine.world, [ground, wallLeft, wallRight]) |
| 37 | +} |
| 38 | + |
| 39 | +function draw() { |
| 40 | + background('#faf8e2') |
| 41 | + // translate(width/2, height/2) |
| 42 | + |
| 43 | + for (const blob of blobs) { |
| 44 | + blob.update() |
| 45 | + } |
| 46 | + Matter.Engine.update(engine, 1000 / 60) |
| 47 | + |
| 48 | + for (const blob of blobs) { |
| 49 | + blob.drawBlob() |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +const BLOB_NODE_SIZE = 20 |
| 54 | +const BLOB_NODE_R = 15 |
| 55 | +const BLOB_NODE_AREA = Math.PI * BLOB_NODE_SIZE * BLOB_NODE_SIZE |
| 56 | + |
| 57 | +class Blob { |
| 58 | + c: p5.Color |
| 59 | + nodes: any[] |
| 60 | + springs: any[] |
| 61 | + tex: p5.Image |
| 62 | + |
| 63 | + constructor(x, y, r, c) { |
| 64 | + this.nodes = [] |
| 65 | + this.springs = [] |
| 66 | + this.c = color(c) |
| 67 | + this.tex = createImage(20, 20) |
| 68 | + this.tex.loadPixels() |
| 69 | + for (let i = 0; i < this.tex.pixels.length; i++) { |
| 70 | + this.tex.pixels[i] = 255 |
| 71 | + } |
| 72 | + // @ts-ignore |
| 73 | + renderer.getTexture(this.tex).setInterpolation(NEAREST, NEAREST) |
| 74 | + |
| 75 | + const a = PI * r * r |
| 76 | + const numBlobs = ceil(a / BLOB_NODE_AREA) |
| 77 | + |
| 78 | + while (this.nodes.length < numBlobs) { |
| 79 | + const rx = random(-r, r) |
| 80 | + const ry = random(-r, r) |
| 81 | + if (Math.hypot(rx, ry) > r) continue |
| 82 | + |
| 83 | + const vert = Matter.Bodies.circle(x + rx, y + ry, BLOB_NODE_R, { inertia: Infinity, friction: 0.015 }) |
| 84 | + this.nodes.push(vert) |
| 85 | + } |
| 86 | + |
| 87 | + Matter.World.add(engine.world, this.nodes) |
| 88 | + } |
| 89 | + |
| 90 | + bin(x: number, y: number) { |
| 91 | + return [round(x/80), round(y/80)] |
| 92 | + } |
| 93 | + |
| 94 | + nodeBin(node) { |
| 95 | + return this.bin(node.position.x, node.position.y) |
| 96 | + } |
| 97 | + |
| 98 | + adjacentBins(node) { |
| 99 | + const [x, y] = this.nodeBin(node) |
| 100 | + const bins: [number, number][] = [] |
| 101 | + for (const dx of [-1, 0, 1]) { |
| 102 | + for (const dy of [-1, 0, 1]) { |
| 103 | + bins.push([x + dx, y + dy]) |
| 104 | + } |
| 105 | + } |
| 106 | + return bins |
| 107 | + } |
| 108 | + |
| 109 | + binKey(bin) { |
| 110 | + return bin.join(',') |
| 111 | + } |
| 112 | + |
| 113 | + binnedNodes() { |
| 114 | + const bins = {} |
| 115 | + for (const node of this.nodes) { |
| 116 | + const binKey = this.binKey(this.nodeBin(node)) |
| 117 | + if (!bins[binKey]) { |
| 118 | + bins[binKey] = [] |
| 119 | + } |
| 120 | + bins[binKey].push(node) |
| 121 | + } |
| 122 | + return bins |
| 123 | + } |
| 124 | + |
| 125 | + update() { |
| 126 | + Matter.World.remove(engine.world, this.springs) |
| 127 | + this.springs = [] |
| 128 | + const bins = this.binnedNodes() |
| 129 | + for (const node of this.nodes) { |
| 130 | + const binsToCheck = this.adjacentBins(node) |
| 131 | + for (const bin of binsToCheck) { |
| 132 | + const key = this.binKey(bin) |
| 133 | + if (!bins[key]) continue |
| 134 | + for (const other of bins[key]) { |
| 135 | + if (other === node) continue |
| 136 | + this.springs.push(Matter.Constraint.create({ |
| 137 | + bodyA: node, |
| 138 | + pointA: { x: 0, y: 0 }, |
| 139 | + bodyB: other, |
| 140 | + pointB: { x: 0, y: 0 }, |
| 141 | + stiffness: map( |
| 142 | + Math.hypot(node.position.x - other.position.x, node.position.y - other.position.y), |
| 143 | + 0, 12*BLOB_NODE_SIZE, |
| 144 | + 0.02, 0.03, |
| 145 | + true |
| 146 | + ), |
| 147 | + damping: 0.001, |
| 148 | + // length: 0, |
| 149 | + length: max( |
| 150 | + 2 * BLOB_NODE_SIZE, |
| 151 | + Math.hypot(node.position.x - other.position.x, node.position.y - other.position.y) * 0.975 |
| 152 | + ), |
| 153 | + })) |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + Matter.World.add(engine.world, this.springs) |
| 158 | + } |
| 159 | + |
| 160 | + drawBlob() { |
| 161 | + const minX = Math.min(...this.nodes.map((n) => n.position.x)) - 4 * BLOB_NODE_SIZE |
| 162 | + const maxX = Math.max(...this.nodes.map((n) => n.position.x)) + 4 * BLOB_NODE_SIZE |
| 163 | + const minY = Math.min(...this.nodes.map((n) => n.position.y)) - 4 * BLOB_NODE_SIZE |
| 164 | + const maxY = Math.max(...this.nodes.map((n) => n.position.y)) + 4 * BLOB_NODE_SIZE |
| 165 | + const x = (maxX + minX)/2 |
| 166 | + const y = (maxY + minY)/2 |
| 167 | + const w = maxX - minX |
| 168 | + const h = maxY - minY |
| 169 | + |
| 170 | + this.nodes.forEach((node, i) => { |
| 171 | + this.tex.pixels[i * 4 + 0] = map(node.position.x, minX, maxX, 0, 255, true) |
| 172 | + this.tex.pixels[i * 4 + 1] = map(node.position.y, minY, maxY, 0, 255, true) |
| 173 | + }) |
| 174 | + this.tex.updatePixels() |
| 175 | + |
| 176 | + push() |
| 177 | + translate(x, y) |
| 178 | + noStroke() |
| 179 | + shader(metaballShader) |
| 180 | + metaballShader.setUniform('bbox', [minX, minY, maxX, maxY]) |
| 181 | + metaballShader.setUniform('k', BLOB_NODE_SIZE * 3) |
| 182 | + metaballShader.setUniform('numNodes', this.nodes.length) |
| 183 | + metaballShader.setUniform('data', this.tex) |
| 184 | + metaballShader.setUniform('r', BLOB_NODE_R) |
| 185 | + // TODO: make this a public API |
| 186 | + // @ts-ignore |
| 187 | + metaballShader.setUniform('c', this.c.array()) |
| 188 | + metaballShader.setUniform('spheremap', spheremap) |
| 189 | + plane(w, h) |
| 190 | + pop() |
| 191 | + } |
| 192 | + |
| 193 | + draw2D() { |
| 194 | + fill(this.c) |
| 195 | + stroke(this.c) |
| 196 | + strokeWeight(2 * BLOB_NODE_R) |
| 197 | + strokeJoin(ROUND) |
| 198 | + const hull = convexHull(this.nodes.map(n => n.position)) |
| 199 | + beginShape() |
| 200 | + for (const { x, y } of hull) vertex(x, y) |
| 201 | + endShape(CLOSE) |
| 202 | + |
| 203 | + noStroke() |
| 204 | + fill(0) |
| 205 | + for (const node of this.nodes) { |
| 206 | + circle(node.position.x, node.position.y, BLOB_NODE_R * 2) |
| 207 | + } |
| 208 | + } |
| 209 | +} |
| 210 | + |
| 211 | +let vert = `#version 300 es |
| 212 | +precision highp float; |
| 213 | +
|
| 214 | +in vec3 aPosition; |
| 215 | +in vec2 aTexCoord; |
| 216 | +
|
| 217 | +uniform mat4 uModelViewMatrix; |
| 218 | +uniform mat4 uProjectionMatrix; |
| 219 | +
|
| 220 | +out vec2 vTexCoord; |
| 221 | +
|
| 222 | +void main() { |
| 223 | + // Apply the camera transform |
| 224 | + vec4 viewModelPosition = |
| 225 | + uModelViewMatrix * |
| 226 | + vec4(aPosition, 1.0); |
| 227 | +
|
| 228 | + // Tell WebGL where the vertex goes |
| 229 | + gl_Position = |
| 230 | + uProjectionMatrix * |
| 231 | + viewModelPosition; |
| 232 | +
|
| 233 | + // Pass along data to the fragment shader |
| 234 | + vTexCoord = aTexCoord; |
| 235 | +}` |
| 236 | + |
| 237 | +let frag = `#version 300 es |
| 238 | +precision highp float; |
| 239 | +
|
| 240 | +in vec2 vTexCoord; |
| 241 | +out vec4 fragColor; |
| 242 | +
|
| 243 | +uniform sampler2D data; |
| 244 | +uniform sampler2D spheremap; |
| 245 | +uniform vec4 bbox; |
| 246 | +uniform int numNodes; |
| 247 | +uniform float k; |
| 248 | +uniform float r; |
| 249 | +uniform vec4 c; |
| 250 | +
|
| 251 | +float opSmoothUnion( float d1, float d2, float k ) |
| 252 | +{ |
| 253 | + float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 ); |
| 254 | + return mix( d2, d1, h ) - k*h*(1.0-h); |
| 255 | +} |
| 256 | +
|
| 257 | +vec2 nodeCoord(int i) { |
| 258 | + float x = fract(float(i)/20.) + 1./40.; |
| 259 | + float y = floor(float(i)/20.)/20. + 1./40.; |
| 260 | + vec2 pos = texture(data, vec2(x, y)).xy; |
| 261 | + return mix(bbox.xy, bbox.zw, pos); |
| 262 | +} |
| 263 | +
|
| 264 | +void main() { |
| 265 | + vec2 coord = mix(bbox.xy, bbox.zw, vTexCoord); |
| 266 | +
|
| 267 | + float dist = 100000.; |
| 268 | + float avg = 0.; |
| 269 | + float total = 0.; |
| 270 | + for (int i = 0; i < 400; i++) { |
| 271 | + if (i >= numNodes) break; |
| 272 | + |
| 273 | + float dist2 = length(coord - nodeCoord(i)) - r; |
| 274 | + avg += pow(dist2, 2.); |
| 275 | + total += 1.; |
| 276 | + dist = opSmoothUnion( |
| 277 | + dist, |
| 278 | + dist2, |
| 279 | + k |
| 280 | + ); |
| 281 | + // dist = min(dist, dist2); |
| 282 | + } |
| 283 | + avg /= total; |
| 284 | + // vec3 pos = vec3(coord, avg * 0.05); |
| 285 | + vec3 pos = vec3(coord, -40.*smoothstep(0., 2., pow(-dist, .1))); |
| 286 | +
|
| 287 | + vec3 normal = -normalize(cross(dFdx(pos), dFdy(pos))); |
| 288 | + |
| 289 | + vec3 fromCam = normalize(pos - vec3(0., 0., -800.0)); |
| 290 | + vec3 n = reflect( |
| 291 | + fromCam, |
| 292 | + normal |
| 293 | + ); |
| 294 | + float phi = acos( n.y ); |
| 295 | + float theta = 0.0; |
| 296 | + theta = acos(n.x / sin(phi)); |
| 297 | + float sinTheta = n.z / sin(phi); |
| 298 | + if (sinTheta < 0.0) { |
| 299 | + // Turn it into -theta, but in the 0-2PI range |
| 300 | + theta = 2.0 * 3.14159 - theta; |
| 301 | + } |
| 302 | + theta = theta / (2.0 * 3.14159); |
| 303 | + phi = phi / 3.14159 ; |
| 304 | + vec2 angles = vec2( fract(theta + 0.25), 1.0 - phi ); |
| 305 | +
|
| 306 | + vec3 lightDir = normalize(vec3(-0.3, 0.9, 0.)); |
| 307 | + float l = 0.15 * max(0., dot(lightDir, normal)) + 0.85; |
| 308 | + vec3 outColor = c.xyz * l + pow(texture(spheremap, angles).xyz, vec3(4.)); |
| 309 | + // outColor = vec3(smoothstep(0., 2., pow(-dist, .15))); |
| 310 | + // outColor = normal; |
| 311 | + |
| 312 | + fragColor = vec4(outColor, 1.) * (1. - smoothstep(0., 0.01, dist)); |
| 313 | +}` |
| 314 | + |
| 315 | +const comparison = (a: p5.Vector, b: p5.Vector) => { |
| 316 | + return a.x == b.x ? a.y - b.y : a.x - b.x |
| 317 | +} |
| 318 | + |
| 319 | +const cross = (a: p5.Vector, b: p5.Vector, o: p5.Vector) => { |
| 320 | + return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x) |
| 321 | +} |
| 322 | + |
| 323 | +function convexHull(points: p5.Vector[]) { |
| 324 | + points.sort(comparison) |
| 325 | + const L: p5.Vector[] = [] |
| 326 | + for (let i = 0; i < points.length; i++) { |
| 327 | + while ( |
| 328 | + L.length >= 2 && |
| 329 | + cross(L[L.length - 2], L[L.length - 1], points[i]) <= 0 |
| 330 | + ) { |
| 331 | + L.pop() |
| 332 | + } |
| 333 | + L.push(points[i]) |
| 334 | + } |
| 335 | + const U: p5.Vector[] = [] |
| 336 | + for (let i = points.length - 1; i >= 0; i--) { |
| 337 | + while ( |
| 338 | + U.length >= 2 && |
| 339 | + cross(U[U.length - 2], U[U.length - 1], points[i]) <= 0 |
| 340 | + ) { |
| 341 | + U.pop() |
| 342 | + } |
| 343 | + U.push(points[i]) |
| 344 | + } |
| 345 | + L.pop() |
| 346 | + U.pop() |
| 347 | + return L.concat(U) |
| 348 | +} |
0 commit comments