package armory.trait; import iron.math.Vec4; import iron.math.Mat4; import iron.Trait; import iron.object.MeshObject; import iron.data.MeshData; import iron.data.SceneFormat; #if arm_bullet import armory.trait.physics.bullet.RigidBody; import armory.trait.physics.PhysicsWorld; #end class PhysicsBreak extends Trait { #if (!arm_bullet) public function new() { super(); } #else static var physics:PhysicsWorld = null; static var breaker:ConvexBreaker = null; var body:RigidBody; public function new() { super(); if (breaker == null) breaker = new ConvexBreaker(); notifyOnInit(init); } function init() { if (physics == null) physics = armory.trait.physics.PhysicsWorld.active; body = object.getTrait(RigidBody); breaker.initBreakableObject(cast object, body.mass, body.friction, new Vec4(), new Vec4(), true); notifyOnUpdate(update); } function update() { var ar = physics.getContactPairs(body); if (ar != null) { var maxImpulse = 0.0; var impactPoint:Vec4 = null; var impactNormal:Vec4 = null; for (p in ar) { if (maxImpulse < p.impulse) { maxImpulse = p.impulse; impactPoint = p.posB; impactNormal = p.normOnB; } } var fractureImpulse = 4.0; if (maxImpulse > fractureImpulse) { var radialIter = 1; var randIter = 1; var debris = breaker.subdivideByImpact(cast object, impactPoint, impactNormal, radialIter, randIter); var numObjects = debris.length; for (o in debris) { var ud = breaker.userDataMap.get(cast o); var params = [0.04, 0.1, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.04, 0.0, 0.0, 0.0]; o.addTrait(new RigidBody(Shape.ConvexHull, ud.mass, ud.friction, 0, 1, params)); if (cast(o, MeshObject).data.geom.positions.length < 600) { o.addTrait(new PhysicsBreak()); } } object.remove(); } } } #end } // Based on work by yomboprime https://github.com/yomboprime // This class can be used to subdivide a convex geometry object into pieces class ConvexBreaker { var minSizeForBreak:Float; var smallDelta:Float; var tempLine:Line3; var tempPlane:Plane; var tempPlane2:Plane; var tempCM1:Vec4; var tempCM2:Vec4; var tempVec4:Vec4; var tempVec42:Vec4; var tempVec43:Vec4; var tempCutResult:CutResult; var segments:Array; public var userDataMap:Map; // minSizeForBreak Min size a debris can have to break // smallDelta Max distance to consider that a point belongs to a plane public function new(minSizeForBreak = 1.4, smallDelta = 0.0001) { this.minSizeForBreak = minSizeForBreak; this.smallDelta = smallDelta; tempLine = new Line3(); tempPlane = new Plane(); tempPlane2 = new Plane(); tempCM1 = new Vec4(); tempCM2 = new Vec4(); tempVec4 = new Vec4(); tempVec42 = new Vec4(); tempVec43 = new Vec4(); tempCutResult = new CutResult(); segments = new Array(); var n = 30 * 30; for (i in 0...n) segments.push(false); userDataMap = new Map(); } public function initBreakableObject(object:MeshObject, mass:Float, friction:Float, velocity:Vec4, angularVelocity:Vec4, breakable:Bool) { var ar = object.data.geom.positions; var scalePos = object.data.scalePos; // Create vertices mark var sc = object.transform.scale; var vertices = new Array(); for (i in 0...Std.int(ar.length / 4)) { // Use w component as mark vertices.push(new Vec4( ar[i * 4 ] * sc.x * (1 / 32767) * scalePos, ar[i * 4 + 1] * sc.y * (1 / 32767) * scalePos, ar[i * 4 + 2] * sc.z * (1 / 32767) * scalePos, 0 )); } var ind = object.data.geom.indices[0]; var faces = new Array(); for (i in 0...Std.int(ind.length / 3)) { var a = ind[i * 3]; var b = ind[i * 3 + 1]; var c = ind[i * 3 + 2]; // Merge duplis for (f in faces) { if (vertices[a].equals(vertices[f.a])) a = f.a; else if (vertices[a].equals(vertices[f.b])) a = f.b; else if (vertices[a].equals(vertices[f.c])) a = f.c; if (vertices[b].equals(vertices[f.a])) b = f.a; else if (vertices[b].equals(vertices[f.b])) b = f.b; else if (vertices[b].equals(vertices[f.c])) b = f.c; if (vertices[c].equals(vertices[f.a])) c = f.a; else if (vertices[c].equals(vertices[f.b])) c = f.b; else if (vertices[c].equals(vertices[f.c])) c = f.c; } faces.push(new Face3(a, b, c)); } // Reorder vertices var verts = new Array(); var map = new Map(); var i = 0; function orderVert(fi:Int):Int { var val = map.get(fi); if (val == null) { verts.push(vertices[fi]); map.set(fi, i); i++; return i - 1; } else return val; } for (f in faces) { f.a = orderVert(f.a); f.b = orderVert(f.b); f.c = orderVert(f.c); } var userData = new UserData(); userData.mass = mass; userData.friction = friction; userData.velocity = velocity.clone(); userData.angularVelocity = angularVelocity.clone(); userData.breakable = breakable; userData.vertices = verts; userData.faces = faces; userDataMap.set(object, userData); } // maxRadialIterations Iterations for radial cuts // maxRandomIterations Max random iterations for not-radial cuts public function subdivideByImpact(object:MeshObject, pointOfImpact:Vec4, normal:Vec4, maxRadialIterations:Int, maxRandomIterations:Int):Array { var debris:Array = []; tempVec4.addvecs(pointOfImpact, normal); tempPlane.setFromCoplanarPoints(pointOfImpact, object.transform.loc, tempVec4); var maxTotalIterations = maxRandomIterations + maxRadialIterations; var scope = this; function subdivideRadial(subObject:MeshObject, startAngle:Float, endAngle:Float, numIterations:Int) { if (Math.random() < numIterations * 0.05 || numIterations > maxTotalIterations) { debris.push(subObject); return; } var angle = Math.PI; if (numIterations == 0) { tempPlane2.normal.setFrom(tempPlane.normal); tempPlane2.constant = tempPlane.constant; } else { if (numIterations <= maxRadialIterations) { angle = (endAngle - startAngle) * (0.2 + 0.6 * Math.random()) + startAngle; // Rotate tempPlane2 at impact point around normal axis and the angle scope.tempVec42.setFrom(object.transform.loc).sub(pointOfImpact).applyAxisAngle(normal, angle).add(pointOfImpact); tempPlane2.setFromCoplanarPoints(pointOfImpact, scope.tempVec4, scope.tempVec42); } else { angle = ((0.5 * (numIterations & 1)) + 0.2 * (2 - Math.random())) * Math.PI; // Rotate tempPlane2 at object position around normal axis and the angle scope.tempVec42.setFrom(pointOfImpact).sub(subObject.transform.loc).applyAxisAngle(normal, angle).add(subObject.transform.loc); scope.tempVec43.setFrom(normal).add(subObject.transform.loc); tempPlane2.setFromCoplanarPoints(subObject.transform.loc, scope.tempVec43, scope.tempVec42); } } // Perform the cut scope.cutByPlane(subObject, tempPlane2, scope.tempCutResult); var object1 = scope.tempCutResult.object1; var object2 = scope.tempCutResult.object2; if (object1 != null) subdivideRadial(object1, startAngle, angle, numIterations + 1); if (object2 != null) subdivideRadial(object2, angle, endAngle, numIterations + 1); // Object was subdivided into debris iron.Scene.active.meshes.remove(subObject); } subdivideRadial(object, 0, 2 * Math.PI, 0); return debris; } function transformFreeVector(v:Vec4, m:Mat4):Vec4 { // Vector interpreted as a free vector // Mat4 orthogonal matrix (matrix without scale) var x = v.x, y = v.y, z = v.z; v.x = m._00 * x + m._10 * y + m._20 * z; v.y = m._01 * x + m._11 * y + m._21 * z; v.z = m._02 * x + m._12 * y + m._22 * z; return v; } function transformFreeVectorInverse(v:Vec4, m:Mat4):Vec4 { // Vector interpreted as a free vector // Mat4 orthogonal matrix (matrix without scale) var x = v.x, y = v.y, z = v.z; v.x = m._00 * x + m._01 * y + m._02 * z; v.y = m._10 * x + m._11 * y + m._12 * z; v.z = m._20 * x + m._21 * y + m._22 * z; return v; } function transformTiedVectorInverse(v:Vec4, m:Mat4):Vec4 { // Vector interpreted as a tied (ordinary) vector // Mat4 orthogonal matrix (matrix without scale) var x = v.x, y = v.y, z = v.z; v.x = m._00 * x + m._01 * y + m._02 * z - m._30; v.y = m._10 * x + m._11 * y + m._12 * z - m._31; v.z = m._20 * x + m._21 * y + m._22 * z - m._32; return v; }; function transformPlaneToLocalSpace(plane:Plane, m:Mat4, resultPlane:Plane) { resultPlane.normal.setFrom(plane.normal); resultPlane.constant = plane.constant; var v1 = new Vec4(); var referencePoint = transformTiedVectorInverse(plane.coplanarPoint(v1), m); transformFreeVectorInverse(resultPlane.normal, m); // Recalculate constant resultPlane.constant = -referencePoint.dot(resultPlane.normal); } // Returns breakable objects, the resulting 2 pieces of the cut // object2 can be null if the plane doesn't cut the object // object1 can be null only in case of error // Returned value is number of pieces, 0 for error function cutByPlane(object:MeshObject, plane:Plane, output:CutResult):Int { var userData = userDataMap.get(object); var points:Array = userData.vertices; var faces:Array = userData.faces; var numPoints = points.length; var points1 = []; var points2 = []; var delta = smallDelta; // Reset vertices mark for (i in 0...numPoints) points[i].w = 0; // Reset segments mark var numPointPairs = numPoints * numPoints; for (i in 0...numPointPairs) this.segments[i] = false; // Iterate through the faces to mark edges shared by coplanar faces for (i in 0...faces.length - 1) { var face1 = faces[i]; for (j in (i + 1)...faces.length) { var face2 = faces[j]; var coplanar = 1 - face1.normal.dot(face2.normal) < delta; if (coplanar) { var a1 = face1.a; var b1 = face1.b; var c1 = face1.c; var a2 = face2.a; var b2 = face2.b; var c2 = face2.c; if (a1 == a2 || a1 == b2 || a1 == c2) { if (b1 == a2 || b1 == b2 || b1 == c2) { this.segments[a1 * numPoints + b1] = true; this.segments[b1 * numPoints + a1] = true; } else { this.segments[c1 * numPoints + a1] = true; this.segments[a1 * numPoints + c1] = true; } } else if (b1 == a2 || b1 == b2 || b1 == c2) { this.segments[c1 * numPoints + b1] = true; this.segments[b1 * numPoints + c1] = true; } } } } // Transform the plane to object local space var localPlane = this.tempPlane; object.transform.buildMatrix(); transformPlaneToLocalSpace(plane, object.transform.world, localPlane); // Iterate through the faces adding points to both pieces for (i in 0...faces.length) { var face = faces[i]; for (segment in 0...3) { var i0 = segment == 0 ? face.a : (segment == 1 ? face.b : face.c); var i1 = segment == 0 ? face.b : (segment == 1 ? face.c : face.a); var segmentState = this.segments[i0 * numPoints + i1]; // The segment already has been processed in another face if (segmentState) continue; // Mark segment as processed (also inverted segment) this.segments[i0 * numPoints + i1] = true; this.segments[i1 * numPoints + i0] = true; var p0 = points[i0]; var p1 = points[i1]; if (p0.w == 0) { var d = localPlane.distanceToPoint(p0); // mark: 1 for negative side, 2 for positive side, 3 for coplanar point if (d > delta) { p0.w = 2; points2.push(p0); } else if (d < -delta) { p0.w = 1; points1.push(p0); } else { p0.w = 3; points1.push(p0); var p02 = p0.clone(); p02.w = 3; points2.push(p02); } } if (p1.w == 0) { var d = localPlane.distanceToPoint(p1); // mark: 1 for negative side, 2 for positive side, 3 for coplanar point if (d > delta) { p1.w = 2; points2.push(p1); } else if (d < -delta) { p1.w = 1; points1.push(p1); } else { p1.w = 3; points1.push(p1); var p1_2 = p1.clone(); p1_2.w = 3; points2.push(p1_2); } } var mark0 = p0.w; var mark1 = p1.w; if ((mark0 == 1 && mark1 == 2 ) || ( mark0 == 2 && mark1 == 1)) { // Intersection of segment with the plane tempLine.start.setFrom(p0); tempLine.end.setFrom(p1); var intersection = localPlane.intersectLine(tempLine); if (intersection == null) return 0; intersection.w = 1; points1.push(intersection); var intersection_2 = intersection.clone(); intersection_2.w = 2; points2.push(intersection_2); } } } // Calculate debris mass (very fast and imprecise): var newMass = userData.mass * 0.5; // Calculate debris Center of Mass (again fast and imprecise) tempCM1.set(0, 0, 0); var radius1 = 0.0; var numPoints1 = points1.length; if (numPoints1 > 0) { for (i in 0...numPoints1) { tempCM1.add(points1[i]); } tempCM1.mult(1.0 / numPoints1); for (i in 0...numPoints1) { var p = points1[i]; p.sub(tempCM1); radius1 = Math.max(Math.max(radius1, p.x), Math.max(p.y, p.z)); } tempCM1.add(object.transform.loc); } tempCM2.set(0, 0, 0); var radius2 = 0.0; var numPoints2 = points2.length; if (numPoints2 > 0) { for (i in 0...numPoints2) { tempCM2.add(points2[i]); } tempCM2.mult(1.0 / numPoints2); for (i in 0...numPoints2) { var p = points2[i]; p.sub(tempCM2); radius2 = Math.max(Math.max(radius2, p.x), Math.max(p.y, p.z)); } tempCM2.add(object.transform.loc); } var object1 = null; var object2 = null; var numObjects = 0; if (numPoints1 > 4) { var data1 = makeMeshData(points1); object1 = new MeshObject(data1, object.materials); object1.transform.loc.setFrom(tempCM1); object1.transform.rot.setFrom(object.transform.rot); object1.transform.buildMatrix(); initBreakableObject(object1, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius1 > minSizeForBreak); numObjects++; } if (numPoints2 > 4) { var data2 = makeMeshData(points2); object2 = new MeshObject(data2, object.materials); object2.transform.loc.setFrom(tempCM2); object2.transform.rot.setFrom(object.transform.rot); object2.transform.buildMatrix(); initBreakableObject(object2, newMass, userData.friction, userData.velocity, userData.angularVelocity, 2 * radius2 > minSizeForBreak); numObjects++; } output.object1 = object1; output.object2 = object2; return numObjects; } static var meshIndex = 0; function makeMeshData(points:Array) { while (points.length > 50) points.pop(); var cm = new ConvexHull(points); var maxdim = 1.0; var pa = new Array(); var na = new Array(); for (p in cm.vertices) { pa.push(p.x); pa.push(p.y); pa.push(p.z); na.push(0.0); na.push(0.0); na.push(0.0); var ax = Math.abs(p.x); var ay = Math.abs(p.y); var az = Math.abs(p.z); if (ax > maxdim) maxdim = ax; if (ay > maxdim) maxdim = ay; if (az > maxdim) maxdim = az; } maxdim *= 2; var ind = new Array(); function addFlatNormal(normal:Vec4, fi:Int) { if (na[fi * 3] != 0.0 || na[fi * 3 + 1] != 0.0 || na[fi * 3 + 2] != 0.0) { pa.push(pa[fi * 3 ]); pa.push(pa[fi * 3 + 1]); pa.push(pa[fi * 3 + 2]); na.push(normal.x); na.push(normal.y); na.push(normal.z); ind.push(Std.int(pa.length / 3 - 1)); } else { na[fi * 3 ] = normal.x; na[fi * 3 + 1] = normal.y; na[fi * 3 + 2] = normal.z; ind.push(fi); } } for (f in cm.face3s) { // Duplicate vertex for flat normals addFlatNormal(f.normal, f.a); addFlatNormal(f.normal, f.b); addFlatNormal(f.normal, f.c); } // TODO: var n = Std.int(pa.length / 3); var paa = new kha.arrays.Int16Array(n * 4); var naa = new kha.arrays.Int16Array(n * 2); var invdim = 1 / maxdim; for (i in 0...n) { paa.set(i * 4 , Std.int(pa[i * 3 ] * 32767 * invdim)); paa.set(i * 4 + 1, Std.int(pa[i * 3 + 1] * 32767 * invdim)); paa.set(i * 4 + 2, Std.int(pa[i * 3 + 2] * 32767 * invdim)); naa.set(i * 2 , Std.int(na[i * 3 ] * 32767 * invdim)); naa.set(i * 2 + 1, Std.int(na[i * 3 + 1] * 32767 * invdim)); paa.set(i * 4 + 3, Std.int(na[i * 3 + 2] * 32767 * invdim)); } var inda = new kha.arrays.Uint32Array(ind.length); for (i in 0...ind.length) inda.set(i, ind[i]); var pos:TVertexArray = { attrib: "pos", values: paa }; var nor:TVertexArray = { attrib: "nor", values: naa }; var indices:TIndexArray = { material: 0, values: inda }; var rawmesh:TMeshData = { name: "TempMesh" + (meshIndex++), vertex_arrays: [pos, nor], index_arrays: [indices], scale_pos: maxdim }; // Synchronous on Krom var md = new MeshData(rawmesh, function(d:MeshData) {}); md.geom.calculateAABB(); return md; } } class UserData { public var mass:Float; public var friction:Float; public var velocity:Vec4; public var angularVelocity:Vec4; public var breakable:Bool; public var vertices:Array; public var faces:Array; public function new() { } } class CutResult { public var object1:MeshObject = null; public var object2:MeshObject = null; public function new() { } } class Line3 { public var start:Vec4; public var end:Vec4; public function new() { start = new Vec4(); end = new Vec4(); } public function delta(result:Vec4) { result.subvecs(end, start); return result; } } class Plane { public var normal = new Vec4(1.0, 0.0, 0.0); public var constant = 0.0; public function new() { } public function distanceToPoint(point:Vec4):Float { return normal.dot(point) + constant; } public function setFromCoplanarPoints(a:Vec4, b:Vec4, c:Vec4) { var v1 = new Vec4(); var v2 = new Vec4(); var normal = v1.subvecs(c, b).cross(v2.subvecs(a, b)).normalize(); set(normal, a); return this; } public function set(normal:Vec4, point:Vec4):Plane { this.normal.setFrom(normal); constant = -point.dot(this.normal); return this; } public function coplanarPoint(result:Vec4) { return result.setFrom(normal).mult(-constant); } public function intersectLine(line:Line3):Vec4 { var v1 = new Vec4(); var result = new Vec4(); var direction = line.delta(v1); var denominator = normal.dot(direction); if (denominator == 0) { // line is coplanar, return origin if (distanceToPoint(line.start) == 0) { return result.setFrom(line.start); } // Unsure if this is the correct method to handle this case. return null; } var t = -(line.start.dot(this.normal) + constant) / denominator; if (t < 0 || t > 1) return null; return result.setFrom(direction).mult(t).add(line.start); } } // Based on work by qiao https://github.com/qiao // This is a convex hull generator using the incremental method // The complexity is O(n^2) where n is the number of vertices class ConvexHull { var faces = [[0, 1, 2], [0, 2, 1]]; public var face3s = new Array(); public var vertices = new Array(); public function new(vertices:Array) { for (i in 3...vertices.length) addPoint(i, vertices); // Push vertices into array, skipping those inside the hull // Map from old vertex id to new id var id = 0; var newId = new Array(); for (i in 0...vertices.length) newId.push(-1); for (i in 0...faces.length) { var face = faces[i]; for (j in 0...3) { if (newId[face[j]] == -1) { newId[face[j]] = id++; this.vertices.push(vertices[face[j]]); } face[j] = newId[face[j]]; } } for (i in 0...faces.length) { face3s.push(new Face3(faces[i][0], faces[i][1], faces[i][2])); } computeFaceNormals(); } var cb = new Vec4(); var ab = new Vec4(); function computeFaceNormals() { for (f in 0...face3s.length) { var face = face3s[f]; var va = vertices[face.a]; var vb = vertices[face.b]; var vc = vertices[face.c]; cb.subvecs(vc, vb); ab.subvecs(va, vb); cb.cross(ab); cb.normalize(); face.normal.setFrom(cb); } } function addPoint(vertexId:Int, vertices:Array) { var vertex = vertices[vertexId].clone(); var mag = vertex.length(); vertex.x += mag * randomOffset(); vertex.y += mag * randomOffset(); vertex.z += mag * randomOffset(); var hole:Array> = []; var f = 0; while (f < faces.length) { var face = faces[f]; // For each face, if the vertex can see it, // then we try to add the face's edges into the hole if (visible(face, vertex, vertices)) { for (e in 0...3) { var edge = [face[e], face[(e + 1) % 3]]; var boundary = true; // Remove duplicated edges for (h in 0...hole.length) { if (equalEdge(hole[h], edge)) { hole[h] = hole[hole.length - 1]; hole.pop(); boundary = false; break; } } if (boundary) hole.push(edge); } faces[f] = faces[faces.length - 1]; faces.pop(); } else { f++; } } // Construct the new faces formed by the edges of the hole and the vertex for (h in 0...hole.length) { faces.push([hole[h][0], hole[h][1], vertexId]); } } // Whether the face is visible from the vertex function visible(face:Array, vertex:Vec4, vertices:Array) { var va = vertices[face[0]]; var vb = vertices[face[1]]; var vc = vertices[face[2]]; var n = normal(va, vb, vc); var dist = n.dot(va); // Distance from face to origin return n.dot(vertex) >= dist; } function normal(va:Vec4, vb:Vec4, vc:Vec4) { var cb = new Vec4(); var ab = new Vec4(); cb.subvecs(vc, vb); ab.subvecs(va, vb); cb.cross(ab); cb.normalize(); return cb; } function equalEdge(ea:Array, eb:Array) { return ea[0] == eb[1] && ea[1] == eb[0]; } function randomOffset() { return (Math.random() - 0.5) * 2 * 1e-6; } } class Face3 { public var a:Int; public var b:Int; public var c:Int; public var normal:Vec4; public function new(a:Int, b:Int, c:Int) { this.a = a; this.b = b; this.c = c; normal = new Vec4(); } }