|
// 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()); |
|
} |