Skip to content

Instantly share code, notes, and snippets.

@apokellypse
Created July 17, 2019 02:16
Show Gist options
  • Save apokellypse/73072051786bf9905e382a14a4d72c00 to your computer and use it in GitHub Desktop.
Save apokellypse/73072051786bf9905e382a14a4d72c00 to your computer and use it in GitHub Desktop.
three.js bunny cross-sectioning web app
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - loaders - OBJ loader</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body {
font-family: Monospace;
background-color: #000;
color: #fff;
margin: 0px;
overflow: hidden;
}
#info {
color: #fff;
position: absolute;
top: 10px;
width: 100%;
text-align: center;
z-index: 100;
display:block;
}
#info a, .button { color: #f00; font-weight: bold; text-decoration: underline; cursor: pointer }
</style>
</head>
<body>
<div id="info">
<a href="http://threejs.org" target="_blank" rel="noopener">three.js</a> - OBJLoader test
</div>
<!-- assign these steps to different groups for easy visibility toggling -->
<!--draw intersecting planes along height,
draw intersection points where planes intersect faces
draw lines that connect these points
connect lines that form closed loop and rm other lines
extrude these closed loops -->
<button id="startDrawIntersectionPoints" style="position: absolute; height: 20px;">
Draw Intersection Points
</button>
<button id="startCreateSlices" style="position: absolute; height: 20px; top: 20px;">
Create Slices
</button>
<button id="startNextSlice" style="position: absolute; height: 20px; top: 50px;">
Next Slice
</button>
<button id="startPreviousSlice" style="position: absolute; height: 20px; top: 70px;">
Previous Slice
</button>
<button id="startRemoveBunny" style="position: absolute; height: 20px; top: 100px;">
Hide Bunny
</button>
<button id="startShowBunny" style="position: absolute; height: 20px; top: 120px;">
Show Bunny
</button>
<button id="startSaveLink" style="position: absolute; height: 20px; bottom: 0px;">
Save Link
</button>
<button id="startLogData" style="position: absolute; height: 20px; bottom: 20px;">
Log Data
</button>
<!-- <script src="../build/three.js"></script> -->
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/loaders/OBJLoader.js"></script>
<!-- <script src="js/loaders/OBJLoader.js"></script> -->
<script src="https://threejs.org/examples/js/controls/TrackballControls.js"></script>
<script src="js/controls/TrackballControls.js"></script>
<script>
class Node {
constructor(iden, nodeData, nextNode=null) {
this.iden = iden; // identifier
this.nodeData = nodeData;
this.nextNode = nextNode;
}
}
class NodeData {
constructor(x, y, nextX, nextY) {
this.x = x;
this.y = y;
this.nextX = nextX;
this.nextY = nextY;
}
}
var container;
var camera, scene, renderer;
var object, boundingBox;
// BONUS: make as constant that you can input via gui
var sliceInterval = 0.0625;
// var sliceInterval = 0.5;
// TODO: make this better / make them local
var planes = [];
var startingSlice = 1;
var endingSlice = 2;
var a = new THREE.Vector3(),
b = new THREE.Vector3(),
c = new THREE.Vector3();
var planePointA = new THREE.Vector3(),
planePointB = new THREE.Vector3(),
planePointC = new THREE.Vector3();
var lineAB, lineBC, lineCA;
var pointOfIntersection;
var pointsOfIntersection = [];
var linesOfIntersection = [];
var shapeMeshes = [];
var gridHelpers = [];
init();
animate();
function init() {
initContainer();
initCamera();
initRenderer();
initControls();
initSceneAndLights();
loadObj().then(function(obj) {
var objectGeom = new THREE.Geometry().fromBufferGeometry(obj.children[0].geometry);
objectGeom.scale(20, 20, 20)
object = new THREE.Mesh(objectGeom, new THREE.MeshStandardMaterial( {
bumpScale: 1,
color: new THREE.Color( 0x000000 ),
metalness: 0.3,
roughness: 0.5,
wireframe: true
} ));
scene.add(object);
boundingBox = new THREE.Box3().setFromObject(object);
boundingBox.getCenter(controls.target);
controls.target.x = 0;
// BONUS: have animation where 120 planes get rendered one by one, then disappear one by one
var boundingBoxHeight = boundingBox.max.y - boundingBox.min.y;
console.log('bunny height: ' + boundingBoxHeight)
// numSlices = Math.ceil(boundingBoxHeight / sliceInterval) - 1;
// for (var yOffset = sliceInterval; yOffset < boundingBoxHeight; yOffset += sliceInterval) {
initPlanes();
startDrawIntersectionPoints.addEventListener("click", drawIntersectionPoints, false);
startCreateSlices.addEventListener("click", createSlices, false);
startNextSlice.addEventListener("click", getNextSlice, false);
startPreviousSlice.addEventListener("click", getPreviousSlice, false);
startRemoveBunny.addEventListener('click', removeBunny, false);
startShowBunny.addEventListener('click', showBunny, false);
startSaveLink.addEventListener('click', saveAsImage, false);
startLogData.addEventListener('click', logData, false);
});
window.addEventListener( 'resize', onWindowResize, false );
}
function initContainer() {
container = document.createElement( 'div' );
document.body.appendChild( container );
}
function initCamera() {
camera = new THREE.PerspectiveCamera( 5, window.innerWidth / window.innerHeight, 1, 2000 );
// camera.position.set(0, 50, 200);
camera.position.y += 200;
camera.updateProjectionMatrix();
}
function initRenderer() {
renderer = new THREE.WebGLRenderer({preserveDrawingBuffer : true, alpha: true });
// renderer.setClearColor( 0xffffff, 0 );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
container.appendChild( renderer.domElement );
}
function initSceneAndLights() {
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xffffff );
var ambientLight = new THREE.AmbientLight( 0xcccccc, 0.5 );
scene.add( ambientLight );
var pointLight = new THREE.PointLight( 0xffffff, 0.5 );
camera.add( pointLight );
scene.add( camera );
}
function initPlanes() {
function addPlane(yOffset) {
var planeGeom = new THREE.PlaneGeometry(18, 15);
planeGeom.rotateX(-Math.PI / 2);
plane = new THREE.Mesh(planeGeom, new THREE.MeshBasicMaterial({
color: "lightgray",
transparent: true,
opacity: 0.20,
side: THREE.DoubleSide
}));
boundingBox.getCenter(plane.position);
plane.position.y = boundingBox.min.y + yOffset;
scene.add(plane);
return plane;
}
for (var i = startingSlice; i < endingSlice; i++) {
var yOffset = sliceInterval * i;
console.log(yOffset);
planes[i] = addPlane(yOffset);
initGrid(planes[i]);
}
}
function initGrid(plane) {
// size, divisions
var gridHelper = new THREE.GridHelper( 32, 8, new THREE.Color( 0xff0000), new THREE.Color( 0xff0000 ));
var gridHelper2 = new THREE.GridHelper( 32, 16, new THREE.Color( 0x00ff6a), new THREE.Color( 0x00ff6a ));
var gridHelper3 = new THREE.GridHelper( 32, 32, new THREE.Color( 0xdbdbdb ), new THREE.Color( 0xdbdbdb ));
gridHelpers.push(gridHelper);
gridHelpers.push(gridHelper2);
gridHelpers.push(gridHelper3);
scene.add(gridHelper);
scene.add(gridHelper2);
scene.add(gridHelper3);
// set grid to top of resulting extruded slice (may make sense to move this to extrude logic)
var box = new THREE.Box3().setFromObject(plane);
box.getCenter(gridHelper.position);
box.getCenter(gridHelper2.position);
gridHelper2.position.y -= 0.05;
box.getCenter(gridHelper3.position);
gridHelper3.position.y -= 0.1;
}
function initControls() {
controls = new THREE.TrackballControls( camera, renderer.domElement );
controls.minDistance = 1;
controls.maxDistance = 1000;
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 2;
controls.panSpeed = 0.1;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
controls.keys = [ 65, 83, 68 ];
controls.addEventListener( 'change', render );
}
function loadObj() {
var manager = new THREE.LoadingManager();
manager.onStart = function ( url, itemsLoaded, itemsTotal ) {
console.log( 'Started loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
};
manager.onLoad = function ( ) {
console.log( 'Loading complete!');
};
manager.onProgress = function ( url, itemsLoaded, itemsTotal ) {
console.log( 'Loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
};
manager.onError = function ( url ) {
console.log( 'There was an error loading ' + url );
};
var loader = new THREE.OBJLoader( manager );
// BONUS: do percentage loading logging (see obj loader tutorial)
return new Promise(resolve => {
loader.load( 'models/obj/bunny.obj', resolve);
});
}
function resetGlobalVars() {
planes = [];
a = new THREE.Vector3(),
b = new THREE.Vector3(),
c = new THREE.Vector3();
planePointA = new THREE.Vector3(),
planePointB = new THREE.Vector3(),
planePointC = new THREE.Vector3();
pointsOfIntersection = [];
linesOfIntersection = [];
}
/**
* Thanks to https://stackoverflow.com/questions/42348495/three-js-find-all-points-where-a-mesh-intersects-a-plane
*/
function drawIntersectionPoints() {
function setPointOfIntersection(line, plane, i) {
var pointOfIntersection = plane.intersectLine(line, new THREE.Vector3());
if (pointOfIntersection) {
pointsOfIntersection[i].vertices.push(pointOfIntersection.clone());
};
}
for (var i = 0; i < endingSlice - startingSlice; i++) {
var s = startingSlice + i;
console.log(s);
pointsOfIntersection[s] = new THREE.Geometry();
var mathPlane = new THREE.Plane();
var plane = planes[s];
plane.localToWorld(planePointA.copy(plane.geometry.vertices[plane.geometry.faces[0].a]));
plane.localToWorld(planePointB.copy(plane.geometry.vertices[plane.geometry.faces[0].b]));
plane.localToWorld(planePointC.copy(plane.geometry.vertices[plane.geometry.faces[0].c]));
mathPlane.setFromCoplanarPoints(planePointA, planePointB, planePointC);
object.geometry.faces.forEach(function(face) {
object.localToWorld(a.copy(object.geometry.vertices[face.a]));
object.localToWorld(b.copy(object.geometry.vertices[face.b]));
object.localToWorld(c.copy(object.geometry.vertices[face.c]));
lineAB = new THREE.Line3(a, b);
lineBC = new THREE.Line3(b, c);
lineCA = new THREE.Line3(c, a);
setPointOfIntersection(lineAB, mathPlane, s);
setPointOfIntersection(lineBC, mathPlane, s);
setPointOfIntersection(lineCA, mathPlane, s);
});
var pointsMaterial = new THREE.PointsMaterial({
size: 0.01,
color: 0xffdd00
});
// var points = new THREE.Points(pointsOfIntersection[s], pointsMaterial);
// scene.add(points);
var lines = new THREE.LineSegments(pointsOfIntersection[s], new THREE.LineBasicMaterial({
color: 0x000000
}));
linesOfIntersection[s] = lines;
console.log(pointsOfIntersection[s]);
scene.add(lines);
scene.remove(planes[s]);
}
scene.remove(object);
}
// BONUS: have intermediary button to draw planes first before extruding
function createSlices() {
console.log('createSlices');
for (var i = 0; i < endingSlice - startingSlice; i++) {
var s = startingSlice + i;
var points = pointsOfIntersection[s].vertices;
var nodeList = [];
var reversedNodeList = [];
for (var v = 0; v < points.length; v+=2) {
var x = points[v].x;
var y = points[v].z;
var nextX = points[v+1].x;
var nextY = points[v+1].z;
nodeList.push(new Node(v/2, new NodeData(x, y, nextX, nextY), null));
reversedNodeList.push(new Node(v/2, new NodeData(nextX, nextY, x, y), null));
}
var cycles = getCycles(nodeList, reversedNodeList);
for (var c = 0; c < Object.keys(cycles).length; c++) {
var shape = getShapeFromCycle(cycles[c]);
// console.log(shape);
addShape(shape, s);
// break;
}
}
// remove original bunny object
// scene.remove(object);
}
function getCycles(nodeList, reversedNodeList) {
console.log('in get cycles');
var indicesToProcess = getSetWithNumbersUpTo(nodeList.length);
var cyclic = {}; // cyclic[0] = list of points, in order
var currentLoop = []; // current loop we're looking at, is connected
var indexToProcess = indicesToProcess.values().next().value;
var startNode = nodeList[indexToProcess];
var lastNodeIndex = indexToProcess;
var lineLengths = [];
while (indicesToProcess.size > 0) {
// console.log('-------------------');
indicesToProcess.delete(indexToProcess);
var currentNode = nodeList[indexToProcess];
var nextNodeIndex = findNextNode(lastNodeIndex, currentNode, nodeList);
if (nextNodeIndex === null) {
// console.log('trying to find next node with reversedNodeList');
nextNodeIndex = findNextNode(lastNodeIndex, currentNode, reversedNodeList);
if (nextNodeIndex !== null) {
// console.log('found solution by reversing');
nodeList[nextNodeIndex] = reversedNodeList[nextNodeIndex];
}
}
currentLoop.push(currentNode);
// console.log(currentNode.iden + ' --> ' + nextNodeIndex);
// console.log(currentNode);
// console.log(nodeList[nextNodeIndex]);
lastNodeIndex = indexToProcess;
if (nextNodeIndex === null) {
indexToProcess = indicesToProcess.values().next().value;
startNode = nodeList[indexToProcess];
lineLengths.push(currentLoop.length);
currentLoop = [];
} else if (nextNodeIndex === startNode.iden) {
console.log('cycle detected');
console.log(currentLoop);
cyclic[Object.keys(cyclic).length] = currentLoop.slice();
currentLoop = [];
indexToProcess = indicesToProcess.values().next().value;
startNode = nodeList[indexToProcess];
} else {
indexToProcess = nextNodeIndex;
}
}
// console.log(lineLengths);
// console.log(cyclic);
return cyclic;
}
// BONUS: make this more efficient or use memoization
function findNextNode(lastNodeIndex, currentNode, nodeList) {
var nextX = currentNode.nodeData.nextX;
var nextY = currentNode.nodeData.nextY;
for (var n=0; n<nodeList.length; n++) {
var node = nodeList[n];
// if (n === 36) {
// console.log(node);
// console.log(lastNodeIndex);
// console.log('x: ' + node.nodeData.x);
// console.log('nextX: ' + nextX);
// console.log(node.nodeData.x === nextX);
// console.log(node.nodeData.y === nextY);
// console.log(node.iden !== currentNode.iden);
// console.log(lastNodeIndex !== n);
// }
// make somewhat tolerant to floating point approximations
// BONUS: extend 3js library to use bigdecimal.js, or use python to preprocess
if (Math.abs(node.nodeData.x - nextX) < .0000000001 &&
Math.abs(node.nodeData.y - nextY) < .0000000001 &&
node.iden !== currentNode.iden
// && lastNodeIndex !== n
)
{
return n;
}
}
return null;
}
function getShapeFromCycle(cycle) {
var cyclePts = [];
for (var p = 0; p < cycle.length; p++) {
cyclePts.push(new THREE.Vector2(cycle[p].nodeData.x, cycle[p].nodeData.y));
}
cyclePts.push(new THREE.Vector2(cycle[0].nodeData.x, cycle[0].nodeData.y));
return new THREE.Shape(cyclePts);
}
function addShape( shape, sliceNumber ) {
console.log('in add shape');
var extrudeSettings = {
steps: 1,
depth: sliceInterval,
bevelEnabled: false,
bevelThickness: 0,
bevelSize: 0,
bevelOffset: 0,
bevelSegments: 1
};
// var color = 0xafafaf;
var color = 0x000000;
// extruded shape
var geometry = new THREE.ExtrudeBufferGeometry( shape, extrudeSettings );
var mesh = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial( { color: color, opacity: 0.5, transparent: true } ) );
// BONUS: place objects above the plane to begin with, always
mesh.position.y = boundingBox.min.y + sliceNumber * sliceInterval;
mesh.rotation.x = Math.PI / 2;
// mesh.position.set( x, y, z - 75 );
// mesh.rotation.set( rx, ry, rz );
// mesh.scale.set( s, s, s );
// console.log(geometry);
shapeMeshes.push(mesh);
scene.add( mesh );
}
function getSetWithNumbersUpTo(num) {
var s = new Set()
for (var i = 0; i < num; i++) {
s.add(i);
}
return s;
}
function renderLineSegments(nodeList) {
var points = [];
var y = boundingBox.min.y + sliceInterval * startingSlice;
for (var n = 0; n < nodeList.length; n++) {
points.push(new THREE.Vector3(nodeList[n].nodeData.x, y, nodeList[n].nodeData.y));
points.push(new THREE.Vector3(nodeList[n].nodeData.nextX, y, nodeList[n].nodeData.nextY));
}
var lines = new THREE.LineSegments(points, new THREE.LineBasicMaterial({
color: 0xffffff
}));
console.log(points);
scene.add(lines);
}
// https://codepen.io/shivasaxena/pen/QEzAAv
function saveAsImage() {
var saveFile = function (strData, filename) {
var link = document.createElement('a');
if (typeof link.download === 'string') {
document.body.appendChild(link); //Firefox requires the link to be in the body
link.download = filename;
link.href = strData;
link.click();
document.body.removeChild(link); //remove the link when done
} else {
location.replace(uri);
}
}
var imgData, imgNode;
var strDownloadMime = "image/octet-stream";
try {
var strMime = "image/png";
imgData = renderer.domElement.toDataURL(strMime);
saveFile(imgData.replace(strMime, strDownloadMime), "test.png");
} catch (e) {
console.log(e);
return;
}
}
function logData() {
console.log(controls.target);
}
function removeBunny() {
scene.remove(object);
}
function showBunny() {
scene.add(object);
}
function getNextSlice() {
resetGlobalVars();
startingSlice += 1;
endingSlice += 1;
for (var i = 0; i < shapeMeshes.length; i++) {
scene.remove(shapeMeshes[i]);
}
for (var j = 0; j < gridHelpers.length; j++) {
scene.remove(gridHelpers[j]);
}
initPlanes();
drawIntersectionPoints();
createSlices();
}
function getPreviousSlice() {
resetGlobalVars();
startingSlice -= 1;
endingSlice -= 1;
for (var i = 0; i < shapeMeshes.length; i++) {
scene.remove(shapeMeshes[i]);
}
for (var j = 0; j < gridHelpers.length; j++) {
scene.remove(gridHelpers[j]);
}
initPlanes();
drawIntersectionPoints();
createSlices();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
controls.handleResize();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
requestAnimationFrame( animate );
controls.update();
render();
}
function render() {
renderer.render( scene, camera );
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment