├── .gitignore ├── LICENSE ├── README.md ├── growth.png ├── index.js └── worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules/* 16 | *.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2013 Mikola Lysenko 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Differential growth 2 | 3 | creative-dev weekly challenge, a homage to @inconvergent's experiments 4 | 5 | 6 | -------------------------------------------------------------------------------- /growth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikolalysenko/differential-growth/e7dc42b406fa5d4e96b691e4492f1e00ca44da33/growth.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const regl = require('regl')() 2 | const spawnWorker = require('webworkify') 3 | const lookAt = require('gl-mat4/lookAt') 4 | const perspective = require('gl-mat4/perspective') 5 | 6 | const meshWorker = spawnWorker(require('./worker')) 7 | 8 | const positionBuffer = regl.buffer({usage: 'dynamic'}) 9 | const normalBuffer = regl.buffer({usage: 'dynamic'}) 10 | const cellBuffer = regl.elements({usage: 'dynamic'}) 11 | 12 | let targetRadius = 100.0 13 | let radius = 100.0 14 | 15 | meshWorker.addEventListener('message', ({data: { 16 | radius, positions, normals, cells 17 | }}) => { 18 | targetRadius = radius * 2 + 10 19 | positionBuffer({data: positions, usage: 'dynamic'}) 20 | normalBuffer({data: normals, usage: 'dynamic'}) 21 | cellBuffer({data: cells, usage: 'dynamic'}) 22 | }) 23 | 24 | const drawMesh = regl({ 25 | frag: ` 26 | precision mediump float; 27 | varying vec3 fragNormal; 28 | void main () { 29 | gl_FragColor = vec4(0.5 + 0.5 * fragNormal, 1); 30 | }`, 31 | 32 | vert: ` 33 | precision mediump float; 34 | attribute vec3 position, normal; 35 | uniform mat4 projection, view; 36 | varying vec3 fragNormal; 37 | void main () { 38 | fragNormal = normal; 39 | gl_Position = projection * view * vec4(position, 1); 40 | }`, 41 | 42 | context: { 43 | t: (props, {time}) => time 44 | }, 45 | 46 | attributes: { 47 | position: positionBuffer, 48 | normal: normalBuffer 49 | }, 50 | 51 | uniforms: { 52 | projection: (props, {viewportWidth, viewportHeight}) => 53 | perspective([], 54 | Math.PI / 4.0, 55 | viewportWidth / viewportHeight, 56 | 0.01, 57 | 1000.0), 58 | 59 | view: (props, {t}) => 60 | lookAt([], 61 | [radius * Math.cos(t), 0, radius * Math.sin(t)], 62 | [0, 0, 0], 63 | [0, 1, 0]) 64 | }, 65 | 66 | elements: cellBuffer, 67 | 68 | primitive: 'triangles' 69 | }) 70 | 71 | regl.frame(() => { 72 | regl.clear({ 73 | color: [0, 0, 0, 1], 74 | depth: 1 75 | }) 76 | 77 | radius = 0.8 * radius + 0.2 * targetRadius 78 | 79 | drawMesh() 80 | }) 81 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | /* globals self */ 2 | const seed = require('conway-hart')('I') 3 | const calcNormals = require('angle-normals') 4 | const calcCurvature = require('mesh-mean-curvature') 5 | const refineMesh = require('refine-mesh') 6 | const getBounds = require('bound-points') 7 | const allPairs = require('n-body-pairs')(3, 1024) 8 | 9 | const ITERS = 20 10 | const REPEL_RADIUS = 1.0 11 | const EDGE_LENGTH = REPEL_RADIUS 12 | const GROWTH_RATE = 0.005 13 | const REPEL_STRENGTH = 0.005 14 | 15 | function sendMesh (mesh) { 16 | const bounds = getBounds(mesh.positions) 17 | self.postMessage({ 18 | radius: Math.max( 19 | Math.abs(bounds[0][0]), 20 | Math.abs(bounds[0][2]), 21 | Math.abs(bounds[1][0]), 22 | Math.abs(bounds[1][2])), 23 | positions: mesh.positions, 24 | normals: calcNormals(mesh.cells, mesh.positions), 25 | cells: mesh.cells 26 | }) 27 | } 28 | 29 | function update (mesh) { 30 | // push along curvature 31 | const meanCurvature = calcCurvature(mesh.cells, mesh.positions) 32 | const normals = calcNormals(mesh.cells, mesh.positions) 33 | 34 | // compute edges 35 | const edges = {} 36 | mesh.cells.forEach((c) => { 37 | for (let i = 0; i < 3; ++i) { 38 | for (let j = 0; j < 3; ++j) { 39 | edges[i + ',' + j] = true 40 | } 41 | } 42 | }) 43 | 44 | // repel all non-adjacent vertices 45 | const forces = new Float32Array(3 * mesh.positions.length) 46 | for (let steps = 0; steps < ITERS; ++steps) { 47 | allPairs(mesh.positions, REPEL_RADIUS, (i, j, d2) => { 48 | // skip adjacent points 49 | if (edges[i + ',' + j]) { 50 | return 51 | } 52 | const p = mesh.positions[i] 53 | const q = mesh.positions[j] 54 | for (let d = 0; d < 3; ++d) { 55 | const r = (p[d] - q[d]) / (0.1 + d2) 56 | forces[3 * i + d] += r 57 | forces[3 * j + d] -= r 58 | } 59 | }) 60 | for (let i = 0; i < mesh.positions.length; ++i) { 61 | const p = mesh.positions[i] 62 | const H = meanCurvature[i] 63 | const N = normals[i] 64 | const W = GROWTH_RATE * H * Math.random() * Math.random() 65 | for (let d = 0; d < 3; ++d) { 66 | p[d] += REPEL_STRENGTH * forces[3 * i + d] + W * N[d] 67 | forces[3 * i + d] = 0 68 | } 69 | } 70 | } 71 | 72 | // refine 73 | return refineMesh(mesh.cells, mesh.positions, calcNormals(mesh.cells, mesh.positions), { 74 | edgeLength: EDGE_LENGTH 75 | }) 76 | } 77 | 78 | module.exports = () => { 79 | let mesh = seed 80 | function step () { 81 | sendMesh(mesh) 82 | mesh = update(mesh) 83 | setTimeout(step, 500) 84 | } 85 | step() 86 | } 87 | --------------------------------------------------------------------------------