Created
June 30, 2022 18:30
-
-
Save daniel-packard/feafe942f2fef1ee7dbd8e599e29333d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import './style.css' | |
import * as THREE from 'three' | |
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' | |
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js' | |
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js' | |
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js' | |
import { SimplifyModifier } from 'three/examples/jsm/modifiers/SimplifyModifier.js' | |
import * as dat from 'lil-gui' | |
//V1.2.0 | |
//Ugh, don't ask about this stuff | |
var userUploaded = false | |
// Creates empty mesh container | |
var myMesh = new THREE.Mesh(); | |
// Creates another empty mesh container | |
var tempGeometry = new THREE.Mesh(); | |
var scanGeometry = new THREE.Mesh() | |
// Updated GUI | |
let gui = new dat.GUI() | |
const rotateControls = gui.addFolder('Rotation') | |
rotateControls.close() | |
const decimateControls = gui.addFolder('Decimation') | |
const exportControls = gui.addFolder('Export') | |
//Should detect if user is on mobile or not | |
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { | |
exportControls.close() | |
rotateControls.close() | |
gui.close() | |
var link = document.getElementById('input'); | |
link.style.display = 'none'; | |
} else { | |
console.log('Mobile device detected'); | |
} | |
// Canvas | |
const canvas = document.querySelector('canvas.webgl') | |
// Scene | |
const scene = new THREE.Scene() | |
// STLExport | |
const exporter = new STLExporter(); | |
// OBJExport | |
const objExporter = new OBJExporter(); | |
// Parameters | |
const stlLoader = new STLLoader() | |
//Material | |
const material = new THREE.MeshNormalMaterial() | |
material.flatShading = true | |
material.side = THREE.DoubleSide; | |
//Add Grid Helper | |
const size = 1000; | |
const divisions = (size / 10); | |
const gridHelper = new THREE.GridHelper(size, divisions); | |
scene.add(gridHelper); | |
const modifier = new SimplifyModifier(); | |
const MAX_FACE_COUNT_PER_ITERATION = 250; | |
const renderTimeout = () => { | |
return new Promise((resolve, reject) => { | |
window.requestAnimationFrame(() => { | |
resolve(); | |
}) | |
}) | |
} | |
let modifierInProgress = false; | |
let modifierProgressPercentage = 0; | |
const iterativeModifier = async ({ decimationFaceCount, geometry, updateCallback }) => { | |
modifierInProgress = true; | |
modifierProgressPercentage = 0; | |
let startingFaceCount = geometry.attributes.position.count | |
let currentFaceCount = startingFaceCount; | |
let targetFaceCount = startingFaceCount - decimationFaceCount; | |
let totalFacesToDecimate = startingFaceCount - targetFaceCount; | |
let remainingFacesToDecimate = currentFaceCount - targetFaceCount; | |
let iterationFaceCount = currentFaceCount - MAX_FACE_COUNT_PER_ITERATION; | |
let simplifiedGeometry = geometry; | |
while (iterationFaceCount > targetFaceCount) { | |
console.log({ currentFaceCount, iterationFaceCount, targetFaceCount }); | |
simplifiedGeometry = modifier.modify(simplifiedGeometry, MAX_FACE_COUNT_PER_ITERATION); | |
await renderTimeout(); | |
updateCallback(simplifiedGeometry) | |
await renderTimeout(); | |
currentFaceCount = simplifiedGeometry.attributes.position.count; | |
iterationFaceCount = currentFaceCount - MAX_FACE_COUNT_PER_ITERATION; | |
remainingFacesToDecimate = currentFaceCount - targetFaceCount; | |
modifierProgressPercentage = Math.floor(((totalFacesToDecimate - remainingFacesToDecimate) / totalFacesToDecimate) * 100); | |
} | |
simplifiedGeometry = modifier.modify(simplifiedGeometry, currentFaceCount - targetFaceCount); | |
updateCallback(simplifiedGeometry) | |
modifierProgressPercentage = 100; | |
modifierInProgress = false; | |
} | |
const rotateX = { | |
RotateX: function () { | |
console.clear(); | |
myMesh.rotation.x = myMesh.rotation.x + (-90 * Math.PI / 180); | |
myMesh.geometry.computeBoundingBox(); | |
var bbox = myMesh.geometry.boundingBox; | |
myMesh.position.y = ((bbox.max.z - bbox.min.z) / 2) | |
} | |
} | |
rotateControls.add(rotateX, 'RotateX') | |
const rotateY = { | |
RotateY: function () { | |
console.clear(); | |
myMesh.rotation.y = myMesh.rotation.y + (-90 * Math.PI / 180); | |
myMesh.geometry.computeBoundingBox(); | |
var bbox = myMesh.geometry.boundingBox; | |
myMesh.position.y = ((bbox.max.z - bbox.min.z) / 2) | |
} | |
} | |
rotateControls.add(rotateY, 'RotateY') | |
const rotateZ = { | |
RotateZ: function () { | |
console.clear(); | |
myMesh.rotation.z = myMesh.rotation.z + (-90 * Math.PI / 180); | |
myMesh.geometry.computeBoundingBox(); | |
var bbox = myMesh.geometry.boundingBox; | |
myMesh.position.y = ((bbox.max.z - bbox.min.z) / 2) | |
} | |
} | |
rotateControls.add(rotateZ, 'RotateZ') | |
const decimate = { amount: .25 } | |
decimateControls.add(decimate, 'amount', .01, .50, .01).name('Decimation Percentage') | |
// Sizes | |
const sizes = { | |
width: window.innerWidth, | |
height: window.innerHeight | |
} | |
window.addEventListener('resize', () => { | |
// Update sizes | |
sizes.width = window.innerWidth | |
sizes.height = window.innerHeight | |
// Update camera | |
camera.aspect = sizes.width / sizes.height | |
camera.updateProjectionMatrix() | |
// Update renderer | |
renderer.setSize(sizes.width, sizes.height) | |
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) | |
}) | |
// Camera | |
const camera = new THREE.PerspectiveCamera(50, sizes.width / sizes.height, 0.1, 2000) | |
// scene.add(camera) | |
// Controls | |
const controls = new OrbitControls(camera, canvas) | |
controls.enableDamping = true | |
// Renderer | |
const renderer = new THREE.WebGLRenderer({ | |
canvas: canvas | |
}) | |
renderer.setSize(sizes.width, sizes.height) | |
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) | |
function coord() { | |
console.log(camera.position) | |
} | |
stlLoader.load( | |
'./models/test.stl', | |
function (geometry) { | |
myMesh.material = material; | |
myMesh.geometry = geometry; | |
var tempGeometry = new THREE.Mesh() | |
tempGeometry.geometry = geometry | |
geometry.computeVertexNormals(); | |
myMesh.geometry.center() | |
myMesh.rotation.x = -90 * Math.PI / 180; | |
myMesh.geometry.computeBoundingBox(); | |
var bbox = myMesh.geometry.boundingBox; | |
myMesh.position.y = ((bbox.max.z - bbox.min.z) / 2) | |
tempGeometry.position.copy(myMesh.position) | |
camera.position.set(60, 60, 120); | |
tempGeometry.geometry = modifier.modify(geometry, 0) | |
myMesh.geometry = modifier.modify(geometry, 0) | |
console.log('Face Count:', myMesh.geometry.attributes.position.count) | |
console.log('Temp Face Count:', tempGeometry.geometry.attributes.position.count) | |
scene.add(myMesh); | |
function tick() { | |
var currentTri = (Math.floor(myMesh.geometry.attributes.position.count)) | |
document.getElementById("currentTri").innerHTML = currentTri; | |
if (!modifierInProgress) { | |
var targetTri = myMesh.geometry.attributes.position.count - (Math.floor((decimate.amount * myMesh.geometry.attributes.position.count))) | |
document.getElementById("targetTri").innerHTML = targetTri; | |
var time = Math.floor((decimate.amount * myMesh.geometry.attributes.position.count) * .00267) | |
document.getElementById("time").innerHTML = time; | |
} | |
document.getElementById("progress").innerHTML = `${modifierProgressPercentage}%`; | |
render() | |
controls.update() | |
window.requestAnimationFrame(tick) | |
} | |
function render() { | |
renderer.render(scene, camera) | |
} | |
tick() | |
document.getElementById('defaults').addEventListener('change', consoleLogger); | |
function consoleLogger() { | |
var e = document.getElementById("defaults").value | |
if (e == "frog") { | |
function loadFrog() { | |
stlLoader.load( | |
'./models/frog.stl', | |
function (geometry) { | |
console.clear() | |
myMesh.material = material; | |
myMesh.geometry = geometry; | |
tempGeometry = new THREE.Mesh(myMesh.geometry, myMesh.material) | |
myMesh.position.copy = (tempGeometry.position) | |
geometry.computeVertexNormals(); | |
myMesh.geometry.center() | |
myMesh.rotation.x = -90 * Math.PI / 180; | |
myMesh.geometry.computeBoundingBox(); | |
var bbox = myMesh.geometry.boundingBox; | |
myMesh.position.y = ((bbox.max.z - bbox.min.z) / 2) | |
tempGeometry.geometry = modifier.modify(geometry, 0) | |
myMesh.geometry = modifier.modify(geometry, 0) | |
console.log('Face Count:', tempGeometry.geometry.attributes.position.count) | |
console.log('Face Count:', myMesh.geometry.attributes.position.count) | |
}) | |
} | |
loadFrog() | |
} if (e == "sculpture") { | |
function loadSculpture() { | |
stlLoader.load( | |
'./models/bust.stl', | |
function (geometry) { | |
console.clear() | |
myMesh.material = material; | |
myMesh.geometry = geometry; | |
tempGeometry = new THREE.Mesh(myMesh.geometry, myMesh.material) | |
myMesh.position.copy = (tempGeometry.position) | |
geometry.computeVertexNormals(); | |
myMesh.geometry.center() | |
myMesh.rotation.x = -90 * Math.PI / 180; | |
myMesh.geometry.computeBoundingBox(); | |
var bbox = myMesh.geometry.boundingBox; | |
myMesh.position.y = ((bbox.max.z - bbox.min.z) / 2) | |
tempGeometry.geometry = modifier.modify(geometry, 0) | |
myMesh.geometry = modifier.modify(geometry, 0) | |
console.log('Face Count:', tempGeometry.geometry.attributes.position.count) | |
console.log('Face Count:', myMesh.geometry.attributes.position.count) | |
}) | |
} | |
loadSculpture() | |
} | |
if (e == "bunny") { | |
function loadBunny() { | |
stlLoader.load( | |
'./models/test.stl', | |
function (geometry) { | |
console.clear() | |
myMesh.material = material; | |
myMesh.geometry = geometry; | |
tempGeometry = new THREE.Mesh(myMesh.geometry, myMesh.material) | |
myMesh.position.copy = (tempGeometry.position) | |
geometry.computeVertexNormals(); | |
myMesh.geometry.center() | |
myMesh.rotation.x = -90 * Math.PI / 180; | |
myMesh.geometry.computeBoundingBox(); | |
var bbox = myMesh.geometry.boundingBox; | |
myMesh.position.y = ((bbox.max.z - bbox.min.z) / 2) | |
tempGeometry.geometry = modifier.modify(geometry, 0) | |
myMesh.geometry = modifier.modify(geometry, 0) | |
console.log('Face Count:', tempGeometry.geometry.attributes.position.count) | |
console.log('Face Count:', myMesh.geometry.attributes.position.count) | |
}) | |
} | |
loadBunny() | |
} | |
if (e == "scan") { | |
function loadScan() { | |
stlLoader.load( | |
'./models/scan.stl', | |
function (geometry) { | |
console.clear() | |
myMesh.material = material; | |
myMesh.geometry = geometry; | |
tempGeometry = new THREE.Mesh(myMesh.geometry, myMesh.material) | |
myMesh.position.copy = (tempGeometry.position) | |
geometry.computeVertexNormals(); | |
myMesh.geometry.center() | |
myMesh.rotation.x = -90 * Math.PI / 180; | |
myMesh.geometry.computeBoundingBox(); | |
var bbox = myMesh.geometry.boundingBox; | |
myMesh.position.y = ((bbox.max.z - bbox.min.z) / 2) | |
tempGeometry.geometry = modifier.modify(geometry, 0) | |
myMesh.geometry = modifier.modify(geometry, 0) | |
console.log('Face Count:', tempGeometry.geometry.attributes.position.count) | |
console.log('Face Count:', myMesh.geometry.attributes.position.count) | |
}) | |
} | |
loadScan() | |
} | |
} | |
const updateScene = { | |
Update: function () { | |
console.clear(); | |
console.time('updateScene') | |
console.log('Original Face Count:', myMesh.geometry.attributes.position.count) | |
console.log('Temp Face Count:', tempGeometry.geometry.attributes.position.count) | |
const count = Math.floor(myMesh.geometry.attributes.position.count * decimate.amount); | |
iterativeModifier({ | |
decimationFaceCount: count, geometry: myMesh.geometry, updateCallback: (geometry) => { | |
myMesh.geometry = geometry; | |
} | |
}).then(() => { | |
console.log('Updated Face Count:', myMesh.geometry.attributes.position.count) | |
console.timeEnd('updateScene') | |
}).catch(error => console.error(error)) | |
} | |
} | |
decimateControls.add(updateScene, 'Update') | |
const resetScene = { | |
Reset: function () { | |
console.clear(); | |
coord() | |
// scanGeometry.geometry = modifier.modify(tempGeometry.geometry, 0) | |
// tempGeometry.geometry = modifier.modify(geometry, 0) | |
myMesh.geometry = tempGeometry.geometry | |
myMesh.geometry = modifier.modify(tempGeometry.geometry, 0) | |
console.log('Temp Face Count:', tempGeometry.geometry.attributes.position.count) | |
console.log('Face Count:', myMesh.geometry.attributes.position.count) | |
} | |
} | |
decimateControls.add(resetScene, 'Reset') | |
document.getElementById('file-selector').addEventListener('change', openFile, false); | |
function openFile(evt) { | |
console.clear(); | |
const fileObject = evt.target.files[0]; | |
const reader = new FileReader(); | |
reader.readAsArrayBuffer(fileObject); | |
reader.onload = function () { | |
var geometry = stlLoader.parse(this.result); | |
myMesh.material = material; | |
myMesh.geometry = geometry; | |
tempGeometry = new THREE.Mesh(geometry, material) | |
myMesh.position.copy = (tempGeometry.position) | |
geometry.computeVertexNormals(); | |
myMesh.geometry.center() | |
myMesh.rotation.x = -90 * Math.PI / 180; | |
myMesh.geometry.computeBoundingBox(); | |
var bbox = myMesh.geometry.boundingBox; | |
myMesh.position.y = ((bbox.max.z - bbox.min.z) / 2) | |
myMesh.geometry = modifier.modify(geometry, 0) | |
tempGeometry.geometry = modifier.modify(geometry, 0) | |
console.log('Face Count:', myMesh.geometry.attributes.position.count) | |
console.log('Temp Face Count:', tempGeometry.geometry.attributes.position.count) | |
}; | |
}; | |
}) | |
const exportModel = { | |
Export: function () { | |
var str = exporter.parse(myMesh, { binary: true }); | |
var blob = new Blob([str], { type: 'text/plain' }); | |
var link = document.createElement('a'); | |
link.style.display = 'none'; | |
document.body.appendChild(link); | |
link.href = URL.createObjectURL(blob); | |
link.download = 'Model.stl'; | |
link.click(); | |
} | |
} | |
const exportOBJ = { | |
Export: function () { | |
var str = objExporter.parse(myMesh); | |
var blob = new Blob([str], { type: 'text/plain' }); | |
var link = document.createElement('a'); | |
link.style.display = 'none'; | |
document.body.appendChild(link); | |
link.href = URL.createObjectURL(blob); | |
link.download = 'Model.obj'; | |
link.click(); | |
} | |
} | |
exportControls.add(exportModel, 'Export').name("Export STL") | |
exportControls.add(exportOBJ, 'Export').name("Export OBJ") | |
// console.log("Well, aren't you curious? You can see face count, processing time, and more right here in the console!") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment