Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Created February 14, 2019 16:03
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mattdesl/7dd34f2bec4cdfa3d8c42295dcf1297f to your computer and use it in GitHub Desktop.
Save mattdesl/7dd34f2bec4cdfa3d8c42295dcf1297f to your computer and use it in GitHub Desktop.
A Browser and Node.js compatible canvas-sketch script for generative and parametric 3D geometry.

Generative Geometry in Browser + Node.js

Here is a script that can be run with canvas-sketch to generate OBJ files from a parametric/algorithmic 3D ThreeJS geometry.

Hitting "Cmd + S" from the canvas-sketch tool will export a PNG and OBJ file of the scene.

If the same script is run from Node, it will simply render the OBJ to stdout, or write to the filename argument if given.

// Require ThreeJS and utilities
global.THREE = require('three');
require('three/examples/js/exporters/OBJExporter');
require('three/examples/js/utils/GeometryUtils');
require('three/examples/js/controls/OrbitControls');
// Grab canvas-sketch utils
const canvasSketch = require('canvas-sketch');
const random = require('canvas-sketch-util/random');
// If run in a browser, this will return an empty object
const fs = require('fs');
// The browser uses an empty array here
const argv = process.argv.slice(2);
// We can run all this code in the browser too,
// e.g. we could visualize it as we tweak the algorithm
const isBrowser = typeof document !== 'undefined';
// Set a fixed seed so it always renders the same geometry
random.setSeed('256');
const settings = {
suffix: random.getSeed(),
dimensions: [ 1920, 1080 ],
scaleToView: true,
context: 'webgl',
animate: true,
attributes: {
antialias: true
}
};
// A browser sketch so we can iterate & visualize it without exporting each time
const sketch = ({ context }) => {
// Create a renderer
const renderer = new THREE.WebGLRenderer({
context
});
// WebGL background color
renderer.setClearColor('#fff', 1);
// Setup a camera
const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 100);
const orbitAngle = -45 * Math.PI / 180;
const orbitDistance = 5;
const orbitHeight = 5;
const orbitTranslate = new THREE.Vector3(-2, 0, 0);
camera.position.set(
Math.cos(orbitAngle) * orbitDistance,
orbitHeight,
Math.sin(orbitAngle) * orbitDistance
).add(orbitTranslate);
camera.lookAt(orbitTranslate);
// Setup camera controller
const controls = new THREE.OrbitControls(camera);
// Setup your scene
const scene = new THREE.Scene();
const geometry = generate();
const mesh = new THREE.Mesh(
geometry,
new THREE.MeshNormalMaterial({
flatShading: true
})
);
scene.add(mesh);
// draw each frame
return {
// Handle resize events here
resize ({ pixelRatio, viewportWidth, viewportHeight }) {
renderer.setPixelRatio(pixelRatio);
renderer.setSize(viewportWidth, viewportHeight);
camera.aspect = viewportWidth / viewportHeight;
camera.updateProjectionMatrix();
},
// Update & render your scene here
render ({ time, exporting }) {
controls.update();
renderer.render(scene, camera);
if (exporting) {
// Export both PNG and OBJ file
return [
context.canvas,
{ data: exportGeometry(geometry), extension: '.obj' }
];
}
},
// Dispose of events & renderer for cleaner hot-reloading
unload () {
controls.dispose();
renderer.dispose();
}
};
};
// The actual 'generative geometry' part
function generate () {
const geometry = new THREE.Geometry();
// this is our generative/algorithmic 3D code
const rings = 20;
const ringSpacing = 1 / rings * 2;
let ringRadius = ringSpacing;
for (let ringIndex = 0; ringIndex < rings; ringIndex++) {
const steps = 7 * (ringIndex + 1);
const A = ringIndex / Math.max(1, rings - 1);
const radius = ringRadius;
ringRadius += ringSpacing;
for (let i = 0; i < steps; i++) {
const B = i / Math.max(1, steps - 1);
const angle = (i / steps) * Math.PI * 2;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const thickness = ringSpacing * 0.15;
const height = 0.5 * A * B * random.range(0.25, 3);
const length = A * B * random.range(0.25, 0.5);
// in this case we will build a geometry made of many smaller
// parts, using geometry.merge()
const chunk = new THREE.BoxGeometry(length, height, thickness);
chunk.translate(0, height / 2, 0);
chunk.rotateX(Math.PI / 2 + -angle * 0.15);
const object = new THREE.Object3D();
object.position.set(x, 0, z);
object.rotation.y = -angle;
object.updateMatrix();
// merge in the geometry with the desired matrix
geometry.merge(chunk, object.matrix);
// clean it up after
chunk.dispose();
}
}
// re-center the whole geometry along XZ axis
geometry.computeBoundingBox();
const out = new THREE.Vector3();
const offset = geometry.boundingBox.getCenter(out);
out.negate();
geometry.translate(offset.x, 0, offset.z);
return geometry;
}
// This generates an OBJ file from the geometry
// In Node.js it can write it to a file or stdout,
// In browser it simply returns the string for canvas-sketch to export
function exportGeometry (geometry) {
const file = argv[0];
const object = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial());
const scene = new THREE.Scene();
scene.add(object);
object.updateMatrixWorld(true);
const exporter = new THREE.OBJExporter();
const result = exporter.parse(object);
if (file && !isBrowser) {
// write to file
try {
console.error('Writing to file', file);
fs.writeFileSync(file, result);
} catch (err) {
console.error('Error:', err.message);
}
} else {
// write to stdout
if (!isBrowser) console.log(result);
}
scene.remove(object);
return result;
}
if (isBrowser) {
canvasSketch(sketch, settings);
} else {
exportGeometry(generate());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment