Skip to content

Commit

Permalink
Initial WASM build
Browse files Browse the repository at this point in the history
  • Loading branch information
neurolabusc committed Jul 21, 2024
1 parent 7261977 commit d82f188
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 26 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ npm run dev

- [Will Usher](https://github.com/Twinklebear/webgl-marching-cubes) ported [Marching Cubes](https://paulbourke.net/geometry/polygonise/) to JavaScript.
- This project includes a pure JavaScript port of Sven Forstmann's [Fast Quadric Mesh Simplification](https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification)
- [Tim Knip](https://github.com/timknip/mesh-decimate/tree/master) provides a ThreeJS project that provides both WASM and native JavaScript mesh decimation. Try the [live demo](https://neurolabusc.github.io/simplifyjs/).
15 changes: 12 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
<header>
<label for="volumeSelect">Volume</label>
<select id="volumeSelect">
<option selected>bet</option>
<option selected>tinyT1</option>
<option>bet</option>
<option>Iguana</option>
<option>CT_Philips</option>
<option>mni152</option>
Expand Down Expand Up @@ -45,7 +46,7 @@
<input id="isoNumber" type="text" value="100">
<p>
<p>
<input type="checkbox" id="bubbleCheck" checked/>
<input type="checkbox" id="bubbleCheck" unchecked/>
<label>Fill bubbles</label>
</p>
<p>
Expand All @@ -65,7 +66,11 @@
</p>
<p>
<label>Simplify Percent (1..100)</label>
<input id="shrinkPct" type="number" min="1" value="100" max="100">
<input id="shrinkPct" type="number" min="1" value="30" max="100">
</p>
<p>
<input type="checkbox" id="wasmCheck" checked/>
<label>WASM</label>
</p>
<button id="cancelBtn" formmethod="dialog">Cancel</button>
<button autofocus id="applyBtn" value="default">Apply</button>
Expand All @@ -77,6 +82,10 @@
<label>Simplify Percent (1..100)</label>
<input id="shrinkSimplePct" type="number" min="1" value="10" max="100">
</p>
<p>
<input type="checkbox" id="simpleWasmCheck" unchecked/>
<label>WASM</label>
</p>
<button id="cancelBtn" formmethod="dialog">Cancel</button>
<button autofocus id="applySimpleBtn" value="default">Apply</button>
</form>
Expand Down
93 changes: 75 additions & 18 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,31 @@ function formatNumber(value) {
async function main() {
const MeshWorker = await createMeshWorker()
const VoxelWorker = await createVoxelWorker()
const Nii2meshWorker = new Worker('./nii2meshWorker.js?rnd=' + Math.random())
const loadingCircle = document.getElementById('loadingCircle')
function meshStatus() {
const str = `Mesh has ${nv1.meshes[0].pts.length / 3} vertices and ${nv1.meshes[0].tris.length / 3} triangles`
let startTime = Date.now()
function meshStatus(isTimed = true) {
let str = `Mesh has ${nv1.meshes[0].pts.length / 3} vertices and ${nv1.meshes[0].tris.length / 3} triangles`
if (isTimed)
str += ` ${Date.now() - startTime}ms`
document.getElementById('location').innerHTML = str
console.log(str)
shaderSelect.onchange()
nv1.setMeshProperty(nv1.meshes[0].id, 'visible', visibleCheck.checked)
}
async function loadMesh(vertices, triangles) {
async function loadMz3(meshBuffer) {
if (nv1.meshes.length > 0) {
nv1.removeMesh(nv1.meshes[0])
}
const verticesArray = new Float32Array(vertices)
const trianglesArray = new Uint32Array(triangles)
const meshBuffer = NVMeshUtilities.createMZ3(verticesArray, trianglesArray, false)
await nv1.loadFromArrayBuffer(meshBuffer, 'test.mz3')
loadingCircle.classList.add('hidden')
meshStatus()
}
async function loadMesh(vertices, triangles) {
const verticesArray = new Float32Array(vertices)
const trianglesArray = new Uint32Array(triangles)
const meshBuffer = NVMeshUtilities.createMZ3(verticesArray, trianglesArray, false)
await loadMz3(meshBuffer)
}
MeshWorker.onmessage = async function (e) {
const { vertices, triangles } = e.data
await loadMesh(vertices, triangles)
Expand All @@ -54,6 +61,15 @@ async function main() {
const { vertices, triangles } = e.data
await loadMesh(vertices, triangles)
}
Nii2meshWorker.onmessage = async function (e) {
if (e.data.blob instanceof Blob) {
var reader = new FileReader()
reader.onload = () => {
loadMz3(reader.result)
}
reader.readAsArrayBuffer(e.data.blob)
}
}
saveBtn.onclick = function () {
if (nv1.meshes.length < 1) {
window.alert("No mesh open for saving. Use 'Create Mesh'.")
Expand All @@ -65,9 +81,9 @@ async function main() {
const selectedOption = volumeSelect.options[volumeSelect.selectedIndex]
const txt = selectedOption.text
let fnm = './' + txt
if (volumeSelect.selectedIndex > 3) {
if (volumeSelect.selectedIndex > 4) {
fnm = 'https://niivue.github.io/niivue/images/' + txt
} else if (volumeSelect.selectedIndex > 0) {
} else if (volumeSelect.selectedIndex > 1) {
fnm = 'https://niivue.github.io/niivue-demo-images/' + txt
}
if (nv1.meshes.length > 0) {
Expand All @@ -76,7 +92,7 @@ async function main() {
if (nv1.volumes.length > 0) {
nv1.removeVolumeByIndex(0)
}
if (volumeSelect.selectedIndex > 4) {
if (volumeSelect.selectedIndex > 5) {
nv1.loadMeshes([{ url: fnm }])
} else {
if (!fnm.endsWith('.mgz')) {
Expand Down Expand Up @@ -117,22 +133,67 @@ async function main() {
console.log('No mesh open to simplify.')
return
}
startTime = Date.now()
const shrinkValue = Math.min(Math.max(Number(shrinkSimplePct.value) / 100, 0.01), 1)
if (shrinkValue >= 1)
return
const verts = nv1.meshes[0].pts.slice()
const tris = nv1.meshes[0].tris.slice()
const shrinkValue = Math.min(Math.max(Number(shrinkSimplePct.value) / 100, 0.01), 1)
loadingCircle.classList.remove('hidden')
if (simpleWasmCheck.checked) {
const meshBuffer = NVMeshUtilities.createMZ3(verts, tris, false)
let mz3 = new Blob([meshBuffer], {
type: 'application/octet-stream'
})
let inName = `em${Math.round(Math.random() * 0xffffff)}.mz3`
let fileMZ3 = new File([mz3], inName)
let outName = `em${Math.round(Math.random() * 0xffffff)}.mz3`
Nii2meshWorker.postMessage({
blob: fileMZ3,
percentage: shrinkValue,
simplify_name: outName,
})
} else {
MeshWorker.postMessage({
verts,
tris,
shrinkValue
})
}
}
applyBtn.onclick = async function () {
if (nv1.volumes.length < 1) {
console.log('No volume open to meshify.')
return
}
startTime = Date.now()
const isoValue = Number(isoNumber.value)
const largestCheckValue = largestCheck.checked
const bubbleCheckValue = bubbleCheck.checked
const shrinkValue = Math.min(Math.max(Number(shrinkPct.value) / 100, 0.01), 1)
const smoothValue = smoothSlide.value
loadingCircle.classList.remove('hidden')
if (wasmCheck.checked) {
//const meshBuffer = NVMeshUtilities.createMZ3(verts, tris, false)
const niiBuffer = await nv1.saveImage().buffer
console.log('WASM nii2mesh', niiBuffer)
let nii = new Blob([niiBuffer], {
type: 'application/octet-stream'
})
let inName = `em${Math.round(Math.random() * 0xffffff)}.nii`
let fileNii = new File([nii], inName)
let outName = `em${Math.round(Math.random() * 0xffffff)}.mz3`
Nii2meshWorker.postMessage({
blob: fileNii,
percentage: shrinkValue,
simplify_name: outName,
isoValue: isoValue,
onlyLargest: largestCheckValue,
fillBubbles: bubbleCheckValue,
postSmooth: smoothValue
})
return
}
let img = new Float32Array(nv1.volumes[0].img)
const scl_slope = nv1.volumes[0].hdr.scl_slope
const scl_inter = nv1.volumes[0].hdr.scl_inter
Expand All @@ -143,12 +204,7 @@ async function main() {
}
}
const dims = [nv1.volumes[0].hdr.dims[1], nv1.volumes[0].hdr.dims[2], nv1.volumes[0].hdr.dims[3]]
const largestCheckValue = largestCheck.checked
const bubbleCheckValue = bubbleCheck.checked
const affine = nv1.volumes[0].hdr.affine
const shrinkValue = Math.min(Math.max(Number(shrinkPct.value) / 100, 0.01), 1)
const smoothValue = smoothSlide.value
loadingCircle.classList.remove('hidden')
VoxelWorker.postMessage(
{
img,
Expand All @@ -173,7 +229,7 @@ async function main() {
nv1.setMeshShader(nv1.meshes[0].id, this.value)
}
function handleMeshLoaded() {
meshStatus()
meshStatus(false)
}
const defaults = {
onMeshLoaded: handleMeshLoaded,
Expand Down Expand Up @@ -206,7 +262,8 @@ async function main() {
nv1.opts.multiplanarForceRender = true
nv1.opts.yoke3Dto2DZoom = true
nv1.setInterpolation(true)
await nv1.loadVolumes([{ url: './bet.nii.gz' }])
//await nv1.loadVolumes([{ url: './bet.nii.gz' }])
await nv1.loadVolumes([{ url: './tinyT1.nii.gz' }])
imageStatus()
applyBtn.onclick()
}
Expand Down
1 change: 1 addition & 0 deletions nii2mesh.js

Large diffs are not rendered by default.

Binary file added nii2mesh.wasm
Binary file not shown.
55 changes: 55 additions & 0 deletions nii2meshWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
self.addEventListener('message', function(e) {
const file = e.data.blob
const percentage = e.data.percentage || 0.5;
const simplify_name = e.data.simplify_name
const isoValue = e.data.isoValue || 0.5;
onlyLargest = e.data.onlyLargest || false
fillBubbles = e.data.fillBubbles || false
postSmooth = e.data.postSmooth || 0
verbose = e.data.berbose || true
prepare_and_simplify(file, percentage, simplify_name, isoValue, onlyLargest, fillBubbles, postSmooth, verbose)
}, false)

var Module = {
'print': function(text) {
console.log(text)
self.postMessage({"log":text})
}
}

self.importScripts("nii2mesh.js?rnd="+Math.random())

let last_file_name = undefined

function prepare_and_simplify(file, percentage, simplify_name, isoValue = 1, onlyLargest = false, fillBubbles = false , postSmooth = 0, verbose = true) {
var filename = file.name
// if simplify on the same file, don't even read the file
if (filename === last_file_name) {
console.log("skipping load and create data file")
simplify(filename, percentage, simplify_name, isoValue, onlyLargest, fillBubbles, postSmooth, verbose)
return
} else { // remove last file in memory
if (last_file_name !== undefined)
Module.FS_unlink(last_file_name)
}
last_file_name = filename
var fr = new FileReader()
fr.readAsArrayBuffer(file)
fr. onloadend = function (e) {
var data = new Uint8Array(fr.result)
Module.FS_createDataFile(".", filename, data, true, true)
simplify(filename, percentage, simplify_name, isoValue, onlyLargest, fillBubbles, postSmooth, verbose)
}
}

function simplify(filename, percentage, simplify_name, isoValue = 1, onlyLargest = false, fillBubbles = false , postSmooth = 0, verbose = true) {
Module.ccall("simplify", // c function name
undefined, // return
["string", "number", "string", "number","boolean","boolean","number","boolean"], // param
[filename, percentage, simplify_name, isoValue, onlyLargest, fillBubbles,postSmooth, verbose]
)
let out_bin = Module.FS_readFile(simplify_name)
// sla should work for binary mz3
let file = new Blob([out_bin], {type: 'application/sla'})
self.postMessage({"blob":file})
}
Binary file added public/tinyT1.nii.gz
Binary file not shown.
15 changes: 10 additions & 5 deletions simplify.js
Original file line number Diff line number Diff line change
Expand Up @@ -490,14 +490,19 @@ class QuadricSimplifyMesh {
}
}
this.compactMesh()
const finalVs = []
const finalVs = new Float32Array(this.vertices.length * 3)
let j = 0
for (const vertex of this.vertices) {
finalVs.push(vertex.p.x, vertex.p.y, vertex.p.z)
finalVs[j++] = vertex.p.x
finalVs[j++] = vertex.p.y
finalVs[j++] = vertex.p.z
}

const finalTs = []
const finalTs = new Uint32Array(this.triangles.length * 3)
j = 0
for (const triangle of this.triangles) {
finalTs.push(triangle.v[0], triangle.v[1], triangle.v[2])
finalTs[j++] = triangle.v[0]
finalTs[j++] = triangle.v[1]
finalTs[j++] = triangle.v[2]
}
if (verbose) {
const pct = Math.round((100 * (finalTs.length / 3)) / triangleCount)
Expand Down

0 comments on commit d82f188

Please sign in to comment.