Created
April 25, 2024 22:51
-
-
Save PhoenixIllusion/561e36d75471e2217a5b2a8d8f22683e to your computer and use it in GitHub Desktop.
Load ThreeJS files, and convert them to tetrahedron soft-bodies using PhysX, then load into Jolt
This file contains hidden or 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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<title>JoltPhysics.js demo</title> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> | |
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/jrouwe/[email protected]/Examples///style.css"> | |
<script type="importmap"> | |
{ | |
"imports": { | |
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js", | |
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/" | |
} | |
} | |
</script> | |
</head> | |
<body> | |
<div id="container">Loading...</div> | |
<div id="info">JoltPhysics.js Tool: Generate Tetragon Soft Mesh<br /> | |
<button id="open-file">Open .OBJ File</button><br /><input type="file" id="file-input" style="display: none" /><br /> | |
Remesh Resolution: <select id="resolution"><option value="40">40</option><option value="20">20</option><option value="10">10</option><option value="5">5</option></button> | |
</div> | |
<script src="https://cdn.jsdelivr.net/gh/jrouwe/[email protected]/Examples//js/three/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/gh/jrouwe/[email protected]/Examples//js/three/OrbitControls.js"></script> | |
<script src="https://cdn.jsdelivr.net/gh/jrouwe/[email protected]/Examples//js/three/WebGL.js"></script> | |
<script src="https://cdn.jsdelivr.net/gh/jrouwe/[email protected]/Examples//js/three/stats.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/gh/jrouwe/[email protected]/Examples//js/example.js"></script> | |
<script src="https://cdn.jsdelivr.net/gh/jrouwe/[email protected]/Examples//js/three/CSS3DRenderer.js"></script> | |
<script src="https://cdn.jsdelivr.net/gh/jrouwe/[email protected]/Examples//js/debug-renderer.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/physx-js-webidl.min.js"></script> | |
<script type="module"> | |
// In case you haven't built the library yourself, replace URL with: https://www.unpkg.com/jolt-physics/dist/jolt-physics.wasm-compat.js | |
import initJolt from 'https://www.unpkg.com/jolt-physics/dist/jolt-physics.wasm-compat.js'; | |
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; | |
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; | |
import { Mesh, Object3D } from "three"; | |
const loader = new OBJLoader(); | |
const openFile = document.getElementById('open-file'); | |
const filePrompt = document.getElementById('file-input'); | |
const remeshRes = document.getElementById('resolution'); | |
let px; | |
let mesh; | |
async function loadPx() { | |
px = await PhysX(); | |
let version = px.PHYSICS_VERSION; | |
var allocator = new px.PxDefaultAllocator(); | |
var errorCb = new px.PxDefaultErrorCallback(); | |
var _foundation = px.CreateFoundation(version, allocator, errorCb); | |
console.log('PhysX loaded! Version: ' + ((version >> 24) & 0xff) + '.' + ((version >> 16) & 0xff) + '.' + ((version >> 8) & 0xff)); | |
} | |
class PhysXTool { | |
static remesh(mesh, remesherGridResolution = 20) { | |
const vertices = mesh.position; | |
const indices = mesh.index; | |
let inputVertices = new px.PxArray_PxVec3(vertices.length / 3); | |
let inputIndices = new px.PxArray_PxU32(indices.length); | |
for (let i = 0; i < vertices.length; i += 3) { | |
inputVertices.set(i / 3, new px.PxVec3(vertices[i], vertices[i + 1], vertices[i + 2])); | |
} | |
for (let i = 0; i < indices.length; i++) { | |
inputIndices.set(i, indices[i]); | |
} | |
let outputVertices = new px.PxArray_PxVec3(); | |
let outputIndices = new px.PxArray_PxU32(); | |
let vertexMap = new px.PxArray_PxU32(); | |
px.PxTetMaker.prototype.remeshTriangleMesh(inputVertices, inputIndices, remesherGridResolution, outputVertices, outputIndices, vertexMap); | |
// Transform From PxVec3 to THREE.Vector3 | |
let triIndices = new Uint32Array(outputIndices.size()); | |
for (let i = 0; i < triIndices.length; i++) { | |
triIndices[i] = outputIndices.get(i); | |
} | |
let vertPositions = new Float32Array(outputVertices.size() * 3); | |
for (let i = 0; i < outputVertices.size(); i++) { | |
let vec3 = outputVertices.get(i); | |
vertPositions[i * 3 + 0] = vec3.get_x(); | |
vertPositions[i * 3 + 1] = vec3.get_y(); | |
vertPositions[i * 3 + 2] = vec3.get_z(); | |
} | |
inputVertices.__destroy__(); | |
inputIndices.__destroy__(); | |
outputVertices.__destroy__(); | |
outputIndices.__destroy__(); | |
vertexMap.__destroy__(); | |
return { position: vertPositions, index: triIndices }; | |
} | |
static simplifyMesh(mesh, targetTriangleCount = 5000, maximalTriangleEdgeLength = 110.0) { | |
const vertices = mesh.position; | |
const indices = mesh.index; | |
let inputVertices = new px.PxArray_PxVec3(vertices.length / 3); | |
let inputIndices = new px.PxArray_PxU32(indices.length); | |
for (let i = 0; i < vertices.length; i += 3) { | |
inputVertices.set(i / 3, new px.PxVec3(vertices[i], vertices[i + 1], vertices[i + 2])); | |
} | |
for (let i = 0; i < indices.length; i++) { | |
inputIndices.set(i, indices[i]); | |
} | |
let outputVertices = new px.PxArray_PxVec3(); | |
let outputIndices = new px.PxArray_PxU32(); | |
px.PxTetMaker.prototype.simplifyTriangleMesh(inputVertices, inputIndices, targetTriangleCount, maximalTriangleEdgeLength, outputVertices, outputIndices); | |
console.log(inputVertices.size(), inputIndices.size(), outputVertices.size(), outputIndices.size()); | |
// Transform From PxVec3 to THREE.Vector3 | |
let triIndices = new Uint32Array(outputIndices.size()); | |
for (let i = 0; i < triIndices.length; i++) { | |
triIndices[i] = outputIndices.get(i); | |
} | |
let vertPositions = new Float32Array(outputVertices.size() * 3); | |
for (let i = 0; i < outputVertices.size(); i++) { | |
let vec3 = outputVertices.get(i); | |
vertPositions[i * 3 + 0] = vec3.get_x(); | |
vertPositions[i * 3 + 1] = vec3.get_y(); | |
vertPositions[i * 3 + 2] = vec3.get_z(); | |
} | |
inputVertices.__destroy__(); | |
inputIndices.__destroy__(); | |
outputVertices.__destroy__(); | |
outputIndices.__destroy__(); | |
return { position: vertPositions, index: triIndices }; | |
} | |
static createConformingTetrahedronMesh(mesh, minTetVolume = 0.01) { | |
const vertices = mesh.position; | |
const indices = mesh.index; | |
// First need to get the data into PhysX | |
let inputVertices = new px.PxArray_PxVec3(vertices.length / 3); | |
let inputIndices = new px.PxArray_PxU32(indices.length); | |
for (let i = 0; i < vertices.length; i += 3) { | |
inputVertices.set(i / 3, new px.PxVec3(vertices[i], vertices[i + 1], vertices[i + 2])); | |
} | |
for (let i = 0; i < indices.length; i++) { | |
inputIndices.set(i, indices[i]); | |
if (indices[i] < 0 || indices[i] >= inputVertices.size()) { | |
console.log("Index out of range!", i, indices[i], inputVertices.size()); | |
} | |
} | |
// Next need to make the PxBoundedData for both the vertices and indices to make the 'Simple'TriangleMesh | |
let vertexData = new px.PxBoundedData(); | |
let indexData = new px.PxBoundedData(); | |
vertexData.set_count(inputVertices.size()); | |
vertexData.set_data(inputVertices.begin()); | |
indexData.set_count(inputIndices.size() / 3); | |
indexData.set_data(inputIndices.begin()); | |
let simpleMesh = new px.PxSimpleTriangleMesh(); | |
simpleMesh.set_points(vertexData); | |
simpleMesh.set_triangles(indexData); | |
let analysis = px.PxTetMaker.prototype.validateTriangleMesh(simpleMesh); | |
if (!analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eVALID) || analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eMESH_IS_INVALID)) { | |
console.log("eVALID", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eVALID), | |
"\neZERO_VOLUME", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eZERO_VOLUME), | |
"\neOPEN_BOUNDARIES", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eOPEN_BOUNDARIES), | |
"\neSELF_INTERSECTIONS", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eSELF_INTERSECTIONS), | |
"\neINCONSISTENT_TRIANGLE_ORIENTATION", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eINCONSISTENT_TRIANGLE_ORIENTATION), | |
"\neCONTAINS_ACUTE_ANGLED_TRIANGLES", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eCONTAINS_ACUTE_ANGLED_TRIANGLES), | |
"\neEDGE_SHARED_BY_MORE_THAN_TWO_TRIANGLES", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eEDGE_SHARED_BY_MORE_THAN_TWO_TRIANGLES), | |
"\neCONTAINS_DUPLICATE_POINTS", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eCONTAINS_DUPLICATE_POINTS), | |
"\neCONTAINS_INVALID_POINTS", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eCONTAINS_INVALID_POINTS), | |
"\neREQUIRES_32BIT_INDEX_BUFFER", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eREQUIRES_32BIT_INDEX_BUFFER), | |
"\neTRIANGLE_INDEX_OUT_OF_RANGE", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eTRIANGLE_INDEX_OUT_OF_RANGE), | |
"\neMESH_IS_PROBLEMATIC", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eMESH_IS_PROBLEMATIC), | |
"\neMESH_IS_INVALID", analysis.isSet(px.PxTriangleMeshAnalysisResultEnum.eMESH_IS_INVALID)); | |
} | |
// Now we should be able to make the Conforming Tetrahedron Mesh | |
let outputVertices = new px.PxArray_PxVec3(); | |
let outputIndices = new px.PxArray_PxU32(); | |
px.PxTetMaker.prototype.createConformingTetrahedronMesh(simpleMesh, outputVertices, outputIndices, true, minTetVolume); | |
// Transform From PxVec3 to THREE.Vector3 | |
let tetIndices = new Uint32Array(outputIndices.size()); | |
for (let i = 0; i < tetIndices.length; i++) { | |
tetIndices[i] = outputIndices.get(i); | |
} | |
// Transform from Tet Indices to Edge Indices | |
let segIndices = new Uint32Array((outputIndices.size() / 4) * 12); | |
for (let i = 0; i < outputIndices.size() / 4; i++) { | |
let a = outputIndices.get(i * 4 + 0); | |
let b = outputIndices.get(i * 4 + 1); | |
let c = outputIndices.get(i * 4 + 2); | |
let d = outputIndices.get(i * 4 + 3); | |
segIndices[i * 12 + 0] = a; | |
segIndices[i * 12 + 1] = b; | |
segIndices[i * 12 + 2] = a; | |
segIndices[i * 12 + 3] = c; | |
segIndices[i * 12 + 4] = a; | |
segIndices[i * 12 + 5] = d; | |
segIndices[i * 12 + 6] = b; | |
segIndices[i * 12 + 7] = c; | |
segIndices[i * 12 + 8] = b; | |
segIndices[i * 12 + 9] = d; | |
segIndices[i * 12 + 10] = c; | |
segIndices[i * 12 + 11] = d; | |
} | |
let vertPositions = new Float32Array(outputVertices.size() * 3); | |
for (let i = 0; i < outputVertices.size(); i++) { | |
let vec3 = outputVertices.get(i); | |
vertPositions[i * 3 + 0] = vec3.get_x(); | |
vertPositions[i * 3 + 1] = vec3.get_y(); | |
vertPositions[i * 3 + 2] = vec3.get_z(); | |
} inputVertices.__destroy__(); | |
inputIndices.__destroy__(); | |
vertexData.__destroy__(); | |
indexData.__destroy__(); | |
simpleMesh.__destroy__(); | |
outputVertices.__destroy__(); | |
outputIndices.__destroy__(); | |
return { | |
faceIDs: indices, | |
numTets: tetIndices.length / 4, | |
numTetEdges: segIndices.length / 2, | |
vertices: vertPositions, | |
tetIDs: tetIndices, | |
tetEdgeIDs: segIndices | |
}; | |
} | |
static generateTetMesh(geo, options) { | |
const meshingParams = { | |
RemeshResolution: 20, | |
TargetTriangles: 2000, | |
MaxTriangleEdgeLength: 50.0, | |
MinTetVolume: 0.00001 | |
} | |
Object.assign(meshingParams, options); | |
const opt = { | |
remesh: false, | |
simplify: false | |
} | |
Object.assign(opt, options); | |
let index = geo.getIndex()?.array; | |
if (!index) { | |
index = new Uint32Array(geo.getAttribute("position").array.length / 3); | |
for (let i = 0; i < index.length; i++) { index[i] = i; } | |
} | |
let meshData = { position: new Float32Array(geo.getAttribute("position").array), index } | |
if (opt.remesh) | |
meshData = this.remesh(meshData, meshingParams.RemeshResolution); | |
if (opt.simplify) | |
meshData = this.simplifyMesh(meshData, meshingParams.TargetTriangles, meshingParams.MaxTriangleEdgeLength); | |
const tetrahedronGeo = this.createConformingTetrahedronMesh(meshData, meshingParams.MinTetVolume); | |
return tetrahedronGeo; | |
} | |
} | |
function CreateSoftMesh(tetrahedronGeo, edgeCompliance, volumeCompliance) { | |
// Create settings | |
const sharedSettings = new Jolt.SoftBodySharedSettings; | |
const v = new Jolt.SoftBodySharedSettingsVertex; | |
for (let i = 0; i < tetrahedronGeo.vertices.length; i+= 3) { | |
v.mPosition.x = tetrahedronGeo.vertices[i + 0]; | |
v.mPosition.y = tetrahedronGeo.vertices[i + 1]; | |
v.mPosition.z = tetrahedronGeo.vertices[i + 2]; | |
sharedSettings.mVertices.push_back(v); | |
} | |
Jolt.destroy(v); | |
// Function to get the vertex index of a point on the cloth | |
const vertex_index = (inX, inY, inZ) => { | |
return inX + inY * inGridSize + inZ * inGridSize * inGridSize; | |
}; | |
const sEdge = new Jolt.SoftBodySharedSettingsEdge(0, 0, 0); | |
sEdge.mCompliance = edgeCompliance; | |
// Create edges | |
for (let i = 0; i < tetrahedronGeo.tetEdgeIDs.length; i += 2) { | |
sEdge.set_mVertex(0, tetrahedronGeo.tetEdgeIDs[i + 0]); | |
sEdge.set_mVertex(1, tetrahedronGeo.tetEdgeIDs[i + 1]); | |
sharedSettings.mEdgeConstraints.push_back(sEdge); | |
} | |
Jolt.destroy(sEdge); | |
sharedSettings.CalculateEdgeLengths(); | |
// Create volume constraints | |
const sVol = new Jolt.SoftBodySharedSettingsVolume(0, 0, 0, 0, 0); | |
sVol.mCompliance = volumeCompliance; | |
for (let i = 0; i < tetrahedronGeo.tetIDs.length; i += 4) { | |
sVol.set_mVertex(0, tetrahedronGeo.tetIDs[i + 0]); | |
sVol.set_mVertex(1, tetrahedronGeo.tetIDs[i + 1]); | |
sVol.set_mVertex(2, tetrahedronGeo.tetIDs[i + 2]); | |
sVol.set_mVertex(3, tetrahedronGeo.tetIDs[i + 3]); | |
sharedSettings.mVolumeConstraints.push_back(sVol); | |
} | |
Jolt.destroy(sVol); | |
sharedSettings.CalculateVolumeConstraintVolumes(); | |
// Create faces | |
const f = new Jolt.SoftBodySharedSettingsFace(0, 0, 0, 0); | |
for (let i = 0; i < tetrahedronGeo.faceIDs.length; i += 3) { | |
// Face 1 | |
f.set_mVertex(0, tetrahedronGeo.faceIDs[i + 0]); | |
f.set_mVertex(1, tetrahedronGeo.faceIDs[i + 1]); | |
f.set_mVertex(2, tetrahedronGeo.faceIDs[i + 2]); | |
sharedSettings.AddFace(f); | |
} | |
Jolt.destroy(f); | |
// Optimize the settings | |
sharedSettings.Optimize(); | |
return sharedSettings; | |
} | |
let position, rotAxis, rotation; | |
let body, sharedSettings | |
async function processMesh() { | |
if (!px) { | |
await loadPx(); | |
position = new Jolt.RVec3(); | |
rotAxis = new Jolt.Vec3(1, 0, 0); | |
rotation = Jolt.Quat.prototype.sRotation(rotAxis, -Math.PI / 4); | |
} | |
const softGeo = PhysXTool.generateTetMesh(mesh.geometry, {remesh: true, simplify: true, RemeshResolution: parseInt(remeshRes.value)}); | |
if(sharedSettings) { | |
removeFromScene(dynamicObjects[dynamicObjects.length - 1]); | |
} | |
sharedSettings = CreateSoftMesh(softGeo, 5e-5, 1e-6); | |
position.Set(0, 10, 0) | |
const bodyCreationSettings = new Jolt.SoftBodyCreationSettings(sharedSettings, position, rotation, LAYER_MOVING); | |
bodyCreationSettings.mPressure = 2; | |
bodyCreationSettings.mObjectLayer = LAYER_MOVING; | |
body = bodyInterface.CreateSoftBody(bodyCreationSettings); | |
addToScene(body, 0xff00ff); | |
Jolt.destroy(bodyCreationSettings); | |
} | |
let material = new THREE.MeshPhongMaterial({ color: 0xff00ff }); | |
function loadObj(text) { | |
if (mesh) { | |
scene.remove(mesh); | |
} | |
const obj = loader.parse(text); | |
const geometry= []; | |
obj.traverse((x) => { if(x.isMesh) geometry.push(x.geometry.scale(10,10,10))}); | |
const geo = BufferGeometryUtils.mergeGeometries ( geometry, false ); | |
mesh = new THREE.Mesh(geo, material) | |
mesh.position.x -= 10; | |
mesh.position.y += 10; | |
scene.add(mesh); | |
processMesh(); | |
} | |
const reader = new FileReader(); | |
filePrompt.onchange = e => { | |
const file = e.target.files[0]; | |
if (file) { | |
reader.readAsText(file, 'UTF-8'); | |
reader.onload = (readerEvent) => { | |
const content = readerEvent.target.result; | |
loadObj(content) | |
} | |
} | |
} | |
document.getElementById('open-file').onclick = () => { | |
filePrompt.click(); | |
} | |
initJolt().then(function (Jolt) { | |
if(Jolt.DebugRendererJS) { | |
// Initialize this example | |
const debugRendererWidget = new RenderWidget(Jolt); | |
initExample(Jolt, () => { | |
debugRendererWidget.render(); | |
}); | |
debugRendererWidget.init(); | |
camera.layers.mask = 1 | |
document.body.appendChild(debugRendererWidget.domElement); | |
} else { | |
initExample(Jolt); | |
} | |
// Create a basic floor | |
let floor = createFloor(); | |
floor.SetFriction(1.0); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment