Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Created February 14, 2019 16:03
Show Gist options
  • Save mattdesl/7dd34f2bec4cdfa3d8c42295dcf1297f to your computer and use it in GitHub Desktop.
Save mattdesl/7dd34f2bec4cdfa3d8c42295dcf1297f to your computer and use it in GitHub Desktop.

Revisions

  1. mattdesl created this gist Feb 14, 2019.
    7 changes: 7 additions & 0 deletions about.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    # Generative Geometry in Browser + Node.js

    Here is a script that can be run with [canvas-sketch](https://github.com/mattdesl/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.
    185 changes: 185 additions & 0 deletions obj-export.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,185 @@
    // 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());
    }