3D Boolean operations with Three CSG - three.js

Following the example here:
http://learningthreejs.com/blog/2011/12/10/constructive-solid-geometry-with-csg-js/
And using Three.js with https://github.com/chandlerprall/ThreeCSG, I'm trying to do 3D boolean operations on nodes from the model. Like for example if I have a wall with a window, I want to do invert() on that to get just the window.
I have a function that returns all the vertices of the polygons of a node, here's an example of vertices of an object without holes https://pastebin.com/8dhYzPwE.
I'm using ThreeCSG like this:
const geometryThree = new THREE.Geometry();
geometryThree.vertices.push(
...vertices
);
const geometryCsg = new ThreeBSP(geometryThree);
But that's what I'm getting in geometryCsg:
"{
"matrix": {
"elements": {
"0": 1,
"1": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 1,
"6": 0,
"7": 0,
"8": 0,
"9": 0,
"10": 1,
"11": 0,
"12": 0,
"13": 0,
"14": 0,
"15": 1
}
},
"tree": {
"polygons": []
}
}"
I think it's because the geometry.faces.length is 0.
How can I make the vertices array to be a proper Three.Geometry such that the faces won't be empty? Geometry.elementsNeedsUpdate doesn't work...
Is there an example that uses polygons of a shape as an array of Vector3s and transforms that to csg?

I just worked on a demo using THREE csg: the Viewer meshes have an indexed array of vertices so you cannot create a BSP directly out of it. Also my code is using a web worker to process the meshes in order to keep the UI responsive with large models, so I need first to send the mesh data to the worker and reconstruct a simple THREE.Mesh on the worker side, the code looks like below:
// Sends component geometry to the web worker
postComponent (dbId) {
const geometry = this.getComponentGeometry(dbId)
const msg = {
boundingBox: this.getComponentBoundingBox(dbId),
matrixWorld: geometry.matrixWorld,
nbMeshes: geometry.meshes.length,
msgId: 'MSG_ID_COMPONENT',
dbId
}
geometry.meshes.forEach((mesh, idx) => {
msg['positions' + idx] = mesh.positions
msg['indices' + idx] = mesh.indices
msg['stride' + idx] = mesh.stride
})
this.worker.postMessage(msg)
}
// get geometry for all fragments in a component
getComponentGeometry (dbId) {
const fragIds = Toolkit.getLeafFragIds(
this.viewer.model, dbId)
let matrixWorld = null
const meshes = fragIds.map((fragId) => {
const renderProxy = this.viewer.impl.getRenderProxy(
this.viewer.model,
fragId)
const geometry = renderProxy.geometry
const attributes = geometry.attributes
const positions = geometry.vb
? geometry.vb
: attributes.position.array
const indices = attributes.index.array || geometry.ib
const stride = geometry.vb ? geometry.vbstride : 3
const offsets = geometry.offsets
matrixWorld = matrixWorld ||
renderProxy.matrixWorld.elements
return {
positions,
indices,
offsets,
stride
}
})
return {
matrixWorld,
meshes
}
}
// On the worker side reconstruct THREE.Mesh
// from received data and create ThreeBSP
function buildComponentMesh (data) {
const vertexArray = []
for (let idx=0; idx < data.nbMeshes; ++idx) {
const meshData = {
positions: data['positions' + idx],
indices: data['indices' + idx],
stride: data['stride' + idx]
}
getMeshGeometry (meshData, vertexArray)
}
const geometry = new THREE.Geometry()
for (var i = 0; i < vertexArray.length; i += 3) {
geometry.vertices.push(vertexArray[i])
geometry.vertices.push(vertexArray[i + 1])
geometry.vertices.push(vertexArray[i + 2])
const face = new THREE.Face3(i, i + 1, i + 2)
geometry.faces.push(face)
}
const matrixWorld = new THREE.Matrix4()
matrixWorld.fromArray(data.matrixWorld)
const mesh = new THREE.Mesh(geometry)
mesh.applyMatrix(matrixWorld)
mesh.boundingBox = data.boundingBox
mesh.bsp = new ThreeBSP(mesh)
mesh.dbId = data.dbId
return mesh
}
function getMeshGeometry (data, vertexArray) {
const offsets = [{
count: data.indices.length,
index: 0,
start: 0}
]
for (var oi = 0, ol = offsets.length; oi < ol; ++oi) {
var start = offsets[oi].start
var count = offsets[oi].count
var index = offsets[oi].index
for (var i = start, il = start + count; i < il; i += 3) {
const a = index + data.indices[i]
const b = index + data.indices[i + 1]
const c = index + data.indices[i + 2]
const vA = new THREE.Vector3()
const vB = new THREE.Vector3()
const vC = new THREE.Vector3()
vA.fromArray(data.positions, a * data.stride)
vB.fromArray(data.positions, b * data.stride)
vC.fromArray(data.positions, c * data.stride)
vertexArray.push(vA)
vertexArray.push(vB)
vertexArray.push(vC)
}
}
}
The complete code of my sample is there: Wall Analyzer and the live demo there.

Related

Ammo Threejs Breakable objects debis set material after breaking in fragments

i have initially breakable object with nice texture material loaded.
After breaking in fragments lost original material and load some default material.
Any suggest?
I use code from threejs example with ammo:
Source code:
import * as THREE from "three";
import {ConvexObjectBreaker} from "../jsm/misc/ConvexObjectBreaker";
import {updatePhysics} from "./updater";
export class MagicPhysics {
// Physics variables
gravityConstant = 7.8;
collisionConfiguration;
dispatcher;
broadphase;
solver;
physicsWorld;
margin = 0.05;
convexBreaker = new ConvexObjectBreaker();
// Rigid bodies include all movable objects
rigidBodies = [];
pos = new THREE.Vector3();
quat = new THREE.Quaternion();
transformAux1;
tempBtVec3_1;
objectsToRemove = [];
// Player
ammoTmpPos;
ammoTmpQuat;
tmpTrans;
numObjectsToRemove = 0;
impactPoint = new THREE.Vector3();
impactNormal = new THREE.Vector3();
// kinekt type of movement
kMoveDirection = {left: 0, right: 0, forward: 0, back: 0};
// velocity type of movement
moveDirection = {left: 0, right: 0, forward: 0, back: 0};
tmpPos = new THREE.Vector3();
tmpQuat = new THREE.Quaternion();
constructor(options) {
console.log("MagicPhysics =>", options)
this.updatePhysics = updatePhysics.bind(this);
this.config = options.config;
}
initPhysics() {
// Physics configuration
this.collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
this.dispatcher = new Ammo.btCollisionDispatcher(this.collisionConfiguration);
this.broadphase = new Ammo.btDbvtBroadphase();
const solver = new Ammo.btSequentialImpulseConstraintSolver();
this.physicsWorld = new Ammo.btDiscreteDynamicsWorld(
this.dispatcher,
this.broadphase,
solver,
this.collisionConfiguration
);
this.physicsWorld.setGravity(new Ammo.btVector3(0, -this.gravityConstant, 0));
this.transformAux1 = new Ammo.btTransform();
this.tempBtVec3_1 = new Ammo.btVector3(0, 0, 0);
}
createRigidBody(object, physicsShape, mass, pos, quat, vel, angVel) {
if(pos) {
object.position.copy(pos);
} else {
pos = object.position;
}
if(quat) {
object.quaternion.copy(quat);
} else {
quat = object.quaternion;
}
const transform = new Ammo.btTransform();
transform.setIdentity();
transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
transform.setRotation(
new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w)
);
const motionState = new Ammo.btDefaultMotionState(transform);
const localInertia = new Ammo.btVector3(0, 0, 0);
physicsShape.calculateLocalInertia(mass, localInertia);
const rbInfo = new Ammo.btRigidBodyConstructionInfo(
mass,
motionState,
physicsShape,
localInertia
);
const body = new Ammo.btRigidBody(rbInfo);
body.setFriction(0.5);
if(vel) {
body.setLinearVelocity(new Ammo.btVector3(vel.x, vel.y, vel.z));
}
if(angVel) {
body.setAngularVelocity(
new Ammo.btVector3(angVel.x, angVel.y, angVel.z)
);
}
object.userData.physicsBody = body;
object.userData.collided = false;
this.scene.add(object);
if(mass > 0) {
this.rigidBodies.push(object);
// Disable deactivation
body.setActivationState(4);
}
this.physicsWorld.addRigidBody(body);
return body;
}
createConvexHullPhysicsShape(coords) {
const shape = new Ammo.btConvexHullShape();
for(let i = 0, il = coords.length;i < il;i += 3) {
this.tempBtVec3_1.setValue(coords[i], coords[i + 1], coords[i + 2]);
const lastOne = i >= il - 3;
shape.addPoint(this.tempBtVec3_1, lastOne);
}
return shape;
}
createParalellepipedWithPhysics(sx, sy, sz, mass, pos, quat, material) {
const object = new THREE.Mesh(
new THREE.BoxGeometry(sx, sy, sz, 1, 1, 1),
material
);
const shape = new Ammo.btBoxShape(
new Ammo.btVector3(sx * 0.5, sy * 0.5, sz * 0.5)
);
shape.setMargin(this.margin);
this.createRigidBody(object, shape, mass, pos, quat);
return object;
}
createDebrisFromBreakableObject(object) {
object.castShadow = true;
object.receiveShadow = true;
const shape = this.createConvexHullPhysicsShape(
object.geometry.attributes.position.array
);
shape.setMargin(this.margin);
const body = this.createRigidBody(
object,
shape,
object.userData.mass,
null,
null,
object.userData.velocity,
object.userData.angularVelocity
);
// Set pointer back to the three object only in the debris objects
const btVecUserData = new Ammo.btVector3(0, 0, 0);
btVecUserData.threeObject = object;
body.setUserPointer(btVecUserData);
}
removeDebris(object) {
this.scene.remove(object);
this.physicsWorld.removeRigidBody(object.userData.physicsBody);
}
}

Is adding the new data point not at the end of array a bad idea?

This involves a design decision. An interviewer asked me to write something to plot the data assuming there are 100 data points, and new data point comes in (and given to the program) every 0.1, 0.3 or 0.5 or 1 second. (it can change in the future, and I think the smallest granularity on a common web browser is 0.03 seconds).
I proceeded to think about adding the new data point to the Nth position in the array. For example, adding the data at array entry 36, and plot the data from 37th to 99th, and then from 0th to 36th. Next time, add the data at array entry 37, and plot the data from 38th to 99th, and then from 0th to 37th.
This way, we don't need to "unshift" (shift out) the data at entry 0 and therefore needing to shift entry 1 to 99 one place forward, and then add the new data point at entry 99, and then plot data entry 0 to 99.
For some reason, the interviewer gave a big frown, and said, "why would we do that? It is not heavy to shift 99 data over." I said what if there are 500 or 1000 data point we'd like to plot in the future, we might want to avoid shifting data about 500 times or 1000 time each time when a new data point comes in.
He mentioned "let's say we just shift them anyway".
Is shifting the data actually not an issue or concern? What if on the screen we have 10 or 15 of such widgets, apps, or webpages, to monitor 15 types of data, we might want to avoid shifting 15,000 data entries constantly.
I naively tried to load n canvas to the page, and compared the time needed to plot against the time taken shifting array.
tldr: Whichever method is used to shift the points, the method is negligible against the time needed to plot the points. (~ 3%)
I only run the code in ff70 with casual js.
(For instance I am stocking an array of objects even though optimization may be available if I stock only floats)
There are two kind of measures: push and refresh.
push measures the time needed to shift a point and add a new one
refresh measures the time needed to replot the canvas
Below three approaches for pushing: either push (Array.prototype.(shift|push), tail to a queue (and move the head), or nopole's approach
Every 10ms I plot the time spent in the push method. On top of the picture, the cumulative time spent. I stop once a run has reached 100 points and reload the page for another run.
The y axis is the same accross all runs
Push
push avg: (838+886+864)/3 = 862ms
Queue
push avg: (625+760+825)/3 = 736ms
refresh avg: (40554+39934+40915+39194+39264+30480)/6 = 38390ms
Nopole
push avg: (792+861+871)/3 = 841ms
Notice that for one sample: (625/30480) we seem to have benefited from some cpu willing to work. So the shifting method feels even more irrelevant.
It is hard to tell which approach is better, because of the few samples drought for each kind of methods and it is likely more an issue of cpu's overall workload rather than the page itself
To reproduce
let timerPush = 0
let timerRefresh = 0
class Canvas {
constructor (f, el, period) {
this.w = 300
this.h = 300
this.points = []
const canvas = document.createElement('canvas')
canvas.style.cssText = 'background:#eeeeee; margin:10px;'
canvas.width = this.w
canvas.height = this.h
this.ctx = canvas.getContext('2d')
this.ctx.transform(1, 0, 0, -1, 0, this.h / 2)
this.ctx.lineWidth = 1
this.dw = this.w / this.MAX_POINTS
this.dh = this.h / 2
el.appendChild(canvas)
let x = 0
this.timer = setInterval(_ => {
x += period
this.push({ x, y: f(x) })
this.refresh()
}, period * 1000)
}
refresh () {
const now = performance.now()
this.ctx.clearRect(0, -this.h / 2, this.w, this.h)
this.ctx.beginPath()
this._plot()
this.ctx.stroke()
this.ctx.closePath()
timerRefresh += performance.now() - now
}
push (p) {
const now = performance.now()
this._push(p)
timerPush += performance.now() - now
}
_plot () {
if (!this.points.length) { return }
this.ctx.moveTo(0 * this.dw, this.points[0].y * this.dh)
for (let i = 1; i < this.points.length; ++i) {
const p = this.points[i]
this.ctx.lineTo(i * this.dw, p.y * this.dh)
}
}
_push (p) {
if (this.points.length == this.MAX_POINTS) {
this.points.shift()
}
this.points.push(p)
}
MAX_POINTS = 100
}
class CanvasQueue extends Canvas {
constructor () {
super(...arguments)
this.tail = {}
this.head = this.tail
this.n = 0
}
_plot () {
if (!this.head.next.p) return
let node = this.head.next
this.ctx.moveTo(0 * this.dw, node.p.y * this.dh)
let i = 1
node = node.next
while (node) {
this.ctx.lineTo(i * this.dw, node.p.y * this.dh)
++i
node = node.next
}
}
_push (p) {
if (this.n === this.MAX_POINTS) {
this.head = this.head.next
} else {
this.n++
}
const node = { p }
this.tail.next = node
this.tail = node
}
}
class CanvasNopole extends Canvas {
constructor () {
super(...arguments)
this.start = 0
}
_plot () {
if (!this.points.length) { return }
const s = this.start
let z = 1
let startBack = 0
if (this.points[s]){
this.ctx.moveTo(0 * this.dw, this.points[s].y * this.dh)
for (let i = s+1; i < this.points.length; ++i) {
const p = this.points[i]
this.ctx.lineTo(z++ * this.dw, p.y * this.dh)
}
}else{
this.ctx.moveTo(0 * this.dw, this.points[0].y * this.dh)
startBack = 1
}
for (let i = startBack; i < s; ++i) {
const p = this.points[i]
this.ctx.lineTo(z++ * this.dw, p.y * this.dh)
}
}
_push (p) {
this.points[this.start] = p
this.start = (this.start + 1) % this.MAX_POINTS
}
}
class CanvasSummary extends Canvas {
constructor () {
super(...arguments)
this.ctx.resetTransform()
this.ctx.transform(1, 0, 0, -1, 0, this.h)
// we know beforehand that timer should not grow bigger
const deltaTimer = 50
this.dh = this.h / deltaTimer
this.old = timerPush
}
refresh () {
this.ctx.clearRect(0, 0, this.w, this.h)
this.ctx.beginPath()
this.ctx.resetTransform()
this.ctx.fillText(`push: ${timerPush} plot: ${timerRefresh}`, 5, 20)
this.ctx.transform(1, 0, 0, -1, 0, this.h)
this._plot()
this.ctx.stroke()
this.ctx.closePath()
}
push (p) {
this._push(p)
}
}
function run () {
const $summary = document.querySelector('.summary')
const $bench = document.querySelector('.bench')
const cs = new CanvasSummary(x => {
if (cs.points.length === cs.MAX_POINTS) {
clearInterval(cs.timer)
}
const y = timerPush - cs.old
cs.old = timerPush
return y
}, $summary, 1)
//const canvas = Array(30).fill(0).map(x => new Canvas(Math.sin, $bench, 0.01))
//const canvas = Array(30).fill(0).map(x => new CanvasQueue(Math.sin, $bench, 0.01))
const canvas = Array(30).fill(0).map(x => new CanvasNopole(Math.sin, $bench, 0.01))
}
run()
<section class="summary"></section>
<hr/>
<div class="bench"></div>

Animation doesn't show steps-in-between

I have created a simple function that would "animate" the cell backcolor at a tap, it works perfectly fine:
Color nOldColor = _grid.BackgroundColor;
for (int i = 0; i <= 100; i += 5)
{
double f = (double)i / (double)100;
Color nNewColor = PCLHelper.BlendColors(nOldColor, Color.Red, f);
_grid.BackgroundColor = nNewColor;
_label1.BackgroundColor = nNewColor;
await Task.Delay(5);
}
_grid.BackgroundColor = nOldColor;
_label1.BackgroundColor = nOldColor;
Now I wanted to do the same with an Animation, but the animation doesn't show the steps "in-between" but rather (as it looks to me) switches to the final color:
private async void animateButtonTouched()
{
int repeatCountMax = 100;
Color nOldColor = _grid.BackgroundColor;
var repeatCount = 0;
_grid.Animate("changeBG", new Animation((val) =>
{
double f = (double)repeatCount / (double)100;
Color nNewColor = PCLHelper.BlendColors(nOldColor, Color.Red, f);
_grid.BackgroundColor = nNewColor;
_label1.BackgroundColor = nNewColor;
}),
5, //duration. I've also tried it with 100. Nothing helped
finished: (val, b) =>
{
repeatCount++;
}, repeat: () =>
{
return repeatCount < repeatCountMax;
});
What am I doing wrong?
"You are making it more difficult than it needs to be." Trademark pending 🍣
The Animate callback is providing the stepping value (or keyframe value). This is a double from 0 to 1 that is called ever X milliseconds (i.e. the length of a single animation frame, 16 default) over the course of X milliseconds (250 default).
So in this example the ShiftToColor gets called 125 times (2000 / 16) with a value that is evenly divided from 0 to 1, thus steps of .008.
var orgColor = aFormsElementInstance.BackgroundColor;
aFormsElementInstance.Animate("changeBG", new Animation((val) =>
{
Color ShiftToColor(Color From, Color To, double pct)
{
var r = From.R + ((To.R - From.R) * val);
var g = From.G + ((To.G - From.G) * val);
var b = From.B + ((To.B - From.B) * val);
return new Color(r, g, b);
}
Device.BeginInvokeOnMainThread(() =>
{
aFormsElementInstance.BackgroundColor = ShiftToColor(orgColor, Color.Red, val);
});
}), 16, 2000);
Results in:

How to convert an closed path SVG to a 0-1 array

Lets say there is a svg with a closed filled rectangle in the middle and around that there's a white space of 2 points .
<path d="M2 2 H 3 V 3 H 2 Z" fill="transparent" stroke="black"/>
So I want to represent this a 2-d matrix where all the white space are represented as 0 and black spaces (covered area) is represented as 1. so for this example it should be-
[
[0, 0, 0, 0],
[0, 1, 1, 1],
[0, 1, 1, 1],
[0, 1, 1, 1]
]
It's a simple path , but I'm trying to find a way where it would work for complex paths including bezier curve. Actually I'm trying to convert an SVG world map to 0-1 matrix so that I can run some AI algorithms on it .
Implemented #Robert Longson suggestion. 1) Draw the svg in canvas 2) Get ImageData as CanvasContext Array 3) Iterate on that array and form your matrix. 4) Array returned by getImageData is a flat array and consecutive 4 array index correspond to one point of canvas and they are r, g, b and alpha (rgba) of the color of that point.
Here's a working react component .
import React, { Component } from 'react';
export default class IndexPage extends Component {
constructor(properties) {
super(properties);
this.canvasWidth = 1052;
this.canvasHeight = 580;
}
componentDidMount() {
const mapCanvas = this.refs.canvas;
const ctx = mapCanvas.getContext('2d');
const img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
this.arrayFromSvg();
}.bind(this);
img.src = 'World.svg';
}
render() {
return ( < div >
< div styles={{
width: this.canvasWidth,
height: this.canvasHeight
}
} >
< canvas width = {
this.canvasWidth
}
height = {
this.canvasHeight
}
ref = "canvas" >
< /canvas> < /div >
< /div>
);
}
arrayFromSvg() {
const mapCanvas = this.refs.canvas;
const ctx = mapCanvas.getContext('2d');
const canvasWidth = mapCanvas.width;
const canvasHeight = mapCanvas.height;
const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight).data;
const imageToMat = [];
for (let row = 0, count = -1; row < canvasWidth; row++) {
imageToMat[row] = [];
// imageToMat[row][col] = 'rgba(' + imageData[++count] + ', ' + imageData[++count] + ', ' + imageData[++count] + ', ' + imageData[++count] + ')';
for (let col = 0; col < canvasHeight; col++) {
if (imageData[++count] + imageData[++count] + imageData[++count] + imageData[++count] > 0) {
imageToMat[row][col] = 1;
} else {
imageToMat[row][col] = 0;
}
}
}
console.log(imageToMat);
}
}

Three.js - can I detect geometry 'islands' when importing?

I'm importing .3DS models into Blender 2.72b, then exporting them with the Three.js import/export addon. The models have multiple geometry 'islands' (separate groups of connected faces and vertices), each with its own material. I'd like to be able to pair each material with its corresponding island, without having to create separate THREE.Geometry objects. After some digging, I found this question which suggests using a THREE.MeshFaceMaterial to achieve multiple materials for one object. The only problem is that the geometry in that example is a simple cube, whereas my models have hundreds of faces spread across 2-5 islands.
Does Three.js have functionality for identifying geometry 'islands' in a mesh?
No. three.js does not have functionality for identifying geometry 'islands' in a mesh.
When using MeshFaceMaterial, WebGLRenderer breaks the geometry into chunks anyway -- one chunk for each material. It does that because WebGL supports one shader per geometry.
I would not merge all your geometries, and then use MeshFaceMaterial, only to have the renderer break the single geometry apart.
You can merge geometries that share a single material if you wish.
three.js r.69
I tried with a function but still is not accurate, it produce more geometries than non connected geometries:
If somebody could have a look on it, it would be grate.
function groupGeometryIntoNonConnectedGeometries(geometry) {
const material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
vertexColors: THREE.VertexColors
});
let geometryArray = [];
const indexArray = geometry.index.array;
const positionArray = geometry.attributes.position.array;
const positionCount = geometry.attributes.position.count;
const color = new THREE.Vector3(geometry.attributes.color.array[0], geometry.attributes.color.array[1], geometry.attributes.color.array[2]);
const totalTriangles = indexArray.length / 3;
let geometryCount = 0;
let indexValueAlreadyVisited = new Uint8Array(indexArray.length);
let structure = [];
/*
* indexValue: {
* child: [ [indexval0, indexval1], [] ],
* parent: null
* }
*/
// Initialize Structure:
for (var vextexIdx=0; vextexIdx<positionCount; vextexIdx++) {
structure[vextexIdx] = {
child: [],
parent: null
}
}
for (idx=0; idx<totalTriangles; idx++) {
const geoIndex1 = indexArray[idx*3];
const geoIndex2 = indexArray[idx*3+1];
const geoIndex3 = indexArray[idx*3+2];
const triangleIndexVertexArray = [ geoIndex1, geoIndex2, geoIndex3 ].sort(function(a, b) {
return a - b;
});
structure[ triangleIndexVertexArray[0] ].child.push(triangleIndexVertexArray[1], triangleIndexVertexArray[2]);
structure[ triangleIndexVertexArray[1] ].parent = triangleIndexVertexArray[0];
structure[ triangleIndexVertexArray[2] ].parent = triangleIndexVertexArray[0];
}
let count = 0;
let currentCount = 0;
let geometryStructureArray = [];
for (let strIdx=0; strIdx<structure.length; strIdx++) {
if (structure[strIdx].parent == null) {
currentCount = count;
geometryStructureArray[currentCount] = {
name: "G_" + currentCount,
indexMap: {},
currentIndex: 0,
indexArray: [],
positionArray: [],
colorArray: []
};
count += 1;
}
if (structure[strIdx].child.length > 0) {
const childLen = structure[strIdx].child.length / 2;
for (let childIdx=0; childIdx<childLen; childIdx++) {
const vertexIndex0 = strIdx;
const vertexIndex1 = structure[strIdx].child[childIdx*2];
const vertexIndex2 = structure[strIdx].child[childIdx*2+1];
const v0 = new THREE.Vector3( positionArray[strIdx*3], positionArray[strIdx*3+1], positionArray[strIdx*3+2] );
const v1 = new THREE.Vector3( positionArray[vertexIndex1*3], positionArray[vertexIndex1*3+1], positionArray[vertexIndex1*3+2] );
const v2 = new THREE.Vector3( positionArray[vertexIndex2*3], positionArray[vertexIndex2*3+1], positionArray[vertexIndex2*3+2] );
// check vertex0
if (geometryStructureArray[currentCount].indexMap[vertexIndex0] == undefined) {
geometryStructureArray[currentCount].indexMap[vertexIndex0] = geometryStructureArray[currentCount].currentIndex;
geometryStructureArray[currentCount].indexArray.push(geometryStructureArray[currentCount].currentIndex);
geometryStructureArray[currentCount].positionArray.push(v0.x, v0.y, v0.z);
geometryStructureArray[currentCount].colorArray.push(color.x, color.y, color.z);
geometryStructureArray[currentCount].currentIndex += 1;
} else {
geometryStructureArray[currentCount].indexArray.push(geometryStructureArray[currentCount].indexMap[vertexIndex0]);
}
// check vertex1
if (geometryStructureArray[currentCount].indexMap[vertexIndex1] == undefined) {
geometryStructureArray[currentCount].indexMap[vertexIndex1] = geometryStructureArray[currentCount].currentIndex;
geometryStructureArray[currentCount].indexArray.push(geometryStructureArray[currentCount].currentIndex);
geometryStructureArray[currentCount].positionArray.push(v1.x, v1.y, v1.z);
geometryStructureArray[currentCount].colorArray.push(color.x, color.y, color.z);
geometryStructureArray[currentCount].currentIndex += 1;
} else {
geometryStructureArray[currentCount].indexArray.push(geometryStructureArray[currentCount].indexMap[vertexIndex1]);
}
// check vertex1
if (geometryStructureArray[currentCount].indexMap[vertexIndex2] == undefined) {
geometryStructureArray[currentCount].indexMap[vertexIndex2] = geometryStructureArray[currentCount].currentIndex;
geometryStructureArray[currentCount].indexArray.push(geometryStructureArray[currentCount].currentIndex);
geometryStructureArray[currentCount].positionArray.push(v2.x, v2.y, v2.z);
geometryStructureArray[currentCount].colorArray.push(color.x, color.y, color.z);
geometryStructureArray[currentCount].currentIndex += 1;
} else {
geometryStructureArray[currentCount].indexArray.push(geometryStructureArray[currentCount].indexMap[vertexIndex2]);
}
}
}
}
// Convert to geometryArray:
const geometryStructureArrayLen = geometryStructureArray.length;
const object3d = new THREE.Object3D();
for (let geoIdx=0; geoIdx<geometryStructureArrayLen; geoIdx++) {
const geo = new THREE.BufferGeometry();
geo.name = "G_" + geoIdx;
const geoPositions = new Float32Array(geometryStructureArray[geoIdx].positionArray);
const geoColors = new Float32Array(geometryStructureArray[geoIdx].colorArray);
const geoIndices = new Uint32Array(geometryStructureArray[geoIdx].indexArray);
//console.log(geoIdx, "geoPositions: ", geoPositions);
//console.log(geoIdx, "geoColors: ", geoColors);
//console.log(geoIdx, "geoIndices: ", geoIndices);
geo.index = new THREE.BufferAttribute(geoIndices, 1, false);
geo.attributes.position = new THREE.BufferAttribute(geoPositions, 3, false);
geo.attributes.color = new THREE.BufferAttribute(geoColors, 3, true);
geo.computeBoundingSphere();
geo.computeBoundingBox();
const mesh = new THREE.Mesh(geo, material);
mesh.name = "M_" + geoIdx;
object3d.add(mesh);
}
//return [structure, geometryStructureArray, object3d, count];
return object3d;
}
Best regards
This is I think the correct way:
function unmergeGeometryArray(geometry) {
// Asumptions:
// geometry is BufferGeometry
// The geometry has no index duplicates (2 equal positions with different index) neither empty triangles, the geometry has been processed with mergeVertices function
// normal attribute is discarded, can be recomputed after, only color and position attributes are taken into account
const material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
vertexColors: THREE.VertexColors
});
const indexArray = geometry.index.array;
const positionArray = geometry.attributes.position.array;
const positionCount = geometry.attributes.position.count;
const totalTriangles = indexArray.length / 3;
let triangleVisitedArray = new Uint8Array(totalTriangles);
let indexVisitedArray = new Uint8Array(positionCount);
let indexToTriangleIndexMap = [];
let missingVertices = positionCount;
let missingTriangles = totalTriangles;
// Functions:
function computeTrianglesRecursive(index, out){
//console.log("- start of computeTriangles with index:", index);
if (indexVisitedArray[index] === 1 || missingVertices === 0 || missingTriangles === 0) {
return;
}
indexVisitedArray[index] = 1;
missingVertices -= 1;
let triangleIndexArray = indexToTriangleIndexMap[index];
for(let i=0; i<indexToTriangleIndexMap[index].length; i++) {
let triangleIndex = indexToTriangleIndexMap[index][i];
if (triangleVisitedArray[triangleIndex] === 0) {
triangleVisitedArray[triangleIndex] = 1
missingTriangles -= 1;
//console.log("-- index: ", index, "; i: ", i, "; triangleIndex: ", triangleIndex);
out.push(triangleIndex);
childIndex1 = indexArray[triangleIndex*3+1];
computeTriangles(childIndex1, out);
childIndex2 = indexArray[triangleIndex*3+2];
computeTriangles(childIndex2, out);
}
}
}
function computeTriangles(indexTocheck){
let out = [];
let startIndex = indexTocheck;
let indexToCheckArray = [indexTocheck];
let i = 0;
while (i<indexToCheckArray.length) {
let index = indexToCheckArray[i];
if (indexVisitedArray[index] == 0) {
indexVisitedArray[index] = 1;
missingVertices -= 1;
let triangleIndexArray = indexToTriangleIndexMap[index];
for(let j=0; j<indexToTriangleIndexMap[index].length; j++) {
let triangleIndex = indexToTriangleIndexMap[index][j];
if (triangleVisitedArray[triangleIndex] === 0) {
triangleVisitedArray[triangleIndex] = 1;
missingTriangles -= 1;
out.push(triangleIndex);
let rootIndex = indexArray[triangleIndex*3];
let child1Index = indexArray[triangleIndex*3+1];
let child2Index = indexArray[triangleIndex*3+2];
if (indexToCheckArray.indexOf(rootIndex) === -1) {
indexToCheckArray.push(rootIndex);
}
if (indexToCheckArray.indexOf(child1Index) === -1) {
indexToCheckArray.push(child1Index);
}
if (indexToCheckArray.indexOf(child2Index) === -1) {
indexToCheckArray.push(child2Index);
}
}
}
}
i +=1;
}
return out;
}
// In the first loop we reorder indices asc order + generate map
for (triangleIndex=0; triangleIndex<totalTriangles; triangleIndex++) {
const geoIndex1 = indexArray[triangleIndex*3];
const geoIndex2 = indexArray[triangleIndex*3+1];
const geoIndex3 = indexArray[triangleIndex*3+2];
const triangleIndexVertexArray = [ geoIndex1, geoIndex2, geoIndex3 ].sort(function(a, b) {
return a - b;
});
if (indexToTriangleIndexMap[geoIndex1] === undefined) {
indexToTriangleIndexMap[geoIndex1] = [triangleIndex];
} else {
indexToTriangleIndexMap[geoIndex1].push(triangleIndex);
}
if (indexToTriangleIndexMap[geoIndex2] === undefined) {
indexToTriangleIndexMap[geoIndex2] = [triangleIndex];
} else {
indexToTriangleIndexMap[geoIndex2].push(triangleIndex);
}
if (indexToTriangleIndexMap[geoIndex3] === undefined) {
indexToTriangleIndexMap[geoIndex3] = [triangleIndex];
} else {
indexToTriangleIndexMap[geoIndex3].push(triangleIndex);
}
//indexArray[triangleIndex*3] = triangleIndexVertexArray[0];
//indexArray[triangleIndex*3+1] = triangleIndexVertexArray[1];
//indexArray[triangleIndex*3+2] = triangleIndexVertexArray[2];
}
let geometryTriangleArray = [];
let index = 0;
while (index<indexToTriangleIndexMap.length && missingVertices>0 && missingTriangles>0){
let out = [];
if (indexVisitedArray[index] === 0) {
out = computeTriangles(index);
}
if (out.length > 0) {
geometryTriangleArray.push(out);
}
index += 1;
}
let geometryArray = [];
for (let i=0; i<geometryTriangleArray.length; i++) {
let out = {
positionArray: [],
colorArray: [],
indexArray: [],
indexMap: [],
currentIndex: 0
}
let triangleArray = geometryTriangleArray[i];
for (let j=0; j<triangleArray.length; j++) {
let triangleIndex = triangleArray[j];
let rootIndex = indexArray[triangleIndex*3];
if (out.indexMap[rootIndex] === undefined) {
out.indexMap[rootIndex] = out.currentIndex;
// add vertex position and color
out.positionArray.push(
geometry.attributes.position.array[rootIndex*3],
geometry.attributes.position.array[rootIndex*3+1],
geometry.attributes.position.array[rootIndex*3+2]
);
if (geometry.attributes.color != undefined) {
out.colorArray.push(
geometry.attributes.color.array[rootIndex*3],
geometry.attributes.color.array[rootIndex*3+1],
geometry.attributes.color.array[rootIndex*3+2]
);
}
out.currentIndex += 1;
}
let child1Index = indexArray[triangleIndex*3+1];
if (out.indexMap[child1Index] === undefined) {
out.indexMap[child1Index] = out.currentIndex;
// add vertex position and color
out.positionArray.push(
geometry.attributes.position.array[child1Index*3],
geometry.attributes.position.array[child1Index*3+1],
geometry.attributes.position.array[child1Index*3+2]
);
if (geometry.attributes.color != undefined) {
out.colorArray.push(
geometry.attributes.color.array[child1Index*3],
geometry.attributes.color.array[child1Index*3+1],
geometry.attributes.color.array[child1Index*3+2]
);
}
out.currentIndex += 1;
}
let child2Index = indexArray[triangleIndex*3+2];
if (out.indexMap[child2Index] === undefined) {
out.indexMap[child2Index] = out.currentIndex;
// add vertex position and color
out.positionArray.push(
geometry.attributes.position.array[child2Index*3],
geometry.attributes.position.array[child2Index*3+1],
geometry.attributes.position.array[child2Index*3+2]
);
if (geometry.attributes.color != undefined) {
out.colorArray.push(
geometry.attributes.color.array[child2Index*3],
geometry.attributes.color.array[child2Index*3+1],
geometry.attributes.color.array[child2Index*3+2]
);
}
out.currentIndex += 1;
}
// Add indices:
out.indexArray.push(out.indexMap[rootIndex], out.indexMap[child1Index], out.indexMap[child2Index]);
}
const geoPositions = new Float32Array(out.positionArray);
const geoColors = new Float32Array(out.colorArray);
const geoIndices = new Uint32Array(out.indexArray);
const geo = new THREE.BufferGeometry();
geo.name = "G_" + i;
geo.index = new THREE.BufferAttribute(geoIndices, 1, false);
geo.attributes.position = new THREE.BufferAttribute(geoPositions, 3, false);
geo.attributes.color = new THREE.BufferAttribute(geoColors, 3, true);
geo.computeBoundingSphere();
geo.computeBoundingBox();
geometryArray.push(geo);
}
return geometryArray;
}

Resources