Skip to content

Instantly share code, notes, and snippets.

@PhoenixIllusion
Created April 25, 2024 22:51
Show Gist options
  • Save PhoenixIllusion/561e36d75471e2217a5b2a8d8f22683e to your computer and use it in GitHub Desktop.
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
<!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/JoltPhysics.js@0.23.0/Examples///style.css">
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.163.0/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/JoltPhysics.js@0.23.0/Examples//js/three/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/jrouwe/JoltPhysics.js@0.23.0/Examples//js/three/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/gh/jrouwe/JoltPhysics.js@0.23.0/Examples//js/three/WebGL.js"></script>
<script src="https://cdn.jsdelivr.net/gh/jrouwe/JoltPhysics.js@0.23.0/Examples//js/three/stats.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/jrouwe/JoltPhysics.js@0.23.0/Examples//js/example.js"></script>
<script src="https://cdn.jsdelivr.net/gh/jrouwe/JoltPhysics.js@0.23.0/Examples//js/three/CSS3DRenderer.js"></script>
<script src="https://cdn.jsdelivr.net/gh/jrouwe/JoltPhysics.js@0.23.0/Examples//js/debug-renderer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/physx-js-webidl@2.3.1/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