From 13e8bc70ff1f59fc4bcd5b49fcd9f10bd673b366 Mon Sep 17 00:00:00 2001 From: RandomityGuy <31925790+RandomityGuy@users.noreply.github.com> Date: Fri, 28 May 2021 16:58:28 +0530 Subject: [PATCH] add files --- .gitignore | 5 + compile.hxml | 8 + index.html | 22 ++ src/CameraController.hx | 43 ++ src/DifBuilder.hx | 252 ++++++++++++ src/InteriorGeometry.hx | 20 + src/Main.hx | 89 +++++ src/Marble.hx | 406 +++++++++++++++++++ src/collision/Collision.hx | 571 +++++++++++++++++++++++++++ src/collision/CollisionEntity.hx | 222 +++++++++++ src/collision/CollisionInfo.hx | 15 + src/collision/CollisionPacket.hx | 67 ++++ src/collision/CollisionSurface.hx | 63 +++ src/collision/CollisionWorld.hx | 30 ++ src/dif/AISpecialNode.hx | 27 ++ src/dif/AnimatedLight.hx | 34 ++ src/dif/BSPNode.hx | 107 +++++ src/dif/BSPSolidLeaf.hx | 24 ++ src/dif/ConvexHull.hx | 86 ++++ src/dif/CoordBin.hx | 28 ++ src/dif/Dif.hx | 149 +++++++ src/dif/Edge.hx | 31 ++ src/dif/Edge2.hx | 48 +++ src/dif/FFSurface.hx | 39 ++ src/dif/ForceField.hx | 63 +++ src/dif/GameEntity.hx | 42 ++ src/dif/Interior.hx | 328 +++++++++++++++ src/dif/InteriorPathFollower.hx | 57 +++ src/dif/LightMap.hx | 41 ++ src/dif/LightState.hx | 37 ++ src/dif/NullSurface.hx | 43 ++ src/dif/Plane.hx | 24 ++ src/dif/Polyhedron.hx | 36 ++ src/dif/PolyhedronEdge.hx | 30 ++ src/dif/Portal.hx | 33 ++ src/dif/ReaderExtensions.hx | 91 +++++ src/dif/StateData.hx | 28 ++ src/dif/Surface.hx | 149 +++++++ src/dif/TexGenEQ.hx | 28 ++ src/dif/TexMatrix.hx | 32 ++ src/dif/Trigger.hx | 44 +++ src/dif/VehicleCollision.hx | 67 ++++ src/dif/Version.hx | 14 + src/dif/WayPoint.hx | 32 ++ src/dif/WindingIndex.hx | 24 ++ src/dif/WriterExtensions.hx | 49 +++ src/dif/Zone.hx | 47 +++ src/dif/io/BytesReader.hx | 60 +++ src/dif/io/BytesWriter.hx | 50 +++ src/dif/math/Box3F.hx | 104 +++++ src/dif/math/PlaneF.hx | 84 ++++ src/dif/math/Point2F.hx | 15 + src/dif/math/Point3F.hx | 94 +++++ src/dif/math/Point4F.hx | 35 ++ src/dif/math/QuatF.hx | 35 ++ src/dif/math/SphereF.hx | 35 ++ src/octree/IOctreeElement.hx | 8 + src/octree/IOctreeObject.hx | 9 + src/octree/Octree.hx | 205 ++++++++++ src/octree/OctreeIntersection.hx | 11 + src/octree/OctreeNode.hx | 221 +++++++++++ src/octree/PriorityQueue.hx | 57 +++ src/octree/PriorityQueueNode.hx | 13 + src/octreenarrowphase/IOctreeNode.hx | 7 + src/octreenarrowphase/Octree.hx | 84 ++++ src/octreenarrowphase/OctreeNode.hx | 168 ++++++++ src/octreenarrowphase/OctreePoint.hx | 21 + 67 files changed, 5041 insertions(+) create mode 100644 .gitignore create mode 100644 compile.hxml create mode 100644 index.html create mode 100644 src/CameraController.hx create mode 100644 src/DifBuilder.hx create mode 100644 src/InteriorGeometry.hx create mode 100644 src/Main.hx create mode 100644 src/Marble.hx create mode 100644 src/collision/Collision.hx create mode 100644 src/collision/CollisionEntity.hx create mode 100644 src/collision/CollisionInfo.hx create mode 100644 src/collision/CollisionPacket.hx create mode 100644 src/collision/CollisionSurface.hx create mode 100644 src/collision/CollisionWorld.hx create mode 100644 src/dif/AISpecialNode.hx create mode 100644 src/dif/AnimatedLight.hx create mode 100644 src/dif/BSPNode.hx create mode 100644 src/dif/BSPSolidLeaf.hx create mode 100644 src/dif/ConvexHull.hx create mode 100644 src/dif/CoordBin.hx create mode 100644 src/dif/Dif.hx create mode 100644 src/dif/Edge.hx create mode 100644 src/dif/Edge2.hx create mode 100644 src/dif/FFSurface.hx create mode 100644 src/dif/ForceField.hx create mode 100644 src/dif/GameEntity.hx create mode 100644 src/dif/Interior.hx create mode 100644 src/dif/InteriorPathFollower.hx create mode 100644 src/dif/LightMap.hx create mode 100644 src/dif/LightState.hx create mode 100644 src/dif/NullSurface.hx create mode 100644 src/dif/Plane.hx create mode 100644 src/dif/Polyhedron.hx create mode 100644 src/dif/PolyhedronEdge.hx create mode 100644 src/dif/Portal.hx create mode 100644 src/dif/ReaderExtensions.hx create mode 100644 src/dif/StateData.hx create mode 100644 src/dif/Surface.hx create mode 100644 src/dif/TexGenEQ.hx create mode 100644 src/dif/TexMatrix.hx create mode 100644 src/dif/Trigger.hx create mode 100644 src/dif/VehicleCollision.hx create mode 100644 src/dif/Version.hx create mode 100644 src/dif/WayPoint.hx create mode 100644 src/dif/WindingIndex.hx create mode 100644 src/dif/WriterExtensions.hx create mode 100644 src/dif/Zone.hx create mode 100644 src/dif/io/BytesReader.hx create mode 100644 src/dif/io/BytesWriter.hx create mode 100644 src/dif/math/Box3F.hx create mode 100644 src/dif/math/PlaneF.hx create mode 100644 src/dif/math/Point2F.hx create mode 100644 src/dif/math/Point3F.hx create mode 100644 src/dif/math/Point4F.hx create mode 100644 src/dif/math/QuatF.hx create mode 100644 src/dif/math/SphereF.hx create mode 100644 src/octree/IOctreeElement.hx create mode 100644 src/octree/IOctreeObject.hx create mode 100644 src/octree/Octree.hx create mode 100644 src/octree/OctreeIntersection.hx create mode 100644 src/octree/OctreeNode.hx create mode 100644 src/octree/PriorityQueue.hx create mode 100644 src/octree/PriorityQueueNode.hx create mode 100644 src/octreenarrowphase/IOctreeNode.hx create mode 100644 src/octreenarrowphase/Octree.hx create mode 100644 src/octreenarrowphase/OctreeNode.hx create mode 100644 src/octreenarrowphase/OctreePoint.hx diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2b062af4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +interiors +*.hl +*.js +*.js.map +.vscode \ No newline at end of file diff --git a/compile.hxml b/compile.hxml new file mode 100644 index 00000000..209b8e69 --- /dev/null +++ b/compile.hxml @@ -0,0 +1,8 @@ +-cp src +-lib headbutt +-lib heaps +-lib hlsdl +-lib polygonal-ds +-hl marblegame.hl +--main Main +-debug \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..09ad9e75 --- /dev/null +++ b/index.html @@ -0,0 +1,22 @@ + + + + + Hello Heaps + + + + + + + diff --git a/src/CameraController.hx b/src/CameraController.hx new file mode 100644 index 00000000..acc19d4e --- /dev/null +++ b/src/CameraController.hx @@ -0,0 +1,43 @@ +import h3d.Vector; +import hxsl.Types.Matrix; +import h3d.scene.Scene; + +enum CameraMode { + FreeOrbit; + FixedOrbit; +} + +class CameraController { + var scene:Scene; + + var view:Matrix; + var projection:Matrix; + + public var Position:Vector; + public var Direction:Vector; + public var Up:Vector; + public var LookAtPoint:Vector; + public var Mode = CameraMode.FixedOrbit; + public var CameraSensitivity = 0.6; + public var CameraPanSensitivity = 0.05; + public var CameraZoomSensitivity = 0.7; + public var CameraZoomSpeed = 15.0; + public var CameraZoomDeceleration = 250.0; + public var CameraSpeed = 15.0; + + var camZoomSpeed:Float; + + public var CameraDistance = 5; + public var CameraMinDistance = 1; + public var CameraMaxDistance = 25; + public var CameraPitch:Float; + public var CameraYaw:Float; + + public function new(scene:Scene) { + this.scene = scene; + } + + function createLookAt() { + this.view = Matrix.lookAtX(LookAtPoint.sub(Position), Up); + } +} diff --git a/src/DifBuilder.hx b/src/DifBuilder.hx new file mode 100644 index 00000000..27384165 --- /dev/null +++ b/src/DifBuilder.hx @@ -0,0 +1,252 @@ +package src; + +import h3d.Vector; +import collision.CollisionSurface; +import collision.CollisionEntity; +import h3d.mat.Data.Wrap; +import hxd.fs.FileSystem; +import hxd.res.Loader; +import hxd.res.Image; +import h3d.mat.Texture; +import haxe.io.Path; +import hxd.File; +import h3d.scene.Mesh; +import h3d.prim.UV; +import h3d.col.Point; +import h3d.prim.Polygon; +import dif.math.Point2F; +import dif.math.Point3F; +import h3d.prim.BigPrimitive; +import dif.Interior; +import dif.Dif; +import src.InteriorGeometry; + +class DifBuilderTriangle { + public var texture:String; + public var normal1:Point3F; + public var normal2:Point3F; + public var normal3:Point3F; + public var p1:Point3F; + public var p2:Point3F; + public var p3:Point3F; + public var uv1:Point2F; + public var uv2:Point2F; + public var uv3:Point2F; + + public function new() {} +} + +class DifBuilder { + public static function loadDif(path:String, loader:Loader) { + var dif = Dif.Load(path); + + var geo = dif.interiors[0]; + + var hulls = geo.convexHulls; + + var triangles = []; + var textures = []; + + var collider = new CollisionEntity(); + + for (i in 0...hulls.length) { + var hullTris = []; + var hull = hulls[i]; + + for (j in hull.surfaceStart...(hull.surfaceStart + hull.surfaceCount)) { + var surfaceindex = geo.hullSurfaceIndices[j]; + var surface = geo.surfaces[surfaceindex]; + var planeindex = surface.planeIndex; + + var planeFlipped = (planeindex & 0x8000) == 0x8000; + if (planeFlipped) + planeindex &= ~0x8000; + + var plane = geo.planes[planeindex]; + var normal = geo.normals[plane.normalIndex]; + + if (planeFlipped) + normal = normal.scalar(-1); + + var texture = geo.materialList[surface.textureIndex]; + if (!textures.contains(texture)) + textures.push(texture); + + var points = geo.points; + + var colliderSurface = new CollisionSurface(); + colliderSurface.points = []; + colliderSurface.normals = []; + colliderSurface.indices = []; + + for (k in (surface.windingStart + 2)...(surface.windingStart + surface.windingCount)) { + var p1, p2, p3; + if ((k - (surface.windingStart + 2)) % 2 == 0) { + p1 = points[geo.windings[k]]; + p2 = points[geo.windings[k - 1]]; + p3 = points[geo.windings[k - 2]]; + } else { + p1 = points[geo.windings[k - 2]]; + p2 = points[geo.windings[k - 1]]; + p3 = points[geo.windings[k]]; + } + + var texgen = geo.texGenEQs[surface.texGenIndex]; + + var uv1 = new Point2F(p1.x * texgen.planeX.x + + p1.y * texgen.planeX.y + + p1.z * texgen.planeX.z + + texgen.planeX.d, + p1.x * texgen.planeY.x + + p1.y * texgen.planeY.y + + p1.z * texgen.planeY.z + + texgen.planeY.d); + var uv2 = new Point2F(p2.x * texgen.planeX.x + + p2.y * texgen.planeX.y + + p2.z * texgen.planeX.z + + texgen.planeX.d, + p2.x * texgen.planeY.x + + p2.y * texgen.planeY.y + + p2.z * texgen.planeY.z + + texgen.planeY.d); + var uv3 = new Point2F(p3.x * texgen.planeX.x + + p3.y * texgen.planeX.y + + p3.z * texgen.planeX.z + + texgen.planeX.d, + p3.x * texgen.planeY.x + + p3.y * texgen.planeY.y + + p3.z * texgen.planeY.z + + texgen.planeY.d); + + var tri = new DifBuilderTriangle(); + tri.texture = texture; + tri.normal1 = normal; + tri.normal2 = normal; + tri.normal3 = normal; + tri.p1 = p1; + tri.p2 = p2; + tri.p3 = p3; + tri.uv1 = uv1; + tri.uv2 = uv2; + tri.uv3 = uv3; + triangles.push(tri); + hullTris.push(tri); + + colliderSurface.points.push(new Vector(-p1.x, p1.y, p1.z)); + colliderSurface.points.push(new Vector(-p2.x, p2.y, p2.z)); + colliderSurface.points.push(new Vector(-p3.x, p3.y, p3.z)); + colliderSurface.normals.push(new Vector(-normal.x, normal.y, normal.z)); + colliderSurface.normals.push(new Vector(-normal.x, normal.y, normal.z)); + colliderSurface.normals.push(new Vector(-normal.x, normal.y, normal.z)); + colliderSurface.indices.push(colliderSurface.indices.length); + colliderSurface.indices.push(colliderSurface.indices.length); + colliderSurface.indices.push(colliderSurface.indices.length); + } + + colliderSurface.generateBoundingBox(); + collider.addSurface(colliderSurface); + } + } + + var mats = new Map>(); + + for (index => value in triangles) { + if (mats.exists(value.texture)) { + mats[value.texture].push(value); + } else { + mats.set(value.texture, [value]); + } + } + + collider.generateBoundingBox(); + var ig = new InteriorGeometry(); + ig.collider = collider; + + function canFindTex(tex:String) { + if (tex.indexOf('/') != -1) { + tex = tex.split('/')[1]; + } + + if (File.exists(Path.directory(path) + "/" + tex + ".jpg")) { + return true; + } + if (File.exists(Path.directory(path) + "/" + tex + ".png")) { + return true; + } + var prevDir = Path.directory(Path.directory(path)); + + if (File.exists(prevDir + "/" + tex + ".jpg")) { + return true; + } + if (File.exists(prevDir + "/" + tex + ".png")) { + return true; + } + + return false; + } + + function tex(tex:String):String { + if (tex.indexOf('/') != -1) { + tex = tex.split('/')[1]; + } + + if (File.exists(Path.directory(path) + "/" + tex + ".jpg")) { + return Path.directory(path) + "/" + tex + ".jpg"; + } + if (File.exists(Path.directory(path) + "/" + tex + ".png")) { + return Path.directory(path) + "/" + tex + ".png"; + } + + var prevDir = Path.directory(Path.directory(path)); + + if (File.exists(prevDir + "/" + tex + ".jpg")) { + return prevDir + "/" + tex + ".jpg"; + } + if (File.exists(prevDir + "/" + tex + ".png")) { + return prevDir + "/" + tex + ".png"; + } + + return null; + } + + for (grp => tris in mats) { + var points = []; + var normals = []; + var uvs = []; + + for (tri in tris) { + var p1 = new Point(-tri.p1.x, tri.p1.y, tri.p1.z); + var p2 = new Point(-tri.p2.x, tri.p2.y, tri.p2.z); + var p3 = new Point(-tri.p3.x, tri.p3.y, tri.p3.z); + var n1 = new Point(-tri.normal1.x, tri.normal1.y, tri.normal1.z); + var n2 = new Point(-tri.normal2.x, tri.normal2.y, tri.normal2.z); + var n3 = new Point(-tri.normal3.x, tri.normal3.y, tri.normal3.z); + var uv1 = new UV(tri.uv1.x, tri.uv1.y); + var uv2 = new UV(tri.uv2.x, tri.uv2.y); + var uv3 = new UV(tri.uv3.x, tri.uv3.y); + points.push(p3); + points.push(p2); + points.push(p1); + normals.push(n3); + normals.push(n2); + normals.push(n1); + uvs.push(uv3); + uvs.push(uv2); + uvs.push(uv1); + } + + var prim = new Polygon(points); + prim.uvs = uvs; + prim.normals = normals; + + var texture:Texture = loader.load(tex(grp)).toImage().toTexture(); + texture.wrap = Wrap.Repeat; + var material = h3d.mat.Material.create(texture); + // material.mainPass.wireframe = true; + + var mesh = new Mesh(prim, material, ig); + } + + return ig; + } +} diff --git a/src/InteriorGeometry.hx b/src/InteriorGeometry.hx new file mode 100644 index 00000000..583fbabc --- /dev/null +++ b/src/InteriorGeometry.hx @@ -0,0 +1,20 @@ +package src; + +import collision.CollisionEntity; +import headbutt.threed.Headbutt; +import headbutt.threed.shapes.Sphere; +import glm.Vec3; +import dif.math.Point3F; +import h3d.scene.Mesh; +import h3d.col.Bounds; +import h3d.scene.RenderContext; +import h3d.prim.Polygon; +import h3d.scene.Object; + +class InteriorGeometry extends Object { + public var collider:CollisionEntity; + + public function new() { + super(); + } +} diff --git a/src/Main.hx b/src/Main.hx new file mode 100644 index 00000000..7bd5a43d --- /dev/null +++ b/src/Main.hx @@ -0,0 +1,89 @@ +package; + +import collision.CollisionWorld; +import src.Marble; +import hxd.res.Loader; +import hxd.fs.LocalFileSystem; +import hxd.fs.FileSystem; +import src.DifBuilder; +import h3d.Vector; +import h3d.scene.fwd.DirLight; +import h3d.mat.Material; +import h3d.prim.Cube; +import h3d.scene.*; + +class Main extends hxd.App { + var scene:Scene; + var fileSystem:FileSystem; + + var marble:Marble; + var collisionWorld:CollisionWorld; + + override function init() { + super.init(); + + this.fileSystem = new LocalFileSystem(".", null); + + var loader = new Loader(fileSystem); + + var cube = new Cube(); + cube.addUVs(); + cube.addNormals(); + + this.collisionWorld = new CollisionWorld(); + + var db = DifBuilder.loadDif("interiors/beginner/beginner_finish.dif", loader); + collisionWorld.addEntity(db.collider); + + var mat = Material.create(); + var difbounds = new CustomObject(cube, mat, s3d); + var bound = db.collider.boundingBox; + var oct = collisionWorld.octree; + difbounds.setPosition(oct.root.min.x, oct.root.min.y, oct.root.min.z); + + var difbounds2 = new CustomObject(cube, mat, s3d); + difbounds2.setPosition(oct.root.min.x + oct.root.size, oct.root.min.y + oct.root.size, oct.root.min.z + oct.root.size); + + var difbounds3 = new CustomObject(cube, mat, s3d); + difbounds3.setPosition(bound.xMin, bound.yMin, bound.zMin); + var difbounds4 = new CustomObject(cube, mat, s3d); + difbounds4.setPosition(bound.xMax, bound.yMax, bound.zMax); + + // for (surf in db.collider.surfaces) { + // var surfmin = new CustomObject(cube, mat, s3d); + // var bound = surf.boundingBox; + // surfmin.setPosition(bound.xMin, bound.yMin, bound.zMin); + + // var surfmax = new CustomObject(cube, mat, s3d); + // surfmax.setPosition(bound.xMax, bound.yMax, bound.zMax); + // } + + s3d.addChild(db); + + // var mat = Material.create(); + // var so = new CustomObject(cube, mat); + // so.setPosition(0, 0, 0); + // s3d.addChild(so); + + var dirlight = new DirLight(new Vector(0.5, 0.5, -0.5), s3d); + dirlight.enableSpecular = true; + s3d.lightSystem.ambientLight.set(0.3, 0.3, 0.3); + + // s3d.camera. + + marble = new Marble(); + s3d.addChild(marble); + marble.setPosition(0, 0, 5); + // marble.setPosition(-10, -5, 5); + s3d.addChild(marble.camera); + } + + override function update(dt:Float) { + super.update(dt); + marble.update(dt, this.collisionWorld); + } + + static function main() { + new Main(); + } +} diff --git a/src/Marble.hx b/src/Marble.hx new file mode 100644 index 00000000..eadd78ac --- /dev/null +++ b/src/Marble.hx @@ -0,0 +1,406 @@ +package src; + +import hxd.Key; +import collision.CollisionInfo; +import h3d.Matrix; +import collision.CollisionWorld; +import h3d.col.ObjectCollider; +import h3d.col.Collider.GroupCollider; +import h3d.Vector; +import h3d.scene.CameraController; +import h3d.mat.Material; +import h3d.scene.CustomObject; +import h3d.prim.Sphere; +import h3d.scene.Object; + +class Move { + public var d:Vector; + public var jump:Bool; + public var powerup:Bool; + + public function new() {} +} + +class Marble extends Object { + public var camera:CameraController; + + var velocity:Vector; + var omega:Vector; + + var _radius = 0.2; + var _maxRollVelocity = 15; + var _angularAcceleration = 75; + var _jumpImpulse = 7.5; + var _kineticFriction = 0.7; + var _staticFriction = 1.1; + var _brakingAcceleration = 30; + var _gravity = 20; + var _airAccel = 5; + var _maxDotSlide = 0.5; + var _minBounceVel = 0.1; + var _bounceKineticFriction = 0.2; + var _bounceRestitution = 0.5; + var _bounceYet:Bool; + var _bounceSpeed:Float; + var _bouncePos:Vector; + var _bounceNormal:Vector; + var _slipAmount:Float; + var _contactTime:Float; + var _totalTime:Float; + + public function new() { + super(); + var geom = Sphere.defaultUnitSphere(); + geom.addUVs(); + var obj = new CustomObject(geom, Material.create(), this); + obj.scale(_radius); + + this.velocity = new Vector(); + this.omega = new Vector(); + this.camera = new CameraController(20); + } + + function findContacts(collisiomWorld:CollisionWorld) { + var c = collisiomWorld.sphereIntersection(this.getAbsPos().getPosition(), this.velocity, _radius); + return c; + } + + function getMarbleAxis() { + var gravitydir = new Vector(0, 0, -1); + var cammat = Matrix.I(); + var xrot = new Matrix(); + xrot.initRotationX(this.camera.phi); + var zrot = new Matrix(); + zrot.initRotationZ(this.camera.theta); + cammat.multiply(xrot, zrot); + var updir = new Vector(0, 0, 1); + var motiondir = new Vector(cammat._21, cammat._22, cammat._23); + var sidedir = motiondir.clone().cross(updir); + + sidedir.normalize(); + motiondir = updir.clone().cross(sidedir); + return [sidedir, motiondir, updir]; + } + + function getExternalForces(m:Move, dt:Float, contacts:Array) { + var gWorkGravityDir = new Vector(0, 0, -1); + var A = gWorkGravityDir.clone().multiply(this._gravity); + if (contacts.length == 0) { + var axes = this.getMarbleAxis(); + var sideDir = axes[0]; + var motionDir = axes[1]; + var upDir = axes[2]; + A = A.add(sideDir.clone() + .multiply(m.d.x) + .add(motionDir.clone().multiply(m.d.y)) + .multiply(this._airAccel)); + } + return A; + } + + function computeMoveForces(m:Move) { + var aControl = new Vector(); + var desiredOmega = new Vector(); + var currentGravityDir = new Vector(0, 0, -1); + var R = currentGravityDir.clone().multiply(-this._radius); + var rollVelocity = this.omega.clone().cross(R); + var axes = this.getMarbleAxis(); + var sideDir = axes[0]; + var motionDir = axes[1]; + var upDir = axes[2]; + var currentYVelocity = rollVelocity.dot(motionDir); + var currentXVelocity = rollVelocity.dot(sideDir); + var mv = m.d; + + mv = mv.multiply(1.53846157); + var mvlen = mv.length(); + if (mvlen > 1) { + mv = mv.multiply(1 / mvlen); + } + var desiredYVelocity = this._maxRollVelocity * mv.y; + var desiredXVelocity = this._maxRollVelocity * mv.x; + if (desiredYVelocity != 0 || desiredXVelocity != 0) { + if (currentYVelocity > desiredYVelocity && desiredYVelocity > 0) { + desiredYVelocity = currentYVelocity; + } else if (currentYVelocity < desiredYVelocity && desiredYVelocity < 0) { + desiredYVelocity = currentYVelocity; + } + if (currentXVelocity > desiredXVelocity && desiredXVelocity > 0) { + desiredXVelocity = currentXVelocity; + } else if (currentXVelocity < desiredXVelocity && desiredXVelocity < 0) { + desiredXVelocity = currentXVelocity; + } + var rsq = R.lengthSq(); + desiredOmega = R.clone().cross(motionDir.clone().multiply(desiredYVelocity).add(sideDir.clone().multiply(desiredXVelocity))).multiply(1 / rsq); + aControl = desiredOmega.clone().sub(this.omega); + var aScalar = aControl.length(); + if (aScalar > this._angularAcceleration) { + aControl = aControl.multiply(this._angularAcceleration / aScalar); + } + return {result: false, aControl: aControl, desiredOmega: desiredOmega}; + } + return {result: true, aControl: aControl, desiredOmega: desiredOmega}; + } + + function velocityCancel(surfaceSlide:Bool, noBounce:Bool, contacts:Array) { + var SurfaceDotThreshold = 0.001; + var looped = false; + var itersIn = 0; + var done:Bool; + do { + done = true; + itersIn++; + for (i in 0...contacts.length) { + var sVel = this.velocity.clone().sub(contacts[i].velocity); + var surfaceDot = contacts[i].normal.dot(sVel); + if ((!looped && surfaceDot < 0) || surfaceDot < -SurfaceDotThreshold) { + var velLen = this.velocity.length(); + var surfaceVel = contacts[i].normal.clone().multiply(surfaceDot); + this.ReportBounce(contacts[i].point, contacts[i].normal, -surfaceDot); + if (noBounce) { + this.velocity = this.velocity.sub(surfaceVel); + } else if (contacts[i].collider != null) { + var info = contacts[i]; + var bounce2 = 0.5; + var normV = info.normal.clone().multiply(this.velocity.dot(info.normal)); + normV = normV.multiply(1 + bounce2); + this.velocity = this.velocity.sub(normV); + } else { + var velocity2 = contacts[i].velocity; + if (velocity2.length() > 0.0001 && !surfaceSlide && surfaceDot > -this._maxDotSlide * velLen) { + var vel = this.velocity.clone(); + vel = vel.sub(surfaceVel); + vel.normalize(); + vel = vel.multiply(velLen); + this.velocity = vel; + surfaceSlide = true; + } else if (surfaceDot > -this._minBounceVel) { + var vel = this.velocity.clone(); + vel = vel.sub(surfaceVel); + this.velocity = vel; + } else { + var restitution = this._bounceRestitution; + restitution *= contacts[i].restitution; + var velocityAdd = surfaceVel.clone().multiply(-(1 + restitution)); + var vAtC = sVel.clone().add(this.omega.clone().cross(contacts[i].normal.clone().multiply(this._radius).multiply(-1))); + var normalVel = -contacts[i].normal.dot(sVel); + vAtC = vAtC.sub(contacts[i].normal.clone().multiply(contacts[i].normal.dot(sVel))); + var vAtCMag = vAtC.length(); + if (vAtCMag != 0) { + var friction = this._bounceKineticFriction * contacts[i].friction; + var angVMagnitude = 5 * friction * normalVel / (2 * this._radius); + if (angVMagnitude > vAtCMag / this._radius) { + angVMagnitude = vAtCMag / this._radius; + } + var vAtCDir = vAtC.clone().multiply(1 / vAtCMag); + var deltaOmega = contacts[i].normal.clone() + .multiply(-1) + .cross(vAtCDir.clone().multiply(-1)) + .multiply(angVMagnitude); + this.omega = this.omega.add(deltaOmega); + this.velocity = this.velocity.sub(deltaOmega.clone().multiply(-1).cross(contacts[i].normal.clone().multiply(-this._radius))); + } + this.velocity = this.velocity.add(velocityAdd); + } + } + done = false; + } + } + looped = true; + if (itersIn > 6 && noBounce) { + done = true; + } + } while (!done); + // if (velocity.LengthSquared() < 625f) + // { + var gotOne = false; + var dir = new Vector(0, 0, 0); + for (j in 0...contacts.length) { + var dir2 = dir.clone().add(contacts[j].normal); + if (dir2.lengthSq() < 0.01) { + dir2 = dir2.add(contacts[j].normal); + } + dir = dir2; + gotOne = true; + } + if (gotOne) { + dir.normalize(); + var soFar = 0.0; + for (k in 0...contacts.length) { + if (contacts[k].penetration < this._radius) { + var timeToSeparate = 0.1; + var dist = contacts[k].penetration; + var outVel = this.velocity.clone().add(dir.clone().multiply(soFar)).dot(contacts[k].normal); + + if (timeToSeparate * outVel < dist) { + soFar += (dist - outVel * timeToSeparate) / timeToSeparate / contacts[k].normal.dot(dir); + } + } + } + // if (soFar < -25) soFar = -25; + // if (soFar > 25) soFar = 25; + this.velocity = this.velocity.add(dir.clone().multiply(soFar)); + } + // } + } + + function applyContactForces(dt:Float, m:Move, isCentered:Bool, aControl:Vector, desiredOmega:Vector, A:Vector, contacts:Array) { + var a = new Vector(); + this._slipAmount = 0; + var gWorkGravityDir = new Vector(0, 0, -1); + var bestSurface = -1; + var bestNormalForce = 0.0; + for (i in 0...contacts.length) { + if (contacts[i].collider == null) { + var normalForce = -contacts[i].normal.dot(A); + if (normalForce > bestNormalForce) { + bestNormalForce = normalForce; + bestSurface = i; + } + } + } + var bestContact = (bestSurface != -1) ? contacts[bestSurface] : new CollisionInfo(); + var canJump = bestSurface != -1; + if (canJump && m.jump) { + var velDifference = this.velocity.clone().sub(bestContact.velocity); + var sv = bestContact.normal.dot(velDifference); + if (sv < 0) { + sv = 0; + } + if (sv < this._jumpImpulse) { + this.velocity = this.velocity.add(bestContact.normal.clone().multiply((this._jumpImpulse - sv))); + } + } + for (j in 0...contacts.length) { + var normalForce2 = -contacts[j].normal.dot(A); + if (normalForce2 > 0 && contacts[j].normal.dot(this.velocity.clone().sub(contacts[j].velocity)) <= 0.0001) { + A = A.add(contacts[j].normal.multiply(normalForce2)); + } + } + if (bestSurface != -1) { + // TODO: FIX + // bestContact.velocity - bestContact.normal * Vector3.Dot(bestContact.normal, bestContact.velocity); + var vAtC = this.velocity.clone().add(this.omega.clone().cross(bestContact.normal.clone().multiply(-this._radius))).sub(bestContact.velocity); + var vAtCMag = vAtC.length(); + var slipping = false; + var aFriction = new Vector(0, 0, 0); + var AFriction = new Vector(0, 0, 0); + if (vAtCMag != 0) { + slipping = true; + var friction = this._kineticFriction * bestContact.friction; + var angAMagnitude = 5 * friction * bestNormalForce / (2 * this._radius); + var AMagnitude = bestNormalForce * friction; + var totalDeltaV = (angAMagnitude * this._radius + AMagnitude) * dt; + if (totalDeltaV > vAtCMag) { + var fraction = vAtCMag / totalDeltaV; + angAMagnitude *= fraction; + AMagnitude *= fraction; + slipping = false; + } + var vAtCDir = vAtC.clone().multiply(1 / vAtCMag); + aFriction = bestContact.normal.clone() + .multiply(-1) + .cross(vAtCDir.clone().multiply(-1)) + .multiply(angAMagnitude); + AFriction = vAtCDir.clone().multiply(-AMagnitude); + this._slipAmount = vAtCMag - totalDeltaV; + } + if (!slipping) { + var R = gWorkGravityDir.clone().multiply(-this._radius); + var aadd = R.cross(A).multiply(1 / R.lengthSq()); + if (isCentered) { + var nextOmega = this.omega.add(a.clone().multiply(dt)); + aControl = desiredOmega.clone().sub(nextOmega); + var aScalar = aControl.length(); + if (aScalar > this._brakingAcceleration) { + aControl = aControl.multiply(this._brakingAcceleration / aScalar); + } + } + var Aadd = aControl.clone().cross(bestContact.normal.multiply(-this._radius)).multiply(-1); + var aAtCMag = aadd.clone().cross(bestContact.normal.multiply(-this._radius)).add(Aadd).length(); + var friction2 = this._staticFriction * bestContact.friction; + + if (aAtCMag > friction2 * bestNormalForce) { + friction2 = this._kineticFriction * bestContact.friction; + Aadd = Aadd.multiply(friction2 * bestNormalForce / aAtCMag); + } + A = A.add(Aadd); + a = a.add(aadd); + } + A = A.add(AFriction); + a = a.add(aFriction); + } + a = a.add(aControl); + return [A, a]; + } + + function ReportBounce(pos:Vector, normal:Vector, speed:Float) { + if (this._bounceYet && speed < this._bounceSpeed) { + return; + } + this._bounceYet = true; + this._bouncePos = pos; + this._bounceSpeed = speed; + this._bounceNormal = normal; + } + + function advancePhysics(m:Move, dt:Float, collisionWorld:CollisionWorld) { + var contacts = this.findContacts(collisionWorld); + var cmf = this.computeMoveForces(m); + var isCentered:Bool = cmf.result; + var aControl = cmf.aControl; + var desiredOmega = cmf.desiredOmega; + this.velocityCancel(isCentered, false, contacts); + var A = this.getExternalForces(m, dt, contacts); + var retf = this.applyContactForces(dt, m, isCentered, aControl, desiredOmega, A, contacts); + A = retf[0]; + var a = retf[1]; + this.velocity = this.velocity.add(A.multiply(dt)); + this.omega = this.omega.add(a.multiply(dt)); + this.velocityCancel(isCentered, true, contacts); + this._totalTime += dt; + if (contacts.length != 0) { + this._contactTime += dt; + } + } + + public function update(dt:Float, collisionWorld:CollisionWorld) { + var move = new Move(); + move.d = new Vector(); + if (Key.isDown(Key.W)) { + move.d.x -= 1; + } + if (Key.isDown(Key.S)) { + move.d.x += 1; + } + if (Key.isDown(Key.A)) { + move.d.y += 1; + } + if (Key.isDown(Key.D)) { + move.d.y -= 1; + } + if (Key.isDown(Key.SPACE)) { + move.jump = true; + } + + var timeRemaining = dt; + var it = 0; + do { + if (timeRemaining <= 0) + break; + + var timeStep = 0.00800000037997961; + if (timeRemaining < 0.00800000037997961) + timeStep = timeRemaining; + + advancePhysics(move, timeStep, collisionWorld); + var newPos = this.getAbsPos().getPosition().add(this.velocity.multiply(timeStep)); + this.setPosition(newPos.x, newPos.y, newPos.z); + + timeRemaining -= timeStep; + it++; + } while (it <= 10); + + this.camera.target.load(this.getAbsPos().getPosition().toPoint()); + } +} diff --git a/src/collision/Collision.hx b/src/collision/Collision.hx new file mode 100644 index 00000000..6bb0a969 --- /dev/null +++ b/src/collision/Collision.hx @@ -0,0 +1,571 @@ +package collision; + +import dif.math.Point3F; +import dif.math.PlaneF; +import h3d.col.Plane; +import h3d.Vector; + +typedef ISCResult = { + var result:Bool; + var tSeg:Float; + var tCap:Float; +} + +typedef CPSSResult = { + var result:Float; + var s:Float; + var t:Float; + var c1:Vector; + var c2:Vector; +} + +typedef ITSResult = { + var result:Bool; + var normal:Vector; + var point:Vector; +} + +class Collision { + public static function IntersectLineSphere(start:Vector, end:Vector, center:Vector, radius:Float) { + var d = end.sub(start).normalized(); + var v = center.sub(start); + var t = v.dot(d); + var p = start.add(d.multiply(t)); + var dist = center.distance(p); + + if (dist > radius) { + return null; + } else + return p; + } + + public static function IntersectTriangleSphere(v0:Vector, v1:Vector, v2:Vector, normal:Vector, center:Vector, radius:Float) { + var radiusSq = radius * radius; + + var res:ITSResult = { + result: false, + point: null, + normal: null + }; + + var p = PlaneF.PointNormal(new Point3F(v0.x, v0.y, v0.z), new Point3F(normal.x, normal.y, normal.z)); + var pdist = p.distance(new Point3F(center.x, center.y, center.z)); + + if (pdist < 0) { + return res; // Dont collide internal edges + } + + function toDifPoint(pt:Vector) { + return new Point3F(pt.x, pt.y, pt.z); + } + + function fromDifPoint(pt:Point3F) { + return new Vector(pt.x, pt.y, pt.z); + } + + if (pdist < radius) { + var t = -toDifPoint(center).dot(p.getNormal()) / p.getNormal().lengthSq(); + var pt = fromDifPoint(p.project(toDifPoint(center))); // center.add(fromDifPoint(p.getNormal().scalar(t))); + if (PointInTriangle(pt, v0, v1, v2)) { + res.result = true; + res.point = pt; + res.normal = center.sub(pt).normalized(); + return res; + } + // return res; + } + + // Check points + if (center.sub(v0).lengthSq() < radiusSq) { + res.result = true; + res.point = v0; + res.normal = center.sub(v0).normalized(); + // center.sub(v0).normalized(); + return res; + } + if (center.sub(v1).lengthSq() < radiusSq) { + res.result = true; + res.point = v1; + res.normal = center.sub(v1).normalized(); + + return res; + } + if (center.sub(v2).lengthSq() < radiusSq) { + res.result = true; + res.point = v2; + res.normal = center.sub(v2).normalized(); + + return res; + } + + // Check edges + var r1 = IntersectLineSphere(v0, v1, center, radius); + if (r1 != null) { + res.result = true; + res.point = r1; + res.normal = center.sub(r1).normalized(); + return res; + } + var r2 = IntersectLineSphere(v1, v2, center, radius); + if (r2 != null) { + res.result = true; + res.point = r2; + res.normal = center.sub(r2).normalized(); + return res; + } + var r3 = IntersectLineSphere(v2, v0, center, radius); + if (r3 != null) { + res.result = true; + res.point = r3; + res.normal = center.sub(r3).normalized(); + return res; + } + + // Check plane + // var p = PlaneF.ThreePoints(toDifPoint(v0), toDifPoint(v1), toDifPoint(v2)); + return res; + } + + public static function IntersectSegmentCapsule(segStart:Vector, segEnd:Vector, capStart:Vector, capEnd:Vector, radius:Float) { + var cpssres = Collision.ClosestPtSegmentSegment(segStart, segEnd, capStart, capEnd); + var res:ISCResult = { + result: cpssres.result < radius * radius, + tSeg: cpssres.s, + tCap: cpssres.t + } + return res; + } + + public static function ClosestPtSegmentSegment(p1:Vector, q1:Vector, p2:Vector, q2:Vector) { + var Epsilon = 0.0001; + var d3 = q1.sub(p1); + var d2 = q2.sub(p2); + var r = p1.sub(p2); + var a = d3.dot(d3); + var e = d2.dot(d2); + var f = d2.dot(r); + + var res:CPSSResult = { + s: 0, + t: 0, + c1: null, + c2: null, + result: -1 + } + + if (a <= Epsilon && e <= Epsilon) { + res = { + s: 0, + t: 0, + c1: p1, + c2: p2, + result: p1.sub(p2).dot(p1.sub(p2)) + } + return res; + } + if (a <= Epsilon) { + res.s = 0; + res.t = f / e; + if (res.t > 1) + res.t = 1; + if (res.t < 0) + res.t = 0; + } else { + var c3 = d3.dot(r); + if (e <= Epsilon) { + res.t = 0; + if (-c3 / a > 1) + res.s = 1; + else if (-c3 / a < 0) + res.s = 0; + else + res.s = (-c3 / a); + } else { + var b = d3.dot(d2); + var denom = a * e - b * b; + if (denom != 0) { + res.s = (b * f - c3 * e) / denom; + if (res.s > 1) + res.s = 1; + if (res.s < 0) + res.s = 0; + } else { + res.s = 0; + } + res.t = (b * res.s + f) / e; + if (res.t < 0) { + res.t = 0; + res.s = -c3 / a; + if (res.s > 1) + res.s = 1; + if (res.s < 0) + res.s = 0; + } else if (res.t > 1) { + res.t = 1; + res.s = (b - c3) / a; + if (res.s > 1) + res.s = 1; + if (res.s < 0) + res.s = 0; + } + } + } + res.c1 = p1.add(d3.multiply(res.s)); + res.c2 = p2.add(d2.multiply(res.t)); + res.result = res.c1.sub(res.c2).lengthSq(); + return res; + } + + private static function PointInTriangle(point:Vector, v0:Vector, v1:Vector, v2:Vector):Bool { + var u = v1.sub(v0); + var v = v2.sub(v0); + var w = point.sub(v0); + + var vw = v.cross(w); + var vu = v.cross(u); + + if (vw.dot(vu) < 0.0) { + return false; + } + + var uw = u.cross(w); + var uv = u.cross(v); + + if (uw.dot(uv) < 0.0) { + return false; + } + + var d:Float = uv.length(); + var r:Float = vw.length() / d; + var t:Float = uw.length() / d; + + return (r + t) <= 1; + } + + private static function PointInTriangle2(point:Vector, a:Vector, b:Vector, c:Vector):Bool { + var a1 = a.sub(point); + var b1 = b.sub(point); + var c1 = c.sub(point); + + var u = b1.cross(c1); + var v = c1.cross(a1); + + if (u.dot(v) < 0) + return false; + + var w = a1.cross(b1); + return !(u.dot(w) < 0); + } + + private static function GetLowestRoot(a:Float, b:Float, c:Float, max:Float):Null { + // check if solution exists + var determinant:Float = b * b - 4.0 * a * c; + + // if negative there is no solution + if (determinant < 0.0) { + return null; + } + + // calculate two roots + var sqrtD:Float = Math.sqrt(determinant); + var r1:Float = (-b - sqrtD) / (2 * a); + var r2:Float = (-b + sqrtD) / (2 * a); + + // set x1 <= x2 + if (r1 > r2) { + var temp:Float = r2; + r2 = r1; + r1 = temp; + } + + // get lowest root + if (r1 > 0 && r1 < max) { + return r1; + } + + if (r2 > 0 && r2 < max) { + return r2; + } + + // no solutions + return null; + } + + public static function ClosestPtPointTriangle(pt:Vector, radius:Float, p0:Vector, p1:Vector, p2:Vector, normal:Vector) { + var closest:Vector = null; + var ptDot = pt.dot(normal); + var triDot = p0.dot(normal); + if (Math.abs(ptDot - triDot) > radius * 1.1) { + return null; + } + closest = pt.add(normal.multiply(triDot - ptDot)); + if (Collision.PointInTriangle2(closest, p0, p1, p2)) { + return closest; + } + var t = 10.0; + var r1 = Collision.IntersectSegmentCapsule(pt, pt, p0, p1, radius); + if (r1.result && r1.tSeg < t) { + closest = p0.add((p1.sub(p0).multiply(r1.tCap))); + t = r1.tSeg; + } + var r2 = Collision.IntersectSegmentCapsule(pt, pt, p1, p2, radius); + if (r2.result && r2.tSeg < t) { + closest = p1.add((p2.sub(p1).multiply(r2.tCap))); + t = r2.tSeg; + } + var r3 = Collision.IntersectSegmentCapsule(pt, pt, p2, p0, radius); + if (r3.result && r3.tSeg < t) { + closest = p2.add((p2.sub(p2).multiply(r3.tCap))); + t = r3.tSeg; + } + var res = t < 1; + if (res) { + return closest; + } + return null; + } + + public static function CheckTriangle(packet:CollisionPacket, p1:Vector, p2:Vector, p3:Vector) { + function toDifPoint(pt:Vector) { + return new Point3F(pt.x, pt.y, pt.z); + } + + function fromDifPoint(pt:Point3F) { + return new Vector(pt.x, pt.y, pt.z); + } + + var plane = PlaneF.ThreePoints(toDifPoint(p1), toDifPoint(p2), toDifPoint(p3)); + + // only check front facing triangles + var dist = plane.distance(toDifPoint(packet.e_norm_velocity)); + if (dist < 0) { + return packet; + } + + // get interval of plane intersection + var t0:Float = 0.0; + var t1:Float = 0.0; + var embedded_in_plane:Bool = false; + + // signed distance from sphere to point on plane + var signed_dist_to_plane:Float = plane.distance(toDifPoint(packet.e_base_point)); + + // cache this as we will reuse + var normal_dot_vel = plane.getNormal().dot(toDifPoint(packet.e_velocity)); + + // if sphere is moving parrallel to plane + if (normal_dot_vel == 0.0) { + if (Math.abs(signed_dist_to_plane) >= 1.0) { + // no collision possible + return packet; + } else { + // sphere is in plane in whole range [0..1] + embedded_in_plane = true; + t0 = 0.0; + t1 = 1.0; + } + } else { + // N dot D is not 0, calc intersect interval + t0 = (-1.0 - signed_dist_to_plane) / normal_dot_vel; + t1 = (1.0 - signed_dist_to_plane) / normal_dot_vel; + + // swap so t0 < t1 + if (t0 > t1) { + var temp = t1; + t1 = t0; + t0 = temp; + } + + // check that at least one result is within range + if (t0 > 1.0 || t1 < 0.0) { + // both values outside range [0,1] so no collision + return packet; + } + + // clamp to [0,1] + if (t0 < 0.0) { + t0 = 0.0; + } + if (t1 < 0.0) { + t1 = 0.0; + } + if (t0 > 1.0) { + t0 = 1.0; + } + if (t1 > 1.0) { + t1 = 1.0; + } + } + + // time to check for a collision + var collision_point:Vector = new Vector(0.0, 0.0, 0.0); + var found_collision:Bool = false; + var t:Float = 1.0; + + // first check collision with the inside of the triangle + if (!embedded_in_plane) { + var plane_intersect:Vector = packet.e_base_point.sub(fromDifPoint(plane.getNormal())); + var temp:Vector = packet.e_velocity.multiply(t0); + plane_intersect = plane_intersect.add(temp); + + if (Collision.PointInTriangle(plane_intersect, p1, p2, p3)) { + found_collision = true; + t = t0; + collision_point = plane_intersect; + } + } + + // no collision yet, check against points and edges + if (!found_collision) { + var velocity = packet.e_velocity.clone(); + var base = packet.e_base_point.clone(); + + var velocity_sq_length = velocity.lengthSq(); + var a:Float = velocity_sq_length; + var b:Float = 0.0; + var c:Float = 0.0; + + // equation is a*t^2 + b*t + c = 0 + // check against points + + // p1 + var temp = base.sub(p1); + b = 2.0 * velocity.dot(temp); + temp = p1.sub(base); + c = temp.lengthSq() - 1.0; + var new_t = Collision.GetLowestRoot(a, b, c, t); + if (new_t != null) { + t = new_t; + found_collision = true; + collision_point = p1; + } + + // p2 + if (!found_collision) { + temp = base.sub(p2); + b = 2.0 * velocity.dot(temp); + temp = p2.sub(base); + c = temp.lengthSq() - 1.0; + new_t = Collision.GetLowestRoot(a, b, c, t); + if (new_t != null) { + t = new_t; + found_collision = true; + collision_point = p2; + } + } + + // p3 + if (!found_collision) { + temp = base.sub(p3); + b = 2.0 * velocity.dot(temp); + temp = p3.sub(base); + c = temp.lengthSq() - 1.0; + new_t = Collision.GetLowestRoot(a, b, c, t); + if (new_t != null) { + t = new_t; + found_collision = true; + collision_point = p3; + } + } + + // check against edges + // p1 -> p2 + var edge = p2.sub(p1); + var base_to_vertex = p1.sub(base); + var edge_sq_length = edge.lengthSq(); + var edge_dot_velocity = edge.dot(velocity); + var edge_dot_base_to_vertex = edge.dot(base_to_vertex); + + // calculate params for equation + a = edge_sq_length * -velocity_sq_length + edge_dot_velocity * edge_dot_velocity; + b = edge_sq_length * (2.0 * velocity.dot(base_to_vertex)) - 2.0 * edge_dot_velocity * edge_dot_base_to_vertex; + c = edge_sq_length * (1.0 - base_to_vertex.lengthSq()) + edge_dot_base_to_vertex * edge_dot_base_to_vertex; + + // do we collide against infinite edge + new_t = Collision.GetLowestRoot(a, b, c, t); + if (new_t != null) { + // check if intersect is within line segment + var f = (edge_dot_velocity * new_t - edge_dot_base_to_vertex) / edge_sq_length; + if (f >= 0.0 && f <= 1.0) { + t = new_t; + found_collision = true; + collision_point = p1.add(edge.multiply(f)); + } + } + + // p2 -> p3 + edge = p3.sub(p2); + base_to_vertex = p2.sub(base); + edge_sq_length = edge.lengthSq(); + edge_dot_velocity = edge.dot(velocity); + edge_dot_base_to_vertex = edge.dot(base_to_vertex); + + // calculate params for equation + a = edge_sq_length * -velocity_sq_length + edge_dot_velocity * edge_dot_velocity; + b = edge_sq_length * (2.0 * velocity.dot(base_to_vertex)) - 2.0 * edge_dot_velocity * edge_dot_base_to_vertex; + c = edge_sq_length * (1.0 - base_to_vertex.lengthSq()) + edge_dot_base_to_vertex * edge_dot_base_to_vertex; + + // do we collide against infinite edge + new_t = Collision.GetLowestRoot(a, b, c, t); + if (new_t != null) { + // check if intersect is within line segment + var f = (edge_dot_velocity * new_t - edge_dot_base_to_vertex) / edge_sq_length; + if (f >= 0.0 && f <= 1.0) { + t = new_t; + found_collision = true; + collision_point = p2.add(edge.multiply(f)); + } + } + + // p3 -> p1 + edge = p1.sub(p3); + base_to_vertex = p3.sub(base); + edge_sq_length = edge.lengthSq(); + edge_dot_velocity = edge.dot(velocity); + edge_dot_base_to_vertex = edge.dot(base_to_vertex); + + // calculate params for equation + a = edge_sq_length * -velocity_sq_length + edge_dot_velocity * edge_dot_velocity; + b = edge_sq_length * (2.0 * velocity.dot(base_to_vertex)) - 2.0 * edge_dot_velocity * edge_dot_base_to_vertex; + c = edge_sq_length * (1.0 - base_to_vertex.lengthSq()) + edge_dot_base_to_vertex * edge_dot_base_to_vertex; + + // do we collide against infinite edge + new_t = Collision.GetLowestRoot(a, b, c, t); + if (new_t != null) { + // check if intersect is within line segment + var f = (edge_dot_velocity * new_t - edge_dot_base_to_vertex) / edge_sq_length; + if (f >= 0.0 && f <= 1.0) { + t = new_t; + found_collision = true; + collision_point = p3.add(edge.multiply(f)); + } + } + } + + // set results + if (found_collision) { + // distance to collision, t is time of collision + var dist_to_coll = t * packet.e_velocity.length(); + + // are we the closest hit? + if (!packet.found_collision || dist_to_coll < packet.nearest_distance) { + packet.nearest_distance = dist_to_coll; + packet.intersect_point = collision_point; + packet.found_collision = true; + } + + // HACK: USE SENSORS FOR THIS AND YOU DON'T GET WALL HITS ANYMORE + // Work out the hit normal so we can determine if the player is in + // contact with a wall or the ground. + var n = collision_point.sub(packet.e_base_point); + n.normalize(); + + var dz = n.dot(new Vector(0, 0, 1)); + if (dz <= -0.5) { + packet.grounded = true; + } + } + + return packet; + } +} diff --git a/src/collision/CollisionEntity.hx b/src/collision/CollisionEntity.hx new file mode 100644 index 00000000..39ad2b96 --- /dev/null +++ b/src/collision/CollisionEntity.hx @@ -0,0 +1,222 @@ +package collision; + +import dif.math.Point3F; +import dif.math.PlaneF; +import h3d.col.Plane; +import octree.Octree; +import h3d.col.Ray; +import h3d.Vector; +import octree.IOctreeObject; +import h3d.Matrix; +import h3d.col.Bounds; + +class CollisionEntity implements IOctreeObject { + public var boundingBox:Bounds; + + var octree:Octree; + + public var surfaces:Array; + + public var priority:Int; + public var position:Int; + + var transform:Matrix; + + public function new() { + this.octree = new Octree(); + this.surfaces = []; + this.transform = Matrix.I(); + } + + public function addSurface(surface:CollisionSurface) { + this.octree.insert(surface); + this.surfaces.push(surface); + } + + public function generateBoundingBox() { + var boundingBox = new Bounds(); + for (surface in this.surfaces) { + var tform = surface.boundingBox.clone(); + tform.transform(transform); + boundingBox.add(tform); + } + this.boundingBox = boundingBox; + } + + public function isIntersectedByRay(rayOrigin:Vector, rayDirection:Vector, intersectionPoint:Vector):Bool { + // TEMP cause bruh + return boundingBox.rayIntersection(Ray.fromValues(rayOrigin.x, rayOrigin.y, rayOrigin.z, rayDirection.x, rayDirection.y, rayDirection.z), true) != -1; + } + + public function getElementType() { + return 2; + } + + public function setPriority(priority:Int) { + this.priority = priority; + } + + public function sphereIntersection(position:Vector, velocity:Vector, radius:Float) { + var invMatrix = transform.clone(); + invMatrix.invert(); + var localpos = position.clone(); + localpos.transform(invMatrix); + + var bigRad = 2 * velocity.length() + radius * 1.1; + + var surfaces = octree.radiusSearch(position, radius * 1.1); + + var contacts = []; + + function toDifPoint(pt:Vector) { + return new Point3F(pt.x, pt.y, pt.z); + } + + function fromDifPoint(pt:Point3F) { + return new Vector(pt.x, pt.y, pt.z); + } + + for (obj in surfaces) { + var surface:CollisionSurface = cast obj; + + var i = 0; + while (i < surface.indices.length) { + var v0 = surface.points[surface.indices[i]]; + var v = surface.points[surface.indices[i + 1]]; + var v2 = surface.points[surface.indices[i + 2]]; + + // var packet = new CollisionPacket(position, velocity, new Vector(radius, radius, radius)); + // packet.e_base_point = packet.e_position.clone(); + // packet.e_norm_velocity = packet.e_velocity.clone().normalized(); + // packet.nearest_distance = 1e20; + + // var plane = PlaneF.PointNormal(toDifPoint(v), toDifPoint(surface.normals[surface.indices[i]])); + + // var retpacket = Collision.CheckTriangle(packet, v0.multiply(1 / radius), v.multiply(1 / radius), v2.multiply(1 / radius)); + + // if (retpacket.found_collision) { + // var cinfo = new CollisionInfo(); + // cinfo.restitution = 1; + // cinfo.friction = 1; + // cinfo.normal = surface.normals[surface.indices[i]]; + // cinfo.point = retpacket.intersect_point; + // cinfo.velocity = new Vector(); + // cinfo.collider = null; + // cinfo.penetration = radius - (position.sub(cinfo.point).dot(cinfo.normal)); + // contacts.push(cinfo); + // } + // var plane = PlaneF.ThreePoints(toDifPoint(v0), toDifPoint(v), toDifPoint(v2)); + + // var distance = plane.distance(toDifPoint(position)); + + // if (Math.abs(distance) <= radius + 0.001) { + // var lastVertex = surface.points[surface.indices[surface.indices.length - 1]]; + + // var contactVert = plane.project(toDifPoint(position)); + // var separation = Math.sqrt(radius * radius - distance * distance); + + // for (j in 0...surface.indices.length) { + // var vertex = surface.points[surface.indices[i]]; + // if (vertex != lastVertex) { + // var vertPlane = PlaneF.ThreePoints(toDifPoint(vertex).add(plane.getNormal()), toDifPoint(vertex), toDifPoint(lastVertex)); + // var vertDistance = vertPlane.distance(contactVert); + // if (vertDistance < 0.0) { + // if (vertDistance < -(separation + 0.0001)) + // return contacts; + // // return contacts; + + // if (PlaneF.ThreePoints(vertPlane.getNormal().add(toDifPoint(vertex)), toDifPoint(vertex), + // toDifPoint(vertex).add(plane.getNormal())) + // .distance(contactVert) >= 0.0) { + // if (PlaneF.ThreePoints(toDifPoint(lastVertex).sub(vertPlane.getNormal()), toDifPoint(lastVertex), + // toDifPoint(lastVertex).add(plane.getNormal())) + // .distance(contactVert) >= 0.0) { + // contactVert = vertPlane.project(contactVert); + // break; + // } + // contactVert = toDifPoint(lastVertex); + // } else { + // contactVert = toDifPoint(vertex); + // } + // } + // lastVertex = vertex; + // } + + // var cinfo = new CollisionInfo(); + // cinfo.restitution = 1; + // cinfo.friction = 1; + // cinfo.normal = surface.normals[i]; + // cinfo.point = fromDifPoint(contactVert); + // cinfo.velocity = new Vector(); + // cinfo.collider = null; + // cinfo.penetration = radius - (position.sub(cinfo.point).dot(cinfo.normal)); + // contacts.push(cinfo); + // } + // } + + // // var norm = Plane.fromPoints(v0.toPoint(), v.toPoint(), v2.toPoint()); + + // var cinfo = new CollisionInfo(); + // cinfo.restitution = 1; + // cinfo.friction = 1; + // cinfo.normal = surface.normals[i]; + // cinfo.point = fromDifPoint(contactVert); + // cinfo.velocity = new Vector(); + // cinfo.collider = null; + // cinfo.penetration = radius - (position.sub(cinfo.point).dot(cinfo.normal)); + // contacts.push(cinfo); + // } + + var res = Collision.IntersectTriangleSphere(v0, v, v2, surface.normals[surface.indices[i]], position, radius); + var closest = res.point; + // Collision.ClosestPtPointTriangle(position, radius, v0, v, v2, surface.normals[surface.indices[i]]); + if (res.result) { + if (position.sub(closest).lengthSq() < radius * radius) { + var normal = res.normal; + + if (position.sub(closest).dot(surface.normals[surface.indices[i]]) > 0) { + normal.normalize(); + + var cinfo = new CollisionInfo(); + cinfo.normal = res.normal; // surface.normals[surface.indices[i]]; + cinfo.point = closest; + // cinfo.collider = this; + cinfo.velocity = new Vector(); + cinfo.penetration = radius - (position.sub(closest).dot(normal)); + cinfo.restitution = 1; + cinfo.friction = 1; + contacts.push(cinfo); + } + } + } + + // var res = Collision.IntersectTriangleSphere(v0, v, v2, surface.normals[surface.indices[i]], position, radius); + // var closest = res.point; + // var closest = Collision.ClosestPtPointTriangle(position, radius, v0, v, v2, surface.normals[surface.indices[i]]); + // if (closest != null) { + // if (position.sub(closest).lengthSq() < radius * radius) { + // var normal = position.sub(closest); + + // if (position.sub(closest).dot(surface.normals[surface.indices[i]]) > 0) { + // normal.normalize(); + + // var cinfo = new CollisionInfo(); + // cinfo.normal = normal; + // cinfo.point = closest; + // // cinfo.collider = this; + // cinfo.velocity = new Vector(); + // cinfo.penetration = radius - (position.sub(closest).dot(normal)); + // cinfo.restitution = 1; + // cinfo.friction = 1; + // contacts.push(cinfo); + // } + // } + // } + + i += 3; + } + } + + return contacts; + } +} diff --git a/src/collision/CollisionInfo.hx b/src/collision/CollisionInfo.hx new file mode 100644 index 00000000..beb9d29d --- /dev/null +++ b/src/collision/CollisionInfo.hx @@ -0,0 +1,15 @@ +package collision; + +import h3d.Vector; + +class CollisionInfo { + public var point:Vector; + public var normal:Vector; + public var velocity:Vector; + public var collider:CollisionEntity; + public var friction:Float; + public var restitution:Float; + public var penetration:Float; + + public function new() {} +} diff --git a/src/collision/CollisionPacket.hx b/src/collision/CollisionPacket.hx new file mode 100644 index 00000000..af26dbb5 --- /dev/null +++ b/src/collision/CollisionPacket.hx @@ -0,0 +1,67 @@ +package collision; + +import h3d.Vector; + +@:publicFields +class CollisionPacket { + static var LARGE_NUMBER:Float = 1e20; + + /** Position in world space. Assumes Z-up. **/ + var r3_position = new Vector(0.0, 0.0, 0.0); + + /** Velocity in world space. Assumes Z-up. **/ + var r3_velocity = new Vector(0.0, 0.0, 0.0); + + /** Ellipsoid radius in world units. Assumes Z-up. **/ + var e_radius = new Vector(1.0, 1.0, 1.0); + + /** Position in ellipsoid-space. Used internally. **/ + var e_position = new Vector(0.0, 0.0, 0.0); + + /** Velocity in ellipsoid-space. Used internally. **/ + var e_velocity = new Vector(0.0, 0.0, 0.0); + + /** Found a collision. **/ + var found_collision = false; + + /** Distance to nearest collision if `found_collision` is set, otherwise `1e20` **/ + var nearest_distance = LARGE_NUMBER; + + /** Iteration depth. Useful for debugging slow collisions. **/ + var depth:Int = 0; + + // internal stuff + var e_norm_velocity = new Vector(0.0, 0.0, 0.0); + var e_base_point = new Vector(0.0, 0.0, 0.0); + var intersect_point = new Vector(0.0, 0.0, 0.0); + + /** `true` if packet is on something reasonable to call the ground. + * + * _in practice, you probably want a sensor in your game code instead._ **/ + var grounded:Bool = false; + + /** Recalculate e-space from r3 vectors. Call this if you updated `r3_*` **/ + inline function to_e() { + this.e_position = new Vector(this.r3_position.x / this.e_radius.x, this.r3_position.y / this.e_radius.y, this.r3_position.z / this.e_radius.z); + this.e_velocity = new Vector(this.r3_velocity.x / this.e_radius.x, this.r3_velocity.y / this.e_radius.y, this.r3_velocity.z / this.e_radius.z); + } + + /** Recalculate e-space from r3 vectors. Call this if you updated `e_*` **/ + inline function to_r3() { + this.r3_position = new Vector(this.e_position.x * this.e_radius.x, this.e_position.y * this.e_radius.y, this.e_position.z * this.e_radius.z); + this.r3_velocity = new Vector(this.e_velocity.x * this.e_radius.x, this.e_velocity.y * this.e_radius.y, this.e_velocity.z * this.e_radius.z); + } + + inline function new(?position:Vector, ?velocity:Vector, ?radius:Vector) { + if (position != null) { + this.r3_position = position; + } + if (velocity != null) { + this.r3_velocity = velocity; + } + if (radius != null) { + this.e_radius = radius; + } + this.to_e(); + } +} diff --git a/src/collision/CollisionSurface.hx b/src/collision/CollisionSurface.hx new file mode 100644 index 00000000..6a4f7e2c --- /dev/null +++ b/src/collision/CollisionSurface.hx @@ -0,0 +1,63 @@ +package collision; + +import h3d.col.Bounds; +import octree.IOctreeObject; +import h3d.Vector; + +class CollisionSurface implements IOctreeObject { + public var priority:Int; + public var position:Int; + + public var boundingBox:Bounds; + + public var points:Array; + public var normals:Array; + public var indices:Array; + + public function new() {} + + public function getElementType() { + return 2; + } + + public function generateBoundingBox() { + var boundingBox = new Bounds(); + boundingBox.xMin = 10e8; + boundingBox.yMin = 10e8; + boundingBox.zMin = 10e8; + boundingBox.xMax = -10e8; + boundingBox.yMax = -10e8; + boundingBox.zMax = -10e8; + + for (point in points) { + if (point.x > boundingBox.xMax) { + boundingBox.xMax = point.x; + } + if (point.x < boundingBox.xMin) { + boundingBox.xMin = point.x; + } + if (point.y > boundingBox.yMax) { + boundingBox.yMax = point.y; + } + if (point.y < boundingBox.yMin) { + boundingBox.yMin = point.y; + } + if (point.z > boundingBox.zMax) { + boundingBox.zMax = point.z; + } + if (point.z < boundingBox.zMin) { + boundingBox.zMin = point.z; + } + } + this.boundingBox = boundingBox; + } + + public function setPriority(priority:Int) { + this.priority = priority; + } + + public function isIntersectedByRay(rayOrigin:Vector, rayDirection:Vector, intersectionPoint:Vector):Bool { + // TEMP cause bruh + return true; + } +} diff --git a/src/collision/CollisionWorld.hx b/src/collision/CollisionWorld.hx new file mode 100644 index 00000000..89c6b0bb --- /dev/null +++ b/src/collision/CollisionWorld.hx @@ -0,0 +1,30 @@ +package collision; + +import h3d.Vector; +import octree.Octree; + +class CollisionWorld { + public var octree:Octree; + + public function new() { + this.octree = new Octree(); + } + + public function sphereIntersection(position:Vector, velocity:Vector, radius:Float) { + var searchdist = velocity.length() + radius; + var intersections = this.octree.radiusSearch(position, searchdist); + + var contacts = []; + + for (obj in intersections) { + var entity:CollisionEntity = cast obj; + + contacts = contacts.concat(entity.sphereIntersection(position, velocity, radius)); + } + return contacts; + } + + public function addEntity(entity:CollisionEntity) { + this.octree.insert(entity); + } +} diff --git a/src/dif/AISpecialNode.hx b/src/dif/AISpecialNode.hx new file mode 100644 index 00000000..46c89fa1 --- /dev/null +++ b/src/dif/AISpecialNode.hx @@ -0,0 +1,27 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import dif.math.Point3F; + +using dif.WriterExtensions; + +@:expose +class AISpecialNode { + public var name:String; + public var position:Point3F; + + public function new(name, position) { + this.name = name; + this.position = position; + } + + public static function read(io:BytesReader) { + return new AISpecialNode(io.readStr(), Point3F.read(io)); + } + + public function write(io:BytesWriter) { + io.writeStr(this.name); + this.position.write(io); + } +} diff --git a/src/dif/AnimatedLight.hx b/src/dif/AnimatedLight.hx new file mode 100644 index 00000000..64f397c1 --- /dev/null +++ b/src/dif/AnimatedLight.hx @@ -0,0 +1,34 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import haxe.Int32; + +@:expose +class AnimatedLight { + public var nameIndex:Int32; + public var stateIndex:Int32; + public var stateCount:Int; + public var flags:Int; + public var duration:Int32; + + public function new(nameIndex:Int32, stateIndex:Int32, stateCount:Int, flags:Int, duration:Int32) { + this.nameIndex = nameIndex; + this.stateIndex = stateIndex; + this.stateCount = stateCount; + this.flags = flags; + this.duration = duration; + } + + public static function read(io:BytesReader) { + return new AnimatedLight(io.readInt32(), io.readInt32(), io.readInt16(), io.readInt16(), io.readInt32()); + } + + public function write(io:BytesWriter) { + io.writeInt32(this.nameIndex); + io.writeInt32(this.stateIndex); + io.writeUInt16(this.stateCount); + io.writeUInt16(this.flags); + io.writeInt32(this.duration); + } +} diff --git a/src/dif/BSPNode.hx b/src/dif/BSPNode.hx new file mode 100644 index 00000000..450873e6 --- /dev/null +++ b/src/dif/BSPNode.hx @@ -0,0 +1,107 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class BSPNode { + public var planeIndex:Int; + public var frontIndex:Int; + public var backIndex:Int; + + public var isFrontLeaf:Bool; + public var isFrontSolid:Bool; + + public var isBackLeaf:Bool; + public var isBackSolid:Bool; + + public function new(planeIndex, frontIndex, backIndex, isFrontLeaf, isFrontSolid, isBackLeaf, isBackSolid) { + this.planeIndex = planeIndex; + this.frontIndex = frontIndex; + this.backIndex = backIndex; + this.isFrontLeaf = isFrontLeaf; + this.isFrontSolid = isFrontSolid; + this.isBackLeaf = isBackLeaf; + this.isBackSolid = isBackSolid; + } + + public static function read(io:BytesReader, version:Version) { + var planeIndex = io.readUInt16(); + var frontIndex, + backIndex, + isfrontleaf = false, + isfrontsolid = false, + isbackleaf = false, + isbacksolid = false; + if (version.interiorVersion >= 14) { + frontIndex = io.readInt32(); + backIndex = io.readInt32(); + if ((frontIndex & 0x80000) != 0) { + frontIndex = (frontIndex & ~0x80000) | 0x8000; + isfrontleaf = true; + } + if ((frontIndex & 0x40000) != 0) { + frontIndex = (frontIndex & ~0x40000) | 0x4000; + isfrontsolid = true; + } + if ((backIndex & 0x80000) != 0) { + backIndex = (backIndex & ~0x80000) | 0x8000; + isbackleaf = true; + } + if ((backIndex & 0x40000) != 0) { + backIndex = (backIndex & ~0x40000) | 0x4000; + isbacksolid = true; + } + } else { + frontIndex = io.readUInt16(); + backIndex = io.readUInt16(); + if ((frontIndex & 0x8000) != 0) { + isfrontleaf = true; + } + if ((frontIndex & 0x4000) != 0) { + isfrontsolid = true; + } + if ((backIndex & 0x8000) != 0) { + isbackleaf = true; + } + if ((backIndex & 0x4000) != 0) { + isbacksolid = true; + } + } + return new BSPNode(planeIndex, frontIndex, backIndex, isfrontleaf, isfrontsolid, isbackleaf, isbacksolid); + } + + public function write(io:BytesWriter, version:Version) { + io.writeUInt16(this.planeIndex); + + if (version.interiorVersion >= 14) { + var frontwrite = this.frontIndex; + var frontwrite = frontIndex; + if (this.isFrontLeaf) { + frontwrite &= ~0x8000; + frontwrite |= 0x80000; + } + if (this.isFrontSolid) { + frontwrite &= ~0x4000; + frontwrite |= 0x40000; + } + + io.writeInt32(frontwrite); + + var backwrite = backIndex; + if (this.isBackLeaf) { + backwrite &= ~0x8000; + backwrite |= 0x80000; + } + if (this.isBackSolid) { + backwrite &= ~0x4000; + backwrite |= 0x40000; + } + + io.writeInt32(backwrite); + } else { + io.writeInt16(this.frontIndex); + io.writeInt16(this.backIndex); + } + } +} diff --git a/src/dif/BSPSolidLeaf.hx b/src/dif/BSPSolidLeaf.hx new file mode 100644 index 00000000..969abd55 --- /dev/null +++ b/src/dif/BSPSolidLeaf.hx @@ -0,0 +1,24 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class BSPSolidLeaf { + public var surfaceStart:Int; + public var surfaceCount:Int; + + public function new(surfaceStart, surfaceCount) { + this.surfaceStart = surfaceStart; + this.surfaceCount = surfaceCount; + } + + public static function read(io:BytesReader) { + return new BSPSolidLeaf(io.readInt32(), io.readInt16()); + } + + public function write(io:BytesWriter) { + io.writeInt32(this.surfaceStart); + io.writeInt16(this.surfaceCount); + } +} diff --git a/src/dif/ConvexHull.hx b/src/dif/ConvexHull.hx new file mode 100644 index 00000000..03c3af83 --- /dev/null +++ b/src/dif/ConvexHull.hx @@ -0,0 +1,86 @@ +package dif; + +import haxe.Int32; +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class ConvexHull { + public var hullStart:Int32; + public var hullCount:Int32; + public var minX:Float; + public var minY:Float; + public var minZ:Float; + public var maxX:Float; + public var maxY:Float; + public var maxZ:Float; + public var surfaceStart:Int32; + public var surfaceCount:Int32; + public var planeStart:Int32; + public var polyListPlaneStart:Int32; + public var polyListPointStart:Int32; + public var polyListStringStart:Int32; + public var staticMesh:Bool; + + public function new() { + this.hullStart = 0; + this.hullCount = 0; + this.minX = 0; + this.minY = 0; + this.minZ = 0; + this.maxX = 0; + this.maxY = 0; + this.maxZ = 0; + this.surfaceStart = 0; + this.surfaceCount = 0; + this.planeStart = 0; + this.polyListPlaneStart = 0; + this.polyListPointStart = 0; + this.polyListStringStart = 0; + this.staticMesh = false; + } + + public static function read(io:BytesReader, version:Version) { + var ret = new ConvexHull(); + ret.hullStart = io.readInt32(); + ret.hullCount = io.readUInt16(); + ret.minX = io.readFloat(); + ret.minY = io.readFloat(); + ret.minZ = io.readFloat(); + ret.maxX = io.readFloat(); + ret.maxY = io.readFloat(); + ret.maxZ = io.readFloat(); + ret.surfaceStart = io.readInt32(); + ret.surfaceCount = io.readUInt16(); + ret.planeStart = io.readInt32(); + ret.polyListPlaneStart = io.readInt32(); + ret.polyListPointStart = io.readInt32(); + ret.polyListStringStart = io.readInt32(); + + if (version.interiorVersion >= 12) { + ret.staticMesh = io.readByte() > 0; + } + return ret; + } + + public function write(io:BytesWriter, version:Version) { + io.writeInt32(this.hullStart); + io.writeUInt16(this.hullCount); + io.writeFloat(this.minX); + io.writeFloat(this.minY); + io.writeFloat(this.minZ); + io.writeFloat(this.maxX); + io.writeFloat(this.maxY); + io.writeFloat(this.maxZ); + io.writeInt32(this.surfaceStart); + io.writeUInt16(this.surfaceCount); + io.writeInt32(this.planeStart); + io.writeInt32(this.polyListPlaneStart); + io.writeInt32(this.polyListPointStart); + io.writeInt32(this.polyListStringStart); + + if (version.interiorVersion >= 12) { + io.writeByte(this.staticMesh ? 1 : 0); + } + } +} diff --git a/src/dif/CoordBin.hx b/src/dif/CoordBin.hx new file mode 100644 index 00000000..9e2fcacd --- /dev/null +++ b/src/dif/CoordBin.hx @@ -0,0 +1,28 @@ +package dif; + +import haxe.Int32; +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class CoordBin { + public var binStart:Int32; + public var binCount:Int32; + + public function new() { + this.binStart = 0; + this.binCount = 0; + } + + public static function read(io:BytesReader) { + var ret = new CoordBin(); + ret.binStart = io.readInt32(); + ret.binCount = io.readInt32(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeInt32(this.binStart); + io.writeInt32(this.binCount); + } +} diff --git a/src/dif/Dif.hx b/src/dif/Dif.hx new file mode 100644 index 00000000..bc7a6a92 --- /dev/null +++ b/src/dif/Dif.hx @@ -0,0 +1,149 @@ +package dif; + +import haxe.io.Bytes; +#if sys +import sys.io.File; +#end +import dif.io.BytesWriter; +import dif.io.BytesReader; +#if js +import js.lib.ArrayBuffer; +#end +#if python +import python.Bytearray; +#end +#if cs +import cs.NativeArray; +#end + +using dif.ReaderExtensions; +using dif.WriterExtensions; + +@:expose +class Dif { + public var difVersion:Int; + public var previewIncluded:Int; + public var interiors:Array; + public var subObjects:Array; + public var triggers:Array; + public var interiorPathfollowers:Array; + public var forceFields:Array; + public var aiSpecialNodes:Array; + public var vehicleCollision:VehicleCollision = null; + public var gameEntities:Array = null; + + public function new() {} + + #if sys + public static function Load(path:String) { + var f = File.read(path); + var bytes = f.readAll(); + var br = new BytesReader(bytes); + return Dif.read(br); + } + + public static function Save(dif:Dif, version:Version, path:String) { + var f = File.write(path); + var bw = new BytesWriter(); + dif.write(bw, version); + f.write(bw.getBuffer()); + } + #end + + public static function LoadFromBuffer(buffer:haxe.io.Bytes) { + var br = new BytesReader(buffer); + return Dif.read(br); + } + + public static function SaveToBuffer(dif:Dif, version:Version) { + var bw = new BytesWriter(); + dif.write(bw, version); + return bw.getBuffer(); + } + + #if js + public static function LoadFromArrayBuffer(buffer:ArrayBuffer) { + var br = new BytesReader(Bytes.ofData(buffer)); + return Dif.read(br); + } + + public static function SaveToArrayBuffer(dif:Dif, version:Version) { + var bw = new BytesWriter(); + dif.write(bw, version); + return bw.getBuffer().getData(); + } + #end + + #if python + public static function LoadFromByteArray(buffer:Bytearray) { + var br = new BytesReader(Bytes.ofData(buffer)); + return Dif.read(br); + } + + public static function SaveToByteArray(dif:Dif, version:Version) { + var bw = new BytesWriter(); + dif.write(bw, version); + return bw.getBuffer().getData(); + } + #end + + #if cs + public static function LoadFromArray(buffer:cs.NativeArray) { + var br = new BytesReader(Bytes.ofData(buffer)); + return Dif.read(br); + } + + public static function SaveToArray(dif:Dif, version:Version) { + var bw = new BytesWriter(); + dif.write(bw, version); + return bw.getBuffer().getData(); + } + #end + + public static function read(io:BytesReader) { + var ret = new Dif(); + var version = new Version(); + version.difVersion = io.readInt32(); + ret.difVersion = version.difVersion; + ret.previewIncluded = io.readByte(); + ret.interiors = io.readArray(io -> Interior.read(io, version)); + ret.subObjects = io.readArray(io -> Interior.read(io, version)); + ret.triggers = io.readArray(Trigger.read); + ret.interiorPathfollowers = io.readArray(InteriorPathFollower.read); + ret.forceFields = io.readArray(ForceField.read); + ret.aiSpecialNodes = io.readArray(AISpecialNode.read); + var readVehicleCollision = io.readInt32(); + if (readVehicleCollision == 1) + ret.vehicleCollision = VehicleCollision.read(io, version); + var readGameEntities = io.readInt32(); + if (readGameEntities == 2) + ret.gameEntities = io.readArray(GameEntity.read); + + return ret; + } + + public function write(io:BytesWriter, version:Version) { + io.writeInt32(this.difVersion); + io.writeByte(this.previewIncluded); + + io.writeArray(this.interiors, (io, p) -> p.write(io, version)); + io.writeArray(this.subObjects, (io, p) -> p.write(io, version)); + io.writeArray(this.triggers, (io, p) -> p.write(io)); + io.writeArray(this.interiorPathfollowers, (io, p) -> p.write(io)); + io.writeArray(this.forceFields, (io, p) -> p.write(io)); + io.writeArray(this.aiSpecialNodes, (io, p) -> p.write(io)); + if (this.vehicleCollision != null) { + io.writeInt32(1); + this.vehicleCollision.write(io, version); + } else { + io.writeInt32(0); + } + if (this.gameEntities != null) { + io.writeInt32(2); + io.writeArray(this.gameEntities, (io, p) -> p.write(io)); + } else { + io.writeInt32(0); + } + io.writeInt32(0); + } +} diff --git a/src/dif/Edge.hx b/src/dif/Edge.hx new file mode 100644 index 00000000..8918dd6f --- /dev/null +++ b/src/dif/Edge.hx @@ -0,0 +1,31 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import haxe.Int32; + +@:expose +class Edge { + public var pointIndex0:Int32; + public var pointIndex1:Int32; + public var surfaceIndex0:Int32; + public var surfaceIndex1:Int32; + + public function new(pointIndex0, pointIndex1, surfaceIndex0, surfaceIndex1) { + this.pointIndex0 = pointIndex0; + this.pointIndex1 = pointIndex1; + this.surfaceIndex0 = surfaceIndex0; + this.surfaceIndex1 = surfaceIndex1; + } + + public static function read(io:BytesReader, version:Version) { + return new Edge(io.readInt32(), io.readInt32(), io.readInt32(), io.readInt32()); + } + + public function write(io:BytesWriter, version:Version) { + io.writeInt32(this.pointIndex0); + io.writeInt32(this.pointIndex1); + io.writeInt32(this.surfaceIndex0); + io.writeInt32(this.surfaceIndex1); + } +} diff --git a/src/dif/Edge2.hx b/src/dif/Edge2.hx new file mode 100644 index 00000000..8a068fa4 --- /dev/null +++ b/src/dif/Edge2.hx @@ -0,0 +1,48 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import haxe.Int32; + +@:expose +class Edge2 { + public var vertex0:Int32; + public var vertex1:Int32; + public var normal0:Int32; + public var normal1:Int32; + public var face0:Int32; + public var face1:Int32; + + public function new() { + this.vertex0 = 0; + this.vertex1 = 0; + this.normal0 = 0; + this.normal1 = 0; + this.face0 = 0; + this.face1 = 0; + } + + public static function read(io:BytesReader, version:Version) { + var ret = new Edge2(); + ret.vertex0 = io.readInt32(); + ret.vertex1 = io.readInt32(); + ret.normal0 = io.readInt32(); + ret.normal1 = io.readInt32(); + if (version.interiorVersion >= 3) { + ret.face0 = io.readInt32(); + ret.face1 = io.readInt32(); + } + return ret; + } + + public function write(io:BytesWriter, version:Version) { + io.writeInt32(this.vertex0); + io.writeInt32(this.vertex1); + io.writeInt32(this.normal0); + io.writeInt32(this.normal1); + if (version.interiorVersion >= 3) { + io.writeInt32(this.face0); + io.writeInt32(this.face1); + } + } +} diff --git a/src/dif/FFSurface.hx b/src/dif/FFSurface.hx new file mode 100644 index 00000000..d43c9e2d --- /dev/null +++ b/src/dif/FFSurface.hx @@ -0,0 +1,39 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class FFSurface { + public var windingStart:Int; + public var windingCount:Int; + public var planeIndex:Int; + public var surfaceFlags:Int; + public var fanMask:Int; + + public function new() { + this.windingStart = 0; + this.windingCount = 0; + this.planeIndex = 0; + this.surfaceFlags = 0; + this.fanMask = 0; + } + + public static function read(io:BytesReader) { + var ret = new FFSurface(); + ret.windingStart = io.readInt32(); + ret.windingCount = io.readByte(); + ret.planeIndex = io.readInt16(); + ret.surfaceFlags = io.readByte(); + ret.fanMask = io.readInt32(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeInt32(this.windingStart); + io.writeByte(this.windingCount); + io.writeInt16(this.planeIndex); + io.writeByte(this.surfaceFlags); + io.writeInt32(this.fanMask); + } +} diff --git a/src/dif/ForceField.hx b/src/dif/ForceField.hx new file mode 100644 index 00000000..52aad780 --- /dev/null +++ b/src/dif/ForceField.hx @@ -0,0 +1,63 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import dif.math.Point3F; +import dif.math.SphereF.Spheref; +import dif.math.Box3F; + +using dif.ReaderExtensions; +using dif.WriterExtensions; + +@:expose +class ForceField { + public var forceFieldFileVersion:Int; + public var name:String; + public var triggers:Array; + public var boundingBox:Box3F; + public var boundingSphere:Spheref; + public var normals:Array; + public var planes:Array; + public var bspNodes:Array; + public var bspSolidLeaves:Array; + public var windings:Array; + public var surfaces:Array; + public var solidLeafSurfaces:Array; + public var color:Array; + + public function new() {} + + public static function read(io:BytesReader) { + var ret = new ForceField(); + ret.forceFieldFileVersion = io.readInt32(); + ret.name = io.readStr(); + ret.triggers = io.readArray(io -> io.readStr()); + ret.boundingBox = Box3F.read(io); + ret.boundingSphere = Spheref.read(io); + ret.normals = io.readArray(Point3F.read); + ret.planes = io.readArray(Plane.read); + ret.bspNodes = io.readArray(io -> BSPNode.read(io, new Version())); + ret.bspSolidLeaves = io.readArray(BSPSolidLeaf.read); + ret.windings = io.readArray(io -> io.readInt32()); + ret.surfaces = io.readArray(FFSurface.read); + ret.solidLeafSurfaces = io.readArray(io -> io.readInt32()); + ret.color = io.readColorF(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeInt32(this.forceFieldFileVersion); + io.writeStr(this.name); + io.writeArray(this.triggers, (io, p) -> io.writeStr(p)); + this.boundingBox.write(io); + this.boundingSphere.write(io); + io.writeArray(this.normals, (io, p) -> p.write(io)); + io.writeArray(this.planes, (io, p) -> p.write(io)); + io.writeArray(this.bspNodes, (io, p) -> p.write(io, new Version())); + io.writeArray(this.bspSolidLeaves, (io, p) -> p.write(io)); + io.writeArray(this.windings, (io, p) -> io.writeInt32(p)); + io.writeArray(this.surfaces, (io, p) -> p.write(io)); + io.writeArray(this.solidLeafSurfaces, (io, p) -> io.writeInt32(p)); + io.writeColorF(this.color); + } +} diff --git a/src/dif/GameEntity.hx b/src/dif/GameEntity.hx new file mode 100644 index 00000000..d99eb708 --- /dev/null +++ b/src/dif/GameEntity.hx @@ -0,0 +1,42 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import dif.math.Point4F; +import haxe.xml.Access; +import haxe.ds.StringMap; +import dif.math.Point3F; + +using dif.ReaderExtensions; +using dif.WriterExtensions; + +@:expose +class GameEntity { + public var datablock:String; + public var gameClass:String; + public var position:Point3F; + public var properties:StringMap; + + public function new() { + this.datablock = ""; + this.gameClass = ""; + this.position = new Point3F(); + this.properties = new StringMap(); + } + + public static function read(io:BytesReader) { + var ret = new GameEntity(); + ret.datablock = io.readStr(); + ret.gameClass = io.readStr(); + ret.position = Point3F.read(io); + ret.properties = io.readDictionary(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeStr(this.datablock); + io.writeStr(this.gameClass); + this.position.write(io); + io.writeDictionary(this.properties); + } +} diff --git a/src/dif/Interior.hx b/src/dif/Interior.hx new file mode 100644 index 00000000..f6fc2e18 --- /dev/null +++ b/src/dif/Interior.hx @@ -0,0 +1,328 @@ +package dif; + +import dif.math.Point4F; +import dif.math.Point3F; +import dif.math.SphereF.Spheref; +import dif.math.Box3F; +import haxe.io.BytesBuffer; +import haxe.io.Bytes; +import haxe.Int32; +import dif.io.BytesWriter; +import dif.io.BytesReader; + +using dif.ReaderExtensions; +using dif.WriterExtensions; + +@:expose +class Interior { + public var detailLevel:Int; + public var minPixels:Int; + public var boundingBox:Box3F; + public var boundingSphere:Spheref; + public var hasAlarmState:Int; + public var numLightStateEntries:Int; + public var normals:Array; + public var planes:Array; + public var points:Array; + public var pointVisibilities:Array; + public var texGenEQs:Array; + public var bspNodes:Array; + public var bspSolidLeaves:Array; + public var materialListVersion:Int; + public var materialList:Array; + public var windings:Array; + public var windingIndices:Array; + public var edges:Array; + public var zones:Array; + public var zoneSurfaces:Array; + public var zoneStaticMeshes:Array; + public var zonePortalList:Array; + public var portals:Array; + public var surfaces:Array; + public var edges2:Array; + public var normals2:Array; + public var normalIndices:Array; + public var normalLMapIndices:Array; + public var alarmLMapIndices:Array; + public var nullSurfaces:Array; + public var lightMaps:Array; + public var solidLeafSurfaces:Array; + public var animatedLights:Array; + public var lightStates:Array; + public var stateDatas:Array; + public var stateDataFlags:Int; + public var stateDataBuffers:Array; + public var nameBuffer:Array; + public var numSubObjects:Int; + public var convexHulls:Array; + public var convexHullEmitStrings:Array; + public var hullIndices:Array; + public var hullPlaneIndices:Array; + public var hullEmitStringIndices:Array; + public var hullSurfaceIndices:Array; + public var polyListPlanes:Array; + public var polyListPoints:Array; + public var polyListStrings:Array; + public var coordBins:Array; + public var coordBinIndices:Array; + public var coordBinMode:Int; + public var baseAmbientColor:Array; + public var alarmAmbientColor:Array; + public var numStaticMeshes:Int; + public var texNormals:Array; + public var texMatrices:Array; + public var texMatIndices:Array; + public var extendedLightMapData:Int; + public var lightMapBorderSize:Int; + + public function new() {} + + public static function read(io:BytesReader, version:Version) { + if (version.interiorType == "?") + version.interiorType = "tgea"; + + version.interiorVersion = io.readInt32(); + var it = new Interior(); + it.detailLevel = io.readInt32(); + it.minPixels = io.readInt32(); + it.boundingBox = Box3F.read(io); + it.boundingSphere = Spheref.read(io); + it.hasAlarmState = io.readByte(); + it.numLightStateEntries = io.readInt32(); + it.normals = io.readArray(Point3F.read); + it.planes = io.readArray(Plane.read); + it.points = io.readArray(Point3F.read); + if (version.interiorVersion == 4) { + it.pointVisibilities = new Array(); + } else { + it.pointVisibilities = io.readArray((io) -> { + return io.readByte(); + }); + } + it.texGenEQs = io.readArray(TexGenEQ.read); + it.bspNodes = io.readArray((io) -> { + return BSPNode.read(io, version); + }); + it.bspSolidLeaves = io.readArray(BSPSolidLeaf.read); + it.materialListVersion = io.readByte(); + it.materialList = io.readArray((io) -> io.readStr()); + it.windings = io.readArrayAs((signed, param) -> param > 0, (io) -> io.readInt32(), io -> io.readInt16()); + it.windingIndices = io.readArray(WindingIndex.read); + if (version.interiorVersion >= 12) { + it.edges = io.readArray(io -> Edge.read(io, version)); + } + it.zones = io.readArray(io -> Zone.read(io, version)); + it.zoneSurfaces = io.readArrayAs((signed, param) -> false, io -> io.readInt16(), io -> io.readInt16()); + if (version.interiorVersion >= 12) { + it.zoneStaticMeshes = io.readArray(io -> io.readInt32()); + } + it.zonePortalList = io.readArrayAs((signed, param) -> false, io -> io.readInt16(), io -> io.readInt16()); + it.portals = io.readArray(Portal.read); + + var pos = io.tell(); + try { + it.surfaces = io.readArray(io -> Surface.read(io, version, it)); + if (version.interiorType == "?") { + version.interiorType = "tge"; + } + } catch (e) { + if (version.interiorType == "tgea") + version.interiorType = "tge"; + io.seek(pos); + try { + it.surfaces = io.readArray(io -> Surface.read(io, version, it)); + } catch (e) {} + } + + if (version.interiorVersion >= 2 && version.interiorVersion <= 5) { + it.edges2 = io.readArray(io -> Edge2.read(io, version)); + if (version.interiorVersion >= 4 && version.interiorVersion <= 5) { + it.normals2 = io.readArray(Point3F.read); + it.normalIndices = io.readArrayAs((alt, param) -> alt && param == 0, io -> io.readUInt16(), io -> io.readByte()); + } + } + + if (version.interiorVersion == 4) { + it.normalLMapIndices = io.readArray(io -> io.readByte()); + it.alarmLMapIndices = new Array(); + } else if (version.interiorVersion >= 13) { + it.normalLMapIndices = io.readArray(io -> io.readInt32()); + it.alarmLMapIndices = io.readArray(io -> io.readInt32()); + } else { + it.normalLMapIndices = io.readArray(io -> io.readByte()); + it.alarmLMapIndices = io.readArray(io -> io.readByte()); + } + + it.nullSurfaces = io.readArray(io -> NullSurface.read(io, version)); + if (version.interiorVersion != 4) { + it.lightMaps = io.readArray(io -> LightMap.read(io, version)); + if (it.lightMaps.length > 0 && version.interiorType == "mbg") { + version.interiorType = "tge"; + } + } else { + it.lightMaps = new Array(); + } + it.solidLeafSurfaces = io.readArrayAs((alt, void) -> alt, io -> io.readInt32(), io -> io.readUInt16()); + it.animatedLights = io.readArray(AnimatedLight.read); + it.lightStates = io.readArray(LightState.read); + + if (version.interiorVersion == 4) { + it.stateDatas = new Array(); + it.stateDataFlags = 0; + it.stateDataBuffers = new Array(); + it.numSubObjects = 0; + } else { + it.stateDatas = io.readArray(StateData.read); + it.stateDataBuffers = io.readArrayFlags(io -> io.readByte()); + it.nameBuffer = io.readArray(io -> io.readByte()); + it.stateDataFlags = 0; + it.numSubObjects = io.readInt32(); + } + + it.convexHulls = io.readArray((io) -> ConvexHull.read(io, version)); + it.convexHullEmitStrings = io.readArray(io -> io.readByte()); + it.hullIndices = io.readArrayAs((alt, that) -> alt, io -> io.readInt32(), io -> io.readUInt16()); + it.hullPlaneIndices = io.readArrayAs((alt, that) -> true, io -> io.readUInt16(), io -> io.readUInt16()); + it.hullEmitStringIndices = io.readArrayAs((alt, that) -> alt, io -> io.readInt32(), io -> io.readUInt16()); + it.hullSurfaceIndices = io.readArrayAs((alt, that) -> alt, io -> io.readInt32(), io -> io.readUInt16()); + it.polyListPlanes = io.readArrayAs((alt, that) -> true, io -> io.readUInt16(), io -> io.readUInt16()); + it.polyListPoints = io.readArrayAs((alt, that) -> alt, io -> io.readInt32(), io -> io.readUInt16()); + it.polyListStrings = io.readArray(io -> io.readByte()); + + it.coordBins = new Array(); + for (i in 0...256) { + it.coordBins.push(CoordBin.read(io)); + } + + it.coordBinIndices = io.readArrayAs((a, b) -> true, io -> io.readUInt16(), io -> io.readUInt16()); + it.coordBinMode = io.readInt32(); + + if (version.interiorVersion == 4) { + it.baseAmbientColor = [0, 0, 0, 255]; + it.alarmAmbientColor = [0, 0, 0, 255]; + it.extendedLightMapData = 0; + it.lightMapBorderSize = 0; + } else { + it.baseAmbientColor = io.readColorF(); + it.alarmAmbientColor = io.readColorF(); + if (version.interiorVersion >= 10) { + it.numStaticMeshes = io.readInt32(); + } + if (version.interiorVersion >= 11) { + it.texNormals = io.readArray(Point3F.read); + it.texMatrices = io.readArray(TexMatrix.read); + it.texMatIndices = io.readArray(io -> io.readInt32()); + } else { + io.readInt32(); + io.readInt32(); + io.readInt32(); + } + it.extendedLightMapData = io.readInt32(); + if (it.extendedLightMapData > 0) { + it.lightMapBorderSize = io.readInt32(); + io.readInt32(); + } + } + + return it; + } + + public function write(io:BytesWriter, version:Version) { + io.writeInt32(this.detailLevel); + io.writeInt32(this.minPixels); + this.boundingBox.write(io); + this.boundingSphere.write(io); + io.writeByte(this.hasAlarmState); + io.writeInt32(this.numLightStateEntries); + io.writeArray(this.normals, (io, p) -> p.write(io)); + io.writeArray(this.planes, (io, p) -> p.write(io)); + io.writeArray(this.points, (io, p) -> p.write(io)); + if (version.interiorVersion != 4) { + io.writeArray(this.pointVisibilities, (io, p) -> io.writeByte(p)); + } + io.writeArray(this.texGenEQs, (io, p) -> p.write(io)); + io.writeArray(this.bspNodes, (io, p) -> p.write(io, version)); + io.writeArray(this.bspSolidLeaves, (io, p) -> p.write(io)); + io.writeByte(this.materialListVersion); + io.writeArray(this.materialList, (io, p) -> io.writeStr(p)); + io.writeArray(this.windings, (io, p) -> io.writeInt32(p)); + io.writeArray(this.windingIndices, (io, p) -> p.write(io)); + if (version.interiorVersion >= 12) + io.writeArray(this.edges, (io, p) -> p.write(io, version)); + io.writeArray(this.zones, (io, p) -> p.write(io, version)); + io.writeArray(this.zoneSurfaces, (io, p) -> io.writeUInt16(p)); + if (version.interiorVersion >= 12) + io.writeArray(this.zoneStaticMeshes, (io, p) -> io.writeInt32(p)); + io.writeArray(this.zonePortalList, (io, p) -> io.writeUInt16(p)); + io.writeArray(this.portals, (io, p) -> p.write(io)); + io.writeArray(this.surfaces, (io, p) -> p.write(io, version)); + if (version.interiorVersion >= 2 && version.interiorVersion <= 5) { + io.writeArray(this.edges2, (io, p) -> p.write(io, version)); + if (version.interiorVersion >= 4 && version.interiorVersion <= 5) { + io.writeArray(this.normals2, (io, p) -> p.write(io)); + io.writeArray(this.normalIndices, (io, p) -> io.writeUInt16(p)); + } + } + if (version.interiorVersion == 4) { + io.writeArray(this.normalLMapIndices, (io, p) -> io.writeByte(p)); + } else if (version.interiorVersion >= 13) { + io.writeArray(this.normalLMapIndices, (io, p) -> io.writeInt32(p)); + io.writeArray(this.normalLMapIndices, (io, p) -> io.writeInt32(p)); + } else { + io.writeArray(this.normalLMapIndices, (io, p) -> io.writeByte(p)); + io.writeArray(this.normalLMapIndices, (io, p) -> io.writeByte(p)); + } + + io.writeArray(this.nullSurfaces, (io, p) -> p.write(io, version)); + if (version.interiorVersion != 4) { + io.writeArray(this.lightMaps, (io, p) -> p.writeLightMap(io, version)); + } + io.writeArray(this.solidLeafSurfaces, (io, p) -> io.writeInt32(p)); + io.writeArray(this.animatedLights, (io, p) -> p.write(io)); + io.writeArray(this.lightStates, (io, p) -> p.write(io)); + if (version.interiorVersion != 4) { + io.writeArray(this.stateDatas, (io, p) -> p.write(io)); + io.writeArrayFlags(this.stateDataBuffers, this.stateDataFlags, (io, p) -> io.writeByte(p)); + io.writeArray(this.nameBuffer, (io, p) -> io.writeByte(p)); + io.writeInt32(this.numSubObjects); + } + + io.writeArray(this.convexHulls, (io, p) -> p.write(io, version)); + io.writeArray(this.convexHullEmitStrings, (io, p) -> io.writeByte(p)); + io.writeArray(this.hullIndices, (io, p) -> io.writeInt32(p)); + io.writeArray(this.hullPlaneIndices, (io, p) -> io.writeInt16(p)); + io.writeArray(this.hullEmitStringIndices, (io, p) -> io.writeInt32(p)); + io.writeArray(this.hullSurfaceIndices, (io, p) -> io.writeInt32(p)); + io.writeArray(this.polyListPlanes, (io, p) -> io.writeInt16(p)); + io.writeArray(this.polyListPoints, (io, p) -> io.writeInt32(p)); + io.writeArray(this.polyListStrings, (io, p) -> io.writeByte(p)); + + for (i in 0...256) { + this.coordBins[i].write(io); + } + + io.writeArray(this.coordBinIndices, (io, p) -> io.writeInt16(p)); + io.writeInt32(this.coordBinMode); + + if (version.interiorVersion != 4) { + io.writeColorF(this.baseAmbientColor); + io.writeColorF(this.alarmAmbientColor); + if (version.interiorVersion >= 10) + io.writeInt32(this.numStaticMeshes); + if (version.interiorVersion >= 11) { + io.writeArray(this.texNormals, (io, p) -> p.write(io)); + io.writeArray(this.texNormals, (io, p) -> p.write(io)); + io.writeArray(this.texMatIndices, (io, p) -> io.writeInt32(p)); + } else { + io.writeInt32(0); + io.writeInt32(0); + io.writeInt32(0); + } + io.writeInt32(this.extendedLightMapData); + if (this.extendedLightMapData > 0) { + io.writeInt32(this.lightMapBorderSize); + io.writeInt32(0); + } + } + } +} diff --git a/src/dif/InteriorPathFollower.hx b/src/dif/InteriorPathFollower.hx new file mode 100644 index 00000000..0cf6cd48 --- /dev/null +++ b/src/dif/InteriorPathFollower.hx @@ -0,0 +1,57 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import haxe.xml.Access; +import haxe.ds.StringMap; +import dif.math.Point3F; + +using dif.ReaderExtensions; +using dif.WriterExtensions; + +@:expose +class InteriorPathFollower { + public var name:String; + public var datablock:String; + public var interiorResIndex:Int; + public var offset:Point3F; + public var properties:StringMap; + public var triggerId:Array; + public var wayPoint:Array; + public var totalMS:Int; + + public function new() { + this.name = ""; + this.datablock = ""; + this.interiorResIndex = 0; + this.offset = new Point3F(); + this.properties = new StringMap(); + this.triggerId = new Array(); + this.wayPoint = new Array(); + this.totalMS = 0; + } + + public static function read(io:BytesReader) { + var ret = new InteriorPathFollower(); + ret.name = io.readStr(); + ret.datablock = io.readStr(); + ret.interiorResIndex = io.readInt32(); + ret.offset = Point3F.read(io); + ret.properties = io.readDictionary(); + ret.triggerId = io.readArray(io -> io.readInt32()); + ret.wayPoint = io.readArray(WayPoint.read); + ret.totalMS = io.readInt32(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeStr(this.name); + io.writeStr(this.datablock); + io.writeInt32(this.interiorResIndex); + this.offset.write(io); + io.writeDictionary(this.properties); + io.writeArray(this.triggerId, (io, p) -> io.writeInt32(p)); + io.writeArray(this.wayPoint, (io, p) -> p.write(io)); + io.writeInt32(this.totalMS); + } +} diff --git a/src/dif/LightMap.hx b/src/dif/LightMap.hx new file mode 100644 index 00000000..c9b6bda5 --- /dev/null +++ b/src/dif/LightMap.hx @@ -0,0 +1,41 @@ +package dif; + +import haxe.io.Bytes; +import dif.io.BytesWriter; +import dif.io.BytesReader; +import haxe.Int32; + +using dif.ReaderExtensions; +using dif.WriterExtensions; + +@:expose +class LightMap { + public var lightmap:Array; + public var lightdirmap:Array; + public var keepLightMap:Int; + + public function new() { + this.lightmap = new Array(); + this.lightdirmap = new Array(); + this.keepLightMap = 0; + } + + public static function read(io:BytesReader, version:Version) { + var ret = new LightMap(); + ret.lightmap = io.readPNG(); + + if (version.interiorType != "mbg" && version.interiorType != "tge") { + ret.lightdirmap = io.readPNG(); + } + ret.keepLightMap = io.readByte(); + return ret; + } + + public function writeLightMap(io:BytesWriter, version:Version) { + io.writePNG(this.lightmap); + if (version.interiorType != "mbg" && version.interiorType != "tge") { + io.writePNG(this.lightdirmap); + } + io.writeByte(this.keepLightMap); + } +} diff --git a/src/dif/LightState.hx b/src/dif/LightState.hx new file mode 100644 index 00000000..2a5908cb --- /dev/null +++ b/src/dif/LightState.hx @@ -0,0 +1,37 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import haxe.Int32; + +@:expose +class LightState { + public var red:Int; + public var green:Int; + public var blue:Int; + public var activeTime:Int32; + public var dataIndex:Int32; + public var dataCount:Int; + + public function new(red:Int, green:Int, blue:Int, activeTime:Int32, dataIndex:Int32, dataCount:Int) { + this.red = red; + this.green = green; + this.blue = blue; + this.activeTime = activeTime; + this.dataIndex = dataIndex; + this.dataCount = dataCount; + } + + public static function read(io:BytesReader) { + return new LightState(io.readByte(), io.readByte(), io.readByte(), io.readInt32(), io.readInt32(), io.readInt16()); + } + + public function write(io:BytesWriter) { + io.writeByte(this.red); + io.writeByte(this.green); + io.writeByte(this.blue); + io.writeInt32(this.activeTime); + io.writeInt32(this.dataIndex); + io.writeInt16(this.dataCount); + } +} diff --git a/src/dif/NullSurface.hx b/src/dif/NullSurface.hx new file mode 100644 index 00000000..bd4e7a06 --- /dev/null +++ b/src/dif/NullSurface.hx @@ -0,0 +1,43 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class NullSurface { + public var windingStart:Int; + public var planeIndex:Int; + public var surfaceFlags:Int; + public var windingCount:Int; + + public function new() { + this.windingStart = 0; + this.planeIndex = 0; + this.surfaceFlags = 0; + this.windingCount = 0; + } + + public static function read(io:BytesReader, version:Version) { + var ret = new NullSurface(); + ret.windingStart = io.readInt32(); + ret.planeIndex = io.readUInt16(); + ret.surfaceFlags = io.readByte(); + if (version.interiorVersion >= 13) { + ret.windingCount = io.readInt32(); + } else { + ret.windingCount = io.readByte(); + } + return ret; + } + + public function write(io:BytesWriter, version:Version) { + io.writeInt32(this.windingStart); + io.writeUInt16(this.planeIndex); + io.writeByte(this.surfaceFlags); + if (version.interiorVersion >= 13) { + io.writeInt32(this.windingCount); + } else { + io.writeByte(this.windingCount); + } + } +} diff --git a/src/dif/Plane.hx b/src/dif/Plane.hx new file mode 100644 index 00000000..33501c89 --- /dev/null +++ b/src/dif/Plane.hx @@ -0,0 +1,24 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class Plane { + public var normalIndex:Int; + public var planeDistance:Float; + + public function new(normalIndex, planeDistance) { + this.normalIndex = normalIndex; + this.planeDistance = planeDistance; + } + + public static function read(io:BytesReader) { + return new Plane(io.readInt16(), io.readFloat()); + } + + public function write(io:BytesWriter) { + io.writeInt16(this.normalIndex); + io.writeFloat(this.planeDistance); + } +} diff --git a/src/dif/Polyhedron.hx b/src/dif/Polyhedron.hx new file mode 100644 index 00000000..92c7f998 --- /dev/null +++ b/src/dif/Polyhedron.hx @@ -0,0 +1,36 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import dif.math.PlaneF; +import dif.math.Point3F; + +using dif.ReaderExtensions; +using dif.WriterExtensions; + +@:expose +class Polyhedron { + public var pointList:Array; + public var planeList:Array; + public var edgeList:Array; + + public function new() { + this.pointList = new Array(); + this.planeList = new Array(); + this.edgeList = new Array(); + } + + public static function read(io:BytesReader) { + var ret = new Polyhedron(); + ret.pointList = io.readArray(Point3F.read); + ret.planeList = io.readArray(PlaneF.read); + ret.edgeList = io.readArray(PolyhedronEdge.read); + return ret; + } + + public function write(io:BytesWriter) { + io.writeArray(this.pointList, (io, p) -> p.write(io)); + io.writeArray(this.planeList, (io, p) -> p.write(io)); + io.writeArray(this.edgeList, (io, p) -> p.write(io)); + } +} diff --git a/src/dif/PolyhedronEdge.hx b/src/dif/PolyhedronEdge.hx new file mode 100644 index 00000000..5cee287f --- /dev/null +++ b/src/dif/PolyhedronEdge.hx @@ -0,0 +1,30 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class PolyhedronEdge { + public var pointIndex0:Int; + public var pointIndex1:Int; + public var faceIndex0:Int; + public var faceIndex1:Int; + + public function new(faceIndex0, faceIndex1, pointIndex0, pointIndex1) { + this.pointIndex0 = pointIndex0; + this.pointIndex1 = pointIndex1; + this.faceIndex0 = faceIndex0; + this.faceIndex1 = faceIndex1; + } + + public static function read(io:BytesReader) { + return new PolyhedronEdge(io.readInt32(), io.readInt32(), io.readInt32(), io.readInt32()); + } + + public function write(io:BytesWriter) { + io.writeInt32(this.faceIndex0); + io.writeInt32(this.faceIndex1); + io.writeInt32(this.pointIndex0); + io.writeInt32(this.pointIndex1); + } +} diff --git a/src/dif/Portal.hx b/src/dif/Portal.hx new file mode 100644 index 00000000..b8a199a5 --- /dev/null +++ b/src/dif/Portal.hx @@ -0,0 +1,33 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class Portal { + public var planeIndex:Int; + public var triFanCount:Int; + public var triFanStart:Int; + public var zoneFront:Int; + public var zoneBack:Int; + + public function new(planeIndex, triFanCount, triFanStart, zoneFront, zoneBack) { + this.planeIndex = planeIndex; + this.triFanCount = triFanCount; + this.triFanStart = triFanStart; + this.zoneFront = zoneFront; + this.zoneBack = zoneBack; + } + + public static function read(io:BytesReader) { + return new Portal(io.readUInt16(), io.readUInt16(), io.readInt32(), io.readUInt16(), io.readUInt16()); + } + + public function write(io:BytesWriter) { + io.writeUInt16(this.planeIndex); + io.writeUInt16(this.triFanCount); + io.writeInt32(this.triFanStart); + io.writeUInt16(this.zoneFront); + io.writeUInt16(this.zoneBack); + } +} diff --git a/src/dif/ReaderExtensions.hx b/src/dif/ReaderExtensions.hx new file mode 100644 index 00000000..6d85cd94 --- /dev/null +++ b/src/dif/ReaderExtensions.hx @@ -0,0 +1,91 @@ +package dif; + +import dif.io.BytesReader; +import haxe.ds.StringMap; + +class ReaderExtensions { + public static function readDictionary(io:BytesReader) { + var len = io.readInt32(); + var dict = new StringMap(); + + for (i in 0...len) { + var name = io.readStr(); + var value = io.readStr(); + dict.set(name, value); + } + + return dict; + } + + public static function readArray(io:BytesReader, readMethod:BytesReader->V):Array { + var len = io.readInt32(); + var arr = new Array(); + + for (i in 0...len) { + arr.push(readMethod(io)); + } + + return arr; + } + + public static function readArrayAs(io:BytesReader, test:(Bool, Int) -> Bool, failMethod:BytesReader->V, passMethod:BytesReader->V) { + var length = io.readInt32(); + var signed = false; + var param = 0; + + if ((length & 0x80000000) > 0) { + length ^= 0x80000000; + signed = true; + param = io.readInt32(); + } + + var array = new Array(); + for (i in 0...length) { + if (test(signed, param)) { + array.push(passMethod(io)); + } else { + array.push(failMethod(io)); + } + } + + return array; + } + + public static function readArrayFlags(io:BytesReader, readMethod:BytesReader->V) { + var length = io.readInt32(); + var flags = io.readInt32(); + var array = new Array(); + for (i in 0...length) { + array.push(readMethod(io)); + } + return array; + }; + + public static function readPNG(io:BytesReader) { + var footer = [0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82]; + + // I can't parse these, so I just read em all + var data = []; + while (true) { + data.push(io.readByte()); + + if (data.length >= 8) { + var match = true; + for (i in 0...8) { + if (data[i + (data.length - 8)] != footer[i]) { + match = false; + break; + } + } + if (match) + break; + } + } + + return data; + }; + + public static function readColorF(io:BytesReader) { + return [io.readByte(), io.readByte(), io.readByte(), io.readByte()]; + } +} diff --git a/src/dif/StateData.hx b/src/dif/StateData.hx new file mode 100644 index 00000000..cdbc54bd --- /dev/null +++ b/src/dif/StateData.hx @@ -0,0 +1,28 @@ +package dif; + +import haxe.Int32; +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class StateData { + public var surfaceIndex:Int32; + public var mapIndex:Int32; + public var lightStateIndex:Int32; + + public function new(surfaceIndex:Int32, mapIndex:Int32, lightStateIndex:Int32) { + this.surfaceIndex = surfaceIndex; + this.mapIndex = mapIndex; + this.lightStateIndex = lightStateIndex; + } + + public static function read(io:BytesReader) { + return new StateData(io.readInt32(), io.readInt32(), io.readInt32()); + } + + public function write(io:BytesWriter) { + io.writeInt32(this.surfaceIndex); + io.writeInt32(this.mapIndex); + io.writeInt32(this.lightStateIndex); + } +} diff --git a/src/dif/Surface.hx b/src/dif/Surface.hx new file mode 100644 index 00000000..e695d1ae --- /dev/null +++ b/src/dif/Surface.hx @@ -0,0 +1,149 @@ +package dif; + +import haxe.Exception; +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class Surface { + public var windingStart:Int; + public var windingCount:Int; + public var planeIndex:Int; + public var textureIndex:Int; + public var texGenIndex:Int; + public var surfaceFlags:Int; + public var fanMask:Int; + public var lightMapFinalWord:Int; + public var lightMapTexGenXD:Float; + public var lightMapTexGenYD:Float; + public var lightCount:Int; + public var lightStateInfoStart:Int; + public var mapOffsetX:Int; + public var mapOffsetY:Int; + public var mapSizeX:Int; + public var mapSizeY:Int; + public var brushId:Int; + + public function new() { + this.windingStart = 0; + this.windingCount = 0; + this.planeIndex = 0; + this.textureIndex = 0; + this.texGenIndex = 0; + this.surfaceFlags = 0; + this.fanMask = 0; + this.lightMapFinalWord = 0; + this.lightMapTexGenXD = 0; + this.lightMapTexGenYD = 0; + this.lightCount = 0; + this.lightStateInfoStart = 0; + this.mapOffsetX = 0; + this.mapOffsetY = 0; + this.mapSizeX = 0; + this.mapSizeY = 0; + this.brushId = 0; + } + + public static function read(io:BytesReader, version:Version, interior:Interior) { + var ret = new Surface(); + ret.windingStart = io.readInt32(); + + if (interior.windings.length <= ret.windingStart) + throw new Exception("DIF Type Error"); + + if (version.interiorVersion >= 13) + ret.windingCount = io.readInt32(); + else + ret.windingCount = io.readByte(); + + if (ret.windingStart + ret.windingCount > interior.windings.length) { + throw new Exception("DIF Type Error"); + } + + ret.planeIndex = io.readInt16(); + var flipped = (ret.planeIndex >> 15 != 0); + var planeIndexTemp = ret.planeIndex & ~0x8000; + + if ((planeIndexTemp & ~0x8000) >= interior.planes.length) { + throw new Exception("DIF Type Error"); + } + + ret.textureIndex = io.readInt16(); + + if (ret.textureIndex >= interior.materialList.length) { + throw new Exception("DIF Type Error"); + } + + ret.texGenIndex = io.readInt32(); + + if (ret.texGenIndex >= interior.texGenEQs.length) { + throw new Exception("DIF Type Error"); + } + + ret.surfaceFlags = io.readByte(); + ret.fanMask = io.readInt32(); + ret.lightMapFinalWord = io.readInt16(); + ret.lightMapTexGenXD = io.readFloat(); + ret.lightMapTexGenYD = io.readFloat(); + ret.lightCount = io.readInt16(); + ret.lightStateInfoStart = io.readInt32(); + + if (version.interiorVersion >= 13) { + ret.mapOffsetX = io.readInt32(); + ret.mapOffsetY = io.readInt32(); + ret.mapSizeX = io.readInt32(); + ret.mapSizeY = io.readInt32(); + } else { + ret.mapOffsetX = io.readByte(); + ret.mapOffsetY = io.readByte(); + ret.mapSizeX = io.readByte(); + ret.mapSizeY = io.readByte(); + } + + if (version.interiorType != "tge" && version.interiorType != "mbg") { + io.readByte(); + if (version.interiorVersion >= 2 && version.interiorVersion <= 5) + ret.brushId = io.readInt32(); + } + + return ret; + } + + public function write(io:BytesWriter, version:Version) { + io.writeInt32(this.windingStart); + + if (version.interiorVersion >= 13) + io.writeInt32(this.windingCount); + else + io.writeByte(this.windingCount); + + io.writeInt16(this.planeIndex); + io.writeInt16(this.textureIndex); + io.writeInt32(this.texGenIndex); + io.writeByte(this.surfaceFlags); + io.writeInt32(this.fanMask); + io.writeInt16(this.lightMapFinalWord); + io.writeFloat(this.lightMapTexGenXD); + io.writeFloat(this.lightMapTexGenYD); + io.writeInt16(this.lightCount); + io.writeInt32(this.lightStateInfoStart); + + if (version.interiorVersion >= 13) { + io.writeInt32(this.mapOffsetX); + io.writeInt32(this.mapOffsetY); + io.writeInt32(this.mapSizeX); + io.writeInt32(this.mapSizeY); + } else { + io.writeByte(this.mapOffsetX); + io.writeByte(this.mapOffsetY); + io.writeByte(this.mapSizeX); + io.writeByte(this.mapSizeY); + } + + if (version.interiorType != "tge" && version.interiorType != "mbg") { + io.writeByte(0); + if (version.interiorVersion >= 2 && version.interiorVersion <= 5) + io.writeInt32(this.brushId); + } + } +} diff --git a/src/dif/TexGenEQ.hx b/src/dif/TexGenEQ.hx new file mode 100644 index 00000000..546258ca --- /dev/null +++ b/src/dif/TexGenEQ.hx @@ -0,0 +1,28 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import dif.math.PlaneF; + +@:expose +class TexGenEQ { + public var planeX:PlaneF; + public var planeY:PlaneF; + + public function new() { + planeX = new PlaneF(); + planeY = new PlaneF(); + } + + public static function read(io:BytesReader) { + var ret = new TexGenEQ(); + ret.planeX = PlaneF.read(io); + ret.planeY = PlaneF.read(io); + return ret; + } + + public function write(io:BytesWriter) { + this.planeX.write(io); + this.planeY.write(io); + } +} diff --git a/src/dif/TexMatrix.hx b/src/dif/TexMatrix.hx new file mode 100644 index 00000000..1a1cd45d --- /dev/null +++ b/src/dif/TexMatrix.hx @@ -0,0 +1,32 @@ +package dif; + +import haxe.Int32; +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class TexMatrix { + public var t:Int32; + public var n:Int32; + public var b:Int32; + + public function new() { + this.t = 0; + this.n = 0; + this.b = 0; + } + + public static function read(io:BytesReader) { + var ret = new TexMatrix(); + ret.t = io.readInt32(); + ret.n = io.readInt32(); + ret.b = io.readInt32(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeInt32(this.t); + io.writeInt32(this.n); + io.writeInt32(this.b); + } +} diff --git a/src/dif/Trigger.hx b/src/dif/Trigger.hx new file mode 100644 index 00000000..dabdb641 --- /dev/null +++ b/src/dif/Trigger.hx @@ -0,0 +1,44 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import dif.math.Point3F; +import haxe.ds.StringMap; + +using dif.ReaderExtensions; +using dif.WriterExtensions; + +@:expose +class Trigger { + public var name:String; + public var datablock:String; + public var properties:StringMap; + public var polyhedron:Polyhedron; + public var offset:Point3F; + + public function new() { + this.name = ""; + this.datablock = ""; + this.offset = new Point3F(); + this.properties = new StringMap(); + this.polyhedron = new Polyhedron(); + } + + public static function read(io:BytesReader) { + var ret = new Trigger(); + ret.name = io.readStr(); + ret.datablock = io.readStr(); + ret.properties = io.readDictionary(); + ret.polyhedron = Polyhedron.read(io); + ret.offset = Point3F.read(io); + return ret; + } + + public function write(io:BytesWriter) { + io.writeStr(this.name); + io.writeStr(this.datablock); + io.writeDictionary(this.properties); + this.polyhedron.write(io); + this.offset.write(io); + } +} diff --git a/src/dif/VehicleCollision.hx b/src/dif/VehicleCollision.hx new file mode 100644 index 00000000..a5cda2fe --- /dev/null +++ b/src/dif/VehicleCollision.hx @@ -0,0 +1,67 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import dif.math.Point3F; + +using dif.ReaderExtensions; +using dif.WriterExtensions; + +@:expose +class VehicleCollision { + public var vehicleCollisionFileVersion:Int; + public var convexHulls:Array; + public var convexHullEmitStrings:Array; + public var hullIndices:Array; + public var hullPlaneIndices:Array; + public var hullEmitStringIndices:Array; + public var hullSurfaceIndices:Array; + public var polyListPlanes:Array; + public var polyListPoints:Array; + public var polyListStrings:Array; + public var nullSurfaces:Array; + public var points:Array; + public var planes:Array; + public var windings:Array; + public var windingIndices:Array; + + public function new() {} + + public static function read(io:BytesReader, version:Version) { + var ret = new VehicleCollision(); + ret.vehicleCollisionFileVersion = io.readInt32(); + ret.convexHulls = io.readArray((io) -> ConvexHull.read(io, version)); + ret.convexHullEmitStrings = io.readArray(io -> io.readByte()); + ret.hullIndices = io.readArray(io -> io.readInt32()); + ret.hullPlaneIndices = io.readArray(io -> io.readInt16()); + ret.hullEmitStringIndices = io.readArray(io -> io.readInt32()); + ret.hullSurfaceIndices = io.readArray(io -> io.readInt32()); + ret.polyListPlanes = io.readArray(io -> io.readInt16()); + ret.polyListPoints = io.readArray(io -> io.readInt32()); + ret.polyListStrings = io.readArray(io -> io.readByte()); + ret.nullSurfaces = io.readArray(io -> NullSurface.read(io, new Version())); + ret.points = io.readArray(Point3F.read); + ret.planes = io.readArray(Plane.read); + ret.windings = io.readArray(io -> io.readInt32()); + ret.windingIndices = io.readArray(WindingIndex.read); + return ret; + } + + public function write(io:BytesWriter, version:Version) { + io.writeInt32(this.vehicleCollisionFileVersion); + io.writeArray(this.convexHulls, (io, p) -> p.write(io, version)); + io.writeArray(this.convexHullEmitStrings, (io, p) -> io.writeByte(p)); + io.writeArray(this.hullIndices, (io, p) -> io.writeInt32(p)); + io.writeArray(this.hullPlaneIndices, (io, p) -> io.writeInt16(p)); + io.writeArray(this.hullEmitStringIndices, (io, p) -> io.writeInt32(p)); + io.writeArray(this.hullSurfaceIndices, (io, p) -> io.writeInt32(p)); + io.writeArray(this.polyListPlanes, (io, p) -> io.writeInt16(p)); + io.writeArray(this.polyListPoints, (io, p) -> io.writeInt32(p)); + io.writeArray(this.polyListStrings, (io, p) -> io.writeByte(p)); + io.writeArray(this.nullSurfaces, (io, p) -> p.write(io, new Version())); + io.writeArray(this.points, (io, p) -> p.write(io)); + io.writeArray(this.planes, (io, p) -> p.write(io)); + io.writeArray(this.windings, (io, p) -> io.writeInt32(p)); + io.writeArray(this.windingIndices, (io, p) -> p.write(io)); + } +} diff --git a/src/dif/Version.hx b/src/dif/Version.hx new file mode 100644 index 00000000..a1188e44 --- /dev/null +++ b/src/dif/Version.hx @@ -0,0 +1,14 @@ +package dif; + +@:expose +class Version { + public var difVersion:Int; + public var interiorVersion:Int; + public var interiorType:String; + + public function new() { + this.difVersion = 44; + this.interiorVersion = 0; + this.interiorType = "?"; + } +} diff --git a/src/dif/WayPoint.hx b/src/dif/WayPoint.hx new file mode 100644 index 00000000..0262ad43 --- /dev/null +++ b/src/dif/WayPoint.hx @@ -0,0 +1,32 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; +import dif.math.QuatF; +import dif.math.Point3F; + +@:expose +class WayPoint { + public var position:Point3F; + public var rotation:QuatF; + public var msToNext:Int; + public var smoothingType:Int; + + public function new(position, rotation, msToNext, smoothingType) { + this.position = position; + this.rotation = rotation; + this.msToNext = msToNext; + this.smoothingType = smoothingType; + } + + public static function read(io:BytesReader) { + return new WayPoint(Point3F.read(io), QuatF.read(io), io.readInt32(), io.readInt32()); + }; + + public function write(io:BytesWriter) { + this.position.write(io); + this.rotation.write(io); + io.writeInt32(this.msToNext); + io.writeInt32(this.smoothingType); + } +} diff --git a/src/dif/WindingIndex.hx b/src/dif/WindingIndex.hx new file mode 100644 index 00000000..845f61af --- /dev/null +++ b/src/dif/WindingIndex.hx @@ -0,0 +1,24 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class WindingIndex { + public var windingStart:Int; + public var windingCount:Int; + + public function new(windingStart, windingCount) { + this.windingStart = windingStart; + this.windingCount = windingCount; + } + + public static function read(io:BytesReader) { + return new WindingIndex(io.readInt32(), io.readInt32()); + } + + public function write(io:BytesWriter) { + io.writeInt32(this.windingStart); + io.writeInt32(this.windingCount); + } +} diff --git a/src/dif/WriterExtensions.hx b/src/dif/WriterExtensions.hx new file mode 100644 index 00000000..202e8481 --- /dev/null +++ b/src/dif/WriterExtensions.hx @@ -0,0 +1,49 @@ +package dif; + +import haxe.io.Bytes; +import haxe.ds.StringMap; +import dif.io.BytesWriter; +import dif.io.BytesReader; + +class WriterExtensions { + public static function writeDictionary(io:BytesWriter, dict:StringMap) { + var len = 0; + for (key in dict.keys()) + len++; + + for (kvp in dict.keyValueIterator()) { + io.writeStr(kvp.key); + io.writeStr(kvp.value); + } + } + + public static function writeArray(io:BytesWriter, arr:Array, writeMethod:(BytesWriter, V) -> Void) { + io.writeInt32(arr.length); + for (i in 0...arr.length) { + writeMethod(io, arr[i]); + } + + return arr; + } + + public static function writeArrayFlags(io:BytesWriter, arr:Array, flags:Int, writeMethod:(BytesWriter, V) -> Void) { + io.writeInt32(arr.length); + io.writeInt32(flags); + for (i in 0...arr.length) { + writeMethod(io, arr[i]); + } + }; + + public static function writePNG(io:BytesWriter, arr:Array) { + for (i in 0...arr.length) { + io.writeByte(arr[i]); + } + }; + + public static function writeColorF(io:BytesWriter, color:Array) { + io.writeByte(color[0]); + io.writeByte(color[1]); + io.writeByte(color[2]); + io.writeByte(color[3]); + } +} diff --git a/src/dif/Zone.hx b/src/dif/Zone.hx new file mode 100644 index 00000000..4ad7dfb4 --- /dev/null +++ b/src/dif/Zone.hx @@ -0,0 +1,47 @@ +package dif; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class Zone { + public var portalStart:Int; + public var portalCount:Int; + public var surfaceStart:Int; + public var surfaceCount:Int; + public var staticMeshStart:Int; + public var staticMeshCount:Int; + + public function new() { + this.portalStart = 0; + this.portalCount = 0; + this.surfaceStart = 0; + this.surfaceCount = 0; + this.staticMeshStart = 0; + this.staticMeshCount = 0; + } + + public static function read(io:BytesReader, version:Version) { + var ret = new Zone(); + ret.portalStart = io.readUInt16(); + ret.portalCount = io.readUInt16(); + ret.surfaceStart = io.readInt32(); + ret.surfaceCount = io.readInt32(); + if (version.interiorVersion >= 12) { + ret.staticMeshStart = io.readInt32(); + ret.staticMeshCount = io.readInt32(); + } + return ret; + } + + public function write(io:BytesWriter, version:Version) { + io.writeInt16(this.portalStart); + io.writeInt16(this.portalCount); + io.writeInt32(this.surfaceStart); + io.writeInt32(this.surfaceCount); + if (version.interiorVersion >= 12) { + io.writeInt32(this.staticMeshStart); + io.writeInt32(this.staticMeshCount); + } + } +} diff --git a/src/dif/io/BytesReader.hx b/src/dif/io/BytesReader.hx new file mode 100644 index 00000000..e2ca0b45 --- /dev/null +++ b/src/dif/io/BytesReader.hx @@ -0,0 +1,60 @@ +package dif.io; + +import haxe.io.Bytes; + +class BytesReader { + var bytes:Bytes; + var position:Int; + + public function new(bytes:Bytes) { + this.bytes = bytes; + this.position = 0; + } + + public function readInt32() { + var b = bytes.getInt32(position); + position += 4; + return b; + } + + public function readInt16() { + var b = bytes.getUInt16(position); + position += 2; + return b; + } + + public function readUInt16() { + var b = bytes.getUInt16(position); + position += 2; + return b; + } + + public function readByte() { + var b = bytes.get(position); + position += 1; + return b; + } + + public function readStr() { + var len = this.readByte(); + var str = ""; + for (i in 0...len) { + str += String.fromCharCode(this.readByte()); + } + return str; + } + + public function readFloat() { + var b = bytes.getFloat(position); + position += 4; + return b; + } + + public function tell() { + return position; + } + + public function seek(pos) { + position = pos; + } +} diff --git a/src/dif/io/BytesWriter.hx b/src/dif/io/BytesWriter.hx new file mode 100644 index 00000000..f6dbd77a --- /dev/null +++ b/src/dif/io/BytesWriter.hx @@ -0,0 +1,50 @@ +package dif.io; + +import haxe.io.BytesData; +import haxe.Int32; +import haxe.io.BytesBuffer; +import haxe.io.Bytes; + +class BytesWriter { + var bytes:BytesBuffer; + + public function new() { + this.bytes = new BytesBuffer(); + } + + public function writeInt32(int:Int) { + this.bytes.addInt32(int); + } + + public function writeUInt16(int:Int) { + var h = int >> 8; + var l = int & 0xFF; + this.bytes.addByte(h); + this.bytes.addByte(l); + } + + public function writeInt16(int:Int) { + var h = int >> 8; + var l = int & 0xFF; + this.bytes.addByte(h); + this.bytes.addByte(l); + } + + public function writeByte(int:Int) { + this.bytes.addByte(int); + } + + public function writeStr(str:String) { + this.bytes.addByte(str.length); + for (c in 0...str.length) + this.bytes.addByte(str.charCodeAt(c)); + } + + public function writeFloat(f:Float) { + this.bytes.addFloat(f); + } + + public function getBuffer() { + return bytes.getBytes(); + } +} diff --git a/src/dif/math/Box3F.hx b/src/dif/math/Box3F.hx new file mode 100644 index 00000000..8666e2f6 --- /dev/null +++ b/src/dif/math/Box3F.hx @@ -0,0 +1,104 @@ +package dif.math; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class Box3F { + public var minX:Float; + public var minY:Float; + public var minZ:Float; + public var maxX:Float; + public var maxY:Float; + public var maxZ:Float; + + public function new(minX = 0.0, minY = 0.0, minZ = 0.0, maxX = 0.0, maxY = 0.0, maxZ = 0.0) { + this.minX = minX; + this.minY = minY; + this.minZ = minZ; + this.maxX = maxX; + this.maxY = maxY; + this.maxZ = maxZ; + } + + public function center() { + return new Point3F(minX + maxX, minY + maxY, minZ + maxZ).scalarDiv(2); + } + + public function Expand(point:Point3F) { + if (minX > point.x) + minX = point.x; + if (minY > point.y) + minY = point.y; + if (minZ > point.z) + minZ = point.z; + + if (maxX < point.x) + maxX = point.x; + if (maxY < point.y) + maxY = point.y; + if (maxZ < point.z) + maxZ = point.z; + } + + public static function PointBounds(point:Point3F, size:Point3F) { + var ret = new Box3F(); + ret.minX = point.x; + ret.minY = point.y; + ret.minZ = point.z; + ret.maxX = point.x + size.x; + ret.maxY = point.y + size.y; + ret.maxZ = point.z + size.z; + return ret; + } + + public function contains(p:Point3F) { + return (minX <= p.x && p.x <= maxX && minY <= p.y && p.y <= maxY && minZ <= p.z && p.z <= maxZ); + } + + public function getClosestPoint(point:Point3F) { + var closest = new Point3F(); + if (minX > point.x) + closest.x = minX; + else if (maxX < point.x) + closest.x = maxX; + else + closest.x = point.x; + + if (minY > point.y) + closest.y = minY; + else if (maxY < point.y) + closest.y = maxY; + else + closest.y = point.y; + + if (minZ > point.z) + closest.z = minZ; + else if (maxZ < point.z) + closest.z = maxZ; + else + closest.z = point.z; + + return closest; + } + + public static function read(io:BytesReader) { + var ret = new Box3F(); + ret.minX = io.readFloat(); + ret.minY = io.readFloat(); + ret.minZ = io.readFloat(); + ret.maxX = io.readFloat(); + ret.maxY = io.readFloat(); + ret.maxZ = io.readFloat(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeFloat(this.minX); + io.writeFloat(this.minY); + io.writeFloat(this.minZ); + io.writeFloat(this.maxX); + io.writeFloat(this.maxY); + io.writeFloat(this.maxZ); + } +} diff --git a/src/dif/math/PlaneF.hx b/src/dif/math/PlaneF.hx new file mode 100644 index 00000000..0a4d83ed --- /dev/null +++ b/src/dif/math/PlaneF.hx @@ -0,0 +1,84 @@ +package dif.math; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class PlaneF { + public var x:Float; + public var y:Float; + public var z:Float; + public var d:Float; + + public function new(x = 0.0, y = 0.0, z = 0.0, d = 0.0) { + this.x = x; + this.y = y; + this.z = z; + this.d = d; + } + + public function distance(p:Point3F) { + return this.x * p.x + this.y * p.y + this.z * p.z + this.d; + } + + public function project(p:Point3F) { + var d = distance(p); + return new Point3F(p.x - d * this.x, p.y - d * this.y, p.z - d * this.z); + } + + public function getNormal() { + return new Point3F(this.x, this.y, this.z); + } + + public static function ThreePoints(a:Point3F, b:Point3F, c:Point3F) { + var v1 = a.sub(b); + var v2 = c.sub(b); + var res = v1.cross(v2); + + var ret = new PlaneF(); + + var normal = res.normalized(); + ret.x = normal.x; + ret.y = normal.y; + ret.z = normal.z; + ret.d = -b.dot(normal); + return ret; + } + + public static function NormalD(normal:Point3F, d:Float) { + var ret = new PlaneF(); + + ret.x = normal.x; + ret.y = normal.y; + ret.z = normal.z; + ret.d = d; + return ret; + } + + public static function PointNormal(pt:Point3F, n:Point3F) { + var ret = new PlaneF(); + + var normal = n.normalized(); + ret.x = normal.x; + ret.y = normal.y; + ret.z = normal.z; + ret.d = -pt.dot(normal); + return ret; + } + + public static function read(io:BytesReader) { + var ret = new PlaneF(); + ret.x = io.readFloat(); + ret.y = io.readFloat(); + ret.z = io.readFloat(); + ret.d = io.readFloat(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeFloat(this.x); + io.writeFloat(this.y); + io.writeFloat(this.z); + io.writeFloat(this.d); + } +} diff --git a/src/dif/math/Point2F.hx b/src/dif/math/Point2F.hx new file mode 100644 index 00000000..af367fdc --- /dev/null +++ b/src/dif/math/Point2F.hx @@ -0,0 +1,15 @@ +package dif.math; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class Point2F { + public var x:Float; + public var y:Float; + + public function new(x, y) { + this.x = x; + this.y = y; + } +} diff --git a/src/dif/math/Point3F.hx b/src/dif/math/Point3F.hx new file mode 100644 index 00000000..58f9f0c8 --- /dev/null +++ b/src/dif/math/Point3F.hx @@ -0,0 +1,94 @@ +package dif.math; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class Point3F { + public var x:Float; + public var y:Float; + public var z:Float; + + public function new(x = 0.0, y = 0.0, z = 0.0) { + this.x = x; + this.y = y; + this.z = z; + } + + @:op([]) public function get(dim:Int) { + if (dim == 0) + return this.x; + if (dim == 1) + return this.y; + if (dim == 2) + return this.z; + return -1; + } + + public function set(dim:Int, value:Float) { + if (dim == 0) + this.x = value; + if (dim == 1) + this.y = value; + if (dim == 2) + this.z = value; + } + + @:op(A + B) public function add(rhs:Point3F) { + return new Point3F(this.x + rhs.x, this.y + rhs.y, this.z + rhs.z); + } + + @:op(A - B) public function sub(rhs:Point3F) { + return new Point3F(this.x - rhs.x, this.y - rhs.y, this.z - rhs.z); + } + + @:op(A * B) public function scalar(rhs:Float) { + return new Point3F(this.x * rhs, this.y * rhs, this.z * rhs); + } + + @:op(A / B) public function scalarDiv(rhs:Float) { + return new Point3F(this.x / rhs, this.y / rhs, this.z / rhs); + } + + public function dot(rhs:Point3F) { + return this.x * rhs.x + this.y * rhs.y + this.z * rhs.z; + } + + public function cross(rhs:Point3F) { + return new Point3F(this.y * rhs.z - this.z * rhs.y, this.z * rhs.x - this.x * rhs.z, this.x * rhs.y - this.y * rhs.x); + } + + public function length() { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + } + + public function lengthSq() { + return this.x * this.x + this.y * this.y + this.z * this.z; + } + + public function normalized() { + return this.length() != 0 ? this.scalarDiv(this.length()) : this; + } + + public function equal(other:Point3F) { + return this.x == other.x && this.y == other.y && this.z == other.z; + } + + public static function read(io:BytesReader) { + var ret = new Point3F(); + ret.x = io.readFloat(); + ret.y = io.readFloat(); + ret.z = io.readFloat(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeFloat(this.x); + io.writeFloat(this.y); + io.writeFloat(this.z); + } + + public function copy() { + return new Point3F(this.x, this.y, this.z); + } +} diff --git a/src/dif/math/Point4F.hx b/src/dif/math/Point4F.hx new file mode 100644 index 00000000..6df2ccbf --- /dev/null +++ b/src/dif/math/Point4F.hx @@ -0,0 +1,35 @@ +package dif.math; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class Point4F { + public var x:Float; + public var y:Float; + public var z:Float; + public var w:Float; + + public function new() { + this.x = 0; + this.y = 0; + this.z = 0; + this.w = 0; + } + + public static function read(io:BytesReader) { + var ret = new Point4F(); + ret.x = io.readFloat(); + ret.y = io.readFloat(); + ret.z = io.readFloat(); + ret.w = io.readFloat(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeFloat(this.x); + io.writeFloat(this.y); + io.writeFloat(this.z); + io.writeFloat(this.w); + } +} diff --git a/src/dif/math/QuatF.hx b/src/dif/math/QuatF.hx new file mode 100644 index 00000000..0092c85c --- /dev/null +++ b/src/dif/math/QuatF.hx @@ -0,0 +1,35 @@ +package dif.math; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class QuatF { + public var x:Float; + public var y:Float; + public var z:Float; + public var w:Float; + + public function new() { + this.x = 0; + this.y = 0; + this.z = 0; + this.w = 0; + } + + public static function read(io:BytesReader) { + var ret = new QuatF(); + ret.x = io.readFloat(); + ret.y = io.readFloat(); + ret.z = io.readFloat(); + ret.w = io.readFloat(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeFloat(this.x); + io.writeFloat(this.y); + io.writeFloat(this.z); + io.writeFloat(this.w); + } +} diff --git a/src/dif/math/SphereF.hx b/src/dif/math/SphereF.hx new file mode 100644 index 00000000..c860c073 --- /dev/null +++ b/src/dif/math/SphereF.hx @@ -0,0 +1,35 @@ +package dif.math; + +import dif.io.BytesWriter; +import dif.io.BytesReader; + +@:expose +class Spheref { + public var originX:Float; + public var originY:Float; + public var originZ:Float; + public var radius:Float; + + public function new() { + this.originX = 0; + this.originY = 0; + this.originZ = 0; + this.radius = 0; + } + + public static function read(io:BytesReader) { + var ret = new Spheref(); + ret.originX = io.readFloat(); + ret.originY = io.readFloat(); + ret.originZ = io.readFloat(); + ret.radius = io.readFloat(); + return ret; + } + + public function write(io:BytesWriter) { + io.writeFloat(this.originX); + io.writeFloat(this.originY); + io.writeFloat(this.originZ); + io.writeFloat(this.radius); + } +} diff --git a/src/octree/IOctreeElement.hx b/src/octree/IOctreeElement.hx new file mode 100644 index 00000000..50acb16c --- /dev/null +++ b/src/octree/IOctreeElement.hx @@ -0,0 +1,8 @@ +package octree; + +import polygonal.ds.Prioritizable; + +interface IOctreeElement extends Prioritizable { + function getElementType():Int; + function setPriority(priority:Int):Void; +} diff --git a/src/octree/IOctreeObject.hx b/src/octree/IOctreeObject.hx new file mode 100644 index 00000000..800d7aab --- /dev/null +++ b/src/octree/IOctreeObject.hx @@ -0,0 +1,9 @@ +package octree; + +import h3d.Vector; +import h3d.col.Bounds; + +interface IOctreeObject extends IOctreeElement { + var boundingBox:Bounds; + function isIntersectedByRay(rayOrigin:Vector, rayDirection:Vector, intersectionPoint:Vector):Bool; +} diff --git a/src/octree/Octree.hx b/src/octree/Octree.hx new file mode 100644 index 00000000..ab896b6c --- /dev/null +++ b/src/octree/Octree.hx @@ -0,0 +1,205 @@ +package octree; + +import h3d.Vector; +import h3d.col.Bounds; + +class Octree { + public var root:OctreeNode; + + /** A map of each object in the octree to the node that it's in. This accelerates removal drastically, as the lookup step can be skipped. */ + public var objectToNode:Map; + + public var tempBox = new Bounds(); + + public function new() { + this.root = new OctreeNode(this, 0); + // Init the octree to a 1x1x1 cube + this.root.min.set(0, 0, 0); + this.root.size = 1; + this.objectToNode = new Map(); + } + + public function insert(object:IOctreeObject) { + var node = this.objectToNode.get(object); + if (node != null) + return; // Don't insert if already contained in the tree + while (!this.root.largerThan(object) || !this.root.containsCenter(object)) { + // The root node does not fit the object; we need to grow the tree. + var a = !this.root.largerThan(object); + var b = !this.root.containsCenter(object); + if (this.root.depth == -32) { + return; + } + this.grow(object); + } + var emptyBefore = this.root.count == 0; + this.root.insert(object); + if (emptyBefore) + this.shrink(); // See if we can fit the octree better now that we actually have an element in it + } + + public function remove(object:IOctreeObject) { + var node = this.objectToNode.get(object); + if (node == null) + return; + node.remove(object); + this.objectToNode.remove(object); + this.shrink(); // Try shrinking the octree + } + + /** Updates an object in the tree whose bounding box has changed. */ + public function update(object:IOctreeObject) { + this.remove(object); + this.insert(object); + } + + /** Expand the octree towards an object that doesn't fit in it. */ + public function grow(towards:IOctreeObject) { + // We wanna grow towards all the vertices of the object's bounding box that lie outside the octree, so we determine the average position of those vertices: + var averagePoint = new Vector(); + var count = 0; + for (i in 0...8) { + var vec = new Vector(); + vec.x = (i & 1) == 1 ? towards.boundingBox.xMin : towards.boundingBox.xMax; + vec.y = (i & 2) == 2 ? towards.boundingBox.yMin : towards.boundingBox.yMax; + vec.z = (i & 4) == 4 ? towards.boundingBox.zMin : towards.boundingBox.zMax; + if (!this.root.containsPoint(vec)) { + averagePoint.add(vec); + count++; + } + } + averagePoint.multiply(1 / count); // count should be greater than 0, because that's why we're growing in the first place. + // Determine the direction from the root center to the determined point + var rootCenter = this.root.min.clone().add(new Vector(this.root.size / 2, this.root.size / 2, this.root.size / 2)); + var direction = averagePoint.sub(rootCenter); // Determine the "direction of growth" + // Create a new root. The current root will become a quadrant in this new root. + var newRoot = new OctreeNode(this, this.root.depth - 1); + newRoot.min = this.root.min.clone(); + newRoot.size = this.root.size * 2; + if (direction.x < 0) + newRoot.min.x -= this.root.size; + if (direction.y < 0) + newRoot.min.y -= this.root.size; + if (direction.z < 0) + newRoot.min.z -= this.root.size; + if (this.root.count > 0) { + var octantIndex = ((direction.x < 0) ? 1 : 0) + ((direction.y < 0) ? 2 : 0) + ((direction.z < 0) ? 4 : 0); + newRoot.createOctants(); + newRoot.octants[octantIndex] = this.root; + this.root.parent = newRoot; + newRoot.count = this.root.count; + newRoot.merge(); + } + this.root = newRoot; + } + + /** Tries to shrink the octree if large parts of the octree are empty. */ + public function shrink() { + if (this.root.size < 1 || this.root.objects.length > 0) + return; + if (this.root.count == 0) { + // Reset to default empty octree + this.root.min.set(0, 0, 0); + this.root.size = 1; + this.root.depth = 0; + return; + } + if (this.root.octants == null) + return; + // Find the only non-empty octant + var nonEmptyOctant:OctreeNode = null; + for (i in 0...8) { + var octant = this.root.octants[i]; + if (octant.count > 0) { + if (nonEmptyOctant != null) + return; // There are more than two non-empty octants -> don't shrink. + else + nonEmptyOctant = octant; + } + } + // Make the only non-empty octant the new root + this.root = nonEmptyOctant; + nonEmptyOctant.parent = null; + this.shrink(); + } + + /** Returns a list of all objects that intersect with the given ray, sorted by distance. */ + public function raycast(rayOrigin:Vector, rayDirection:Vector) { + var intersections:Array = []; + this.root.raycast(rayOrigin, rayDirection, intersections); + intersections.sort((a, b) -> cast(a.distance - b.distance)); + return intersections; + } + + public function radiusSearch(point:Vector, maximumDistance:Float) { + function getClosestPoint(box:Bounds, point:Vector) { + var closest = new Vector(); + if (box.xMin > point.x) + closest.x = box.xMin; + else if (box.xMax < point.x) + closest.x = box.xMax; + else + closest.x = point.x; + + if (box.yMin > point.y) + closest.y = box.yMin; + else if (box.yMax < point.y) + closest.y = box.yMax; + else + closest.y = point.y; + + if (box.zMin > point.z) + closest.z = box.zMin; + else if (box.zMax < point.z) + closest.z = box.zMax; + else + closest.z = point.z; + + return closest; + } + + var L = []; + var queue = new PriorityQueue(); + + var maxDistSq = maximumDistance * maximumDistance; + var closestPoint = this.root.getClosestPoint(point); + var distSq = closestPoint.distanceSq(point); + + if (distSq > maximumDistance) + return L; + + this.root.setPriority(cast(-distSq)); + queue.enqueue(root, distSq); + + while (queue.count > 0) { + var node = queue.dequeue(); + + switch (node.getElementType()) { + case 1: + var octant = cast(node, OctreeNode); + if (octant.objects != null) { + for (object in octant.objects) { + var dist = point.distanceSq(getClosestPoint(object.boundingBox, point)); + if (dist < maxDistSq) { + object.setPriority(cast(-dist)); + queue.enqueue(object, dist); + } + } + } + if (octant.octants != null) { + for (suboctant in octant.octants) { + var dist = point.distanceSq(suboctant.getClosestPoint(point)); + if (dist < maxDistSq) { + suboctant.setPriority(cast(-dist)); + queue.enqueue(suboctant, dist); + } + } + } + + case 2: + L.push(cast(node, IOctreeObject)); + } + } + return L; + } +} diff --git a/src/octree/OctreeIntersection.hx b/src/octree/OctreeIntersection.hx new file mode 100644 index 00000000..78b80627 --- /dev/null +++ b/src/octree/OctreeIntersection.hx @@ -0,0 +1,11 @@ +package octree; + +import h3d.Vector; + +class OctreeIntersection { + public var object:IOctreeObject; + public var point:Vector; + public var distance:Float; + + public function new() {} +} diff --git a/src/octree/OctreeNode.hx b/src/octree/OctreeNode.hx new file mode 100644 index 00000000..d7d1f3d6 --- /dev/null +++ b/src/octree/OctreeNode.hx @@ -0,0 +1,221 @@ +package octree; + +import h3d.col.Ray; +import h3d.col.Bounds; +import h3d.col.Point; +import h3d.Vector; + +class OctreeNode implements IOctreeElement { + public var octree:Octree; + public var parent:OctreeNode = null; + + public var priority:Int; + public var position:Int; + + /** The min corner of the bounding box. */ + public var min = new Vector(); + + /** The size of the bounding box on all three axes. This forces the bounding box to be a cube. */ + public var size:Int; + + public var octants:Array = null; + + /** A list of objects contained in this node. Note that the node doesn't need to be a leaf node for this set to be non-empty; since this is an octree of bounding boxes, some volumes cannot fit into an octant and therefore need to be stored in the node itself. */ + public var objects = new Array(); + + /** The total number of objects in the subtree with this node as its root. */ + public var count = 0; + + public var depth:Int; + + public function new(octree:Octree, depth:Int) { + this.octree = octree; + this.depth = depth; + } + + public function insert(object:IOctreeObject) { + this.count++; + if (this.octants != null) { + // First we check if the object can fit into any of the octants (they all have the same size, so checking only one suffices) + if (this.octants[0].largerThan(object)) { + // Try to insert the object into one of the octants... + for (i in 0...8) { + var octant = this.octants[i]; + if (octant.containsCenter(object)) { + octant.insert(object); + return; + } + } + } + // No octant fit the object, so add it to the list of objects instead + this.objects.push(object); + this.octree.objectToNode.set(object, this); + } else { + this.objects.push(object); + this.octree.objectToNode.set(object, this); + this.split(); // Try splitting this node + } + } + + public function split() { + if (this.objects.length <= 8 || this.depth == 8) + return; + this.createOctants(); + // Put the objects into the correct octants. Note that all objects that couldn't fit into any octant will remain in the set. + for (object in this.objects) { + if (this.octants[0].largerThan(object)) { + for (j in 0...8) { + var octant = this.octants[j]; + if (octant.containsCenter(object)) { + octant.insert(object); + this.objects.remove(object); + } + } + } + } + // Try recursively splitting each octant + for (i in 0...8) { + this.octants[i].split(); + } + } + + public function createOctants() { + this.octants = []; + for (i in 0...8) { + var newNode = new OctreeNode(this.octree, this.depth + 1); + newNode.parent = this; + newNode.size = cast this.size / 2; + newNode.min.set(this.min.x + + newNode.size * ((i & 1) >> 0), // The x coordinate changes every index + this.min.y + + newNode.size * ((i & 2) >> 1), // The y coordinate changes every 2 indices + this.min.z + + newNode.size * ((i & 4) >> 2) // The z coordinate changes every 4 indices + ); + this.octants.push(newNode); + } + } + + // Note: The requirement for this method to be called is that `object` is contained directly in this node. + public function remove(object:IOctreeObject) { + this.objects.remove(object); + this.count--; + this.merge(); + // Clean up all ancestors + var node = this.parent; + while (node != null) { + node.count--; // Reduce the count for all ancestor nodes up until the root + node.merge(); + node = node.parent; + } + } + + public function merge() { + if (this.count > 8 || !(this.octants == null)) + return; + // Add all objects in the octants back to this node + for (i in 0...8) { + var octant = this.octants[i]; + for (object in octant.objects) { + this.objects.push(object); + this.octree.objectToNode.set(object, this); + } + } + this.octants = null; // ...then devare the octants + } + + public function largerThan(object:IOctreeObject) { + var box = object.boundingBox; + var bb = new Bounds(); + bb.setMin(this.min.toPoint()); + bb.xMax = bb.xMin + this.size; + bb.yMax = bb.yMin + this.size; + bb.zMax = bb.zMin + this.size; + return bb.containsBounds(box); + // return this.size > (box.xMax - box.xMin) && this.size > (box.yMax - box.yMin) && this.size > (box.zMax - box.zMin); + } + + public function containsCenter(object:IOctreeObject) { + var box = object.boundingBox; + var x = box.xMin + (box.xMax - box.xMin) / 2; + var y = box.yMin + (box.yMax - box.yMin) / 2; + var z = box.zMin + (box.zMax - box.zMin) / 2; + return this.min.x <= x && x < (this.min.x + this.size) && this.min.y <= y && y < (this.min.y + this.size) && this.min.z <= z + && z < (this.min.z + this.size); + } + + public function containsPoint(point:Vector) { + var x = point.x; + var y = point.y; + var z = point.z; + return this.min.x <= x && x < (this.min.x + this.size) && this.min.y <= y && y < (this.min.y + this.size) && this.min.z <= z + && z < (this.min.z + this.size); + } + + public function raycast(rayOrigin:Vector, rayDirection:Vector, intersections:Array) { + // Construct the loose bounding box of this node (2x in size, with the regular bounding box in the center) + var looseBoundingBox = this.octree.tempBox; + looseBoundingBox.xMin += this.min.x + (-this.size / 2); + looseBoundingBox.yMin += this.min.y + (-this.size / 2); + looseBoundingBox.zMin += this.min.z + (-this.size / 2); + looseBoundingBox.xMax += this.min.x + (this.size * 3 / 2); + looseBoundingBox.yMax += this.min.y + (this.size * 3 / 2); + looseBoundingBox.zMax += this.min.z + (this.size * 3 / 2); + if (looseBoundingBox.rayIntersection(Ray.fromValues(rayOrigin.x, rayOrigin.y, rayOrigin.z, rayDirection.x, rayDirection.y, rayDirection.z), + true) == -1) + return; // The ray doesn't hit the node's loose bounding box; we can stop + var vec = new Vector(); + // Test all objects for intersection + if (this.objects.length > 0) + for (object in this.objects) { + if (object.isIntersectedByRay(rayOrigin, rayDirection, vec)) { + var intersection:OctreeIntersection = new OctreeIntersection(); + intersection.object = object; + intersection.point = vec; + intersection.distance = rayOrigin.distance(vec); + intersections.push(intersection); + vec = new Vector(); + } + } + // Recurse into the octants + if (this.octants != null) + for (i in 0...8) { + var octant = this.octants[i]; + octant.raycast(rayOrigin, rayDirection, intersections); + } + } + + public function getClosestPoint(point:Vector) { + var closest = new Vector(); + if (this.min.x > point.x) + closest.x = this.min.x; + else if (this.min.x + this.size < point.x) + closest.x = this.min.x + this.size; + else + closest.x = point.x; + + if (this.min.y > point.y) + closest.y = this.min.y; + else if (this.min.y + this.size < point.y) + closest.y = this.min.y + this.size; + else + closest.y = point.y; + + if (this.min.z > point.z) + closest.z = this.min.z; + else if (this.min.z + this.size < point.z) + closest.z = this.min.z + this.size; + else + closest.z = point.z; + + return closest; + } + + public function getElementType() { + return 1; + } + + public function setPriority(priority:Int) { + this.priority = priority; + } +} diff --git a/src/octree/PriorityQueue.hx b/src/octree/PriorityQueue.hx new file mode 100644 index 00000000..edd6c546 --- /dev/null +++ b/src/octree/PriorityQueue.hx @@ -0,0 +1,57 @@ +package octree; + +class PriorityQueue { + var first:PriorityQueueNode; + + public var count:Int; + + public function new() { + count = 0; + first = null; + } + + public function enqueue(val:T, priority:Float) { + var node = new PriorityQueueNode(val, priority); + if (this.first == null) { + this.first = node; + } else { + if (this.first.priority >= priority) { + node.next = this.first; + this.first.prev = node; + this.first = node; + } else { + var n = this.first; + var end = false; + while (n.priority < node.priority) { + if (n.next == null) { + end = true; + break; + } + n = n.next; + } + if (!end) { + if (n.prev != null) { + n.prev.next = node; + node.prev = n.prev; + } + n.prev = node; + node.next = n; + } else { + n.next = node; + node.prev = n; + } + } + } + + count++; + } + + public function dequeue() { + var ret = this.first; + this.first = this.first.next; + if (this.first != null) + this.first.prev = null; + count--; + return ret.value; + } +} diff --git a/src/octree/PriorityQueueNode.hx b/src/octree/PriorityQueueNode.hx new file mode 100644 index 00000000..5e5f0f8a --- /dev/null +++ b/src/octree/PriorityQueueNode.hx @@ -0,0 +1,13 @@ +package octree; + +class PriorityQueueNode { + public var value:T; + public var priority:Float; + public var next:PriorityQueueNode; + public var prev:PriorityQueueNode; + + public function new(value:T, priority:Float) { + this.value = value; + this.priority = priority; + } +} diff --git a/src/octreenarrowphase/IOctreeNode.hx b/src/octreenarrowphase/IOctreeNode.hx new file mode 100644 index 00000000..e97967e6 --- /dev/null +++ b/src/octreenarrowphase/IOctreeNode.hx @@ -0,0 +1,7 @@ +package octreenarrowphase; + +import polygonal.ds.Prioritizable; + +interface IOctreeNode extends Prioritizable { + function getNodeType():Int; +} diff --git a/src/octreenarrowphase/Octree.hx b/src/octreenarrowphase/Octree.hx new file mode 100644 index 00000000..d690ee1e --- /dev/null +++ b/src/octreenarrowphase/Octree.hx @@ -0,0 +1,84 @@ +package octreenarrowphase; + +import polygonal.ds.PriorityQueue; +import dif.math.Box3F; +import dif.math.Point3F; + +class Octree { + var root:OctreeNode; + + public function new(pts:Array>, binPoints:Int = 8) { + var pos = pts; + + var min = new Point3F(); + var max = new Point3F(); + + // Generate the bounding box + for (index => op in pos) { + var p = op.point; + if (p.x < min.x) + min.x = p.x; + if (p.y < min.y) + min.y = p.y; + if (p.z < min.z) + min.z = p.z; + + if (p.x > max.x) + max.x = p.x; + if (p.y > max.y) + max.y = p.y; + if (p.z > max.z) + max.z = p.z; + } + + root = new OctreeNode(); + root.box = new Box3F(min.x, min.y, min.z, max.x, max.y, max.z); + + // We use the insert method because its much faster doing this way + for (index => pt in pts) + root.insert(pt.point, pt.value); + } + + public function find(pt:Point3F) + return root.find(pt); + + public function remove(pt:Point3F) + return root.remove(pt); + + public function insert(pt:Point3F, value:T) + return root.insert(pt, value); + + public function knn(point:Point3F, number:Int) { + var queue = new PriorityQueue>(); + root.priority = cast(-root.box.getClosestPoint(point).sub(point).lengthSq()); + queue.enqueue(root); + + var l = new Array>(); + + while (l.length < number && queue.size > 0) { + var node = queue.dequeue(); + + switch (node.getNodeType()) { + case 1: + var leaf:OctreeNode = cast node; + for (index => pt in leaf.points) { + pt.priority = cast(-pt.point.sub(point).lengthSq()); + queue.enqueue(pt); + } + + case 0: + var pt:OctreePoint = cast node; + l.push(pt); + + case 2: + var n:OctreeNode = cast node; + for (subnode in n.nodes) { + subnode.priority = cast(-subnode.box.getClosestPoint(point).sub(point).lengthSq()); + queue.enqueue(subnode); + } + } + } + + return l; + } +} diff --git a/src/octreenarrowphase/OctreeNode.hx b/src/octreenarrowphase/OctreeNode.hx new file mode 100644 index 00000000..284eb983 --- /dev/null +++ b/src/octreenarrowphase/OctreeNode.hx @@ -0,0 +1,168 @@ +package octreenarrowphase; + +import dif.math.Box3F; +import dif.math.Point3F; + +class OctreeNode implements IOctreeNode { + public var nodes:Array>; + + public var priority:Int; + public var position:Int; + + var isLeaf:Bool; + + public var points:Array>; + + var center:Point3F; + + public var box:Box3F; + + public function new() { + this.isLeaf = true; + this.points = new Array>(); + } + + public function getCount() { + if (this.isLeaf) { + return this.points.length; + } else { + var res = 0; + for (index => value in nodes) { + res += value.getCount(); + } + return res; + } + } + + function getIsEmpty() { + if (this.isLeaf) + return this.getCount() == 0; + else { + for (index => value in nodes) { + if (!value.getIsEmpty()) + return false; + } + return true; + } + } + + public function find(pt:Point3F) { + if (this.isLeaf) { + for (index => value in this.points) { + if (value.point.equal(pt)) + return true; + } + return true; + } else { + var msk = 0; + msk |= (pt.x - center.x) < 0 ? 1 : 0; + msk |= (pt.y - center.y) < 0 ? 2 : 0; + msk |= (pt.z - center.z) < 0 ? 4 : 0; + + return nodes[msk].find(pt); + } + } + + public function remove(pt:Point3F) { + if (this.isLeaf) { + var found = false; + var idx = -1; + for (index => value in this.points) { + if (value.point.equal(pt)) { + found = true; + idx = index; + break; + } + } + if (found) { + return this.points.remove(this.points[idx]); + } else + return false; + } else { + var msk = 0; + msk |= (pt.x - center.x) < 0 ? 1 : 0; + msk |= (pt.y - center.y) < 0 ? 2 : 0; + msk |= (pt.z - center.z) < 0 ? 4 : 0; + + var ret = nodes[msk].remove(pt); + this.merge(); + return ret; + } + } + + public function insert(pt:Point3F, value:T) { + if (this.isLeaf) { + this.points.push(new OctreePoint(pt, value)); + subdivide(); + } else { + var msk = 0; + msk |= (pt.x - center.x) < 0 ? 1 : 0; + msk |= (pt.y - center.y) < 0 ? 2 : 0; + msk |= (pt.z - center.z) < 0 ? 4 : 0; + nodes[msk].insert(pt, value); + } + } + + function subdivide(binPoints:Int = 8) { + var min = new Point3F(box.minX, box.minY, box.minZ); + var max = new Point3F(box.maxX, box.maxY, box.maxZ); + center = min.add(max).scalarDiv(2); + + if (points.length > binPoints) { + isLeaf = false; + + var size = max.sub(min); + + nodes = new Array>(); + for (i in 0...8) { + nodes.push(new OctreeNode()); + } + + nodes[0].box = new Box3F(center.x, center.y, center.z, max.x, max.y, max.z); + nodes[1].box = new Box3F(center.x - (size.x / 2), center.y, center.z, max.x - (size.x / 2), max.y, max.z); + nodes[2].box = new Box3F(center.x, center.y - (size.y / 2), center.z, max.x, max.y - (size.y / 2), max.z); + nodes[3].box = new Box3F(center.x - (size.x / 2), center.y - (size.y / 2), center.z, max.x - (size.x / 2), max.y - (size.y / 2), max.z); + nodes[4].box = new Box3F(center.x, center.y, center.z - (size.z / 2), max.x, max.y, max.z - (size.z / 2)); + nodes[5].box = new Box3F(center.x - (size.x / 2), center.y, center.z - (size.z / 2), max.x - (size.x / 2), max.y, max.z - (size.z / 2)); + nodes[6].box = new Box3F(center.x, center.y - (size.y / 2), center.z - (size.z / 2), max.x, max.y - (size.y / 2), max.z - (size.z / 2)); + nodes[7].box = new Box3F(min.x, min.y, min.z, max.x, max.y, max.z); + + for (index => pt in points) { + var msk = 0; + msk |= (pt.point.x - center.x) < 0 ? 1 : 0; + msk |= (pt.point.y - center.y) < 0 ? 2 : 0; + msk |= (pt.point.z - center.z) < 0 ? 4 : 0; + + if (!nodes[msk].find(pt.point)) + nodes[msk].points.push(new OctreePoint(pt.point, pt.value)); + } + + points = null; + + for (index => value in nodes) { + value.subdivide(binPoints); + } + } else { + isLeaf = true; + } + } + + function merge() { + if (this.isLeaf) { + return; + } else { + if (this.getIsEmpty()) { + this.isLeaf = true; + this.nodes = null; + this.points = new Array>(); + } + } + } + + public function getNodeType() { + if (this.isLeaf) + return 1; + else + return 2; + } +} diff --git a/src/octreenarrowphase/OctreePoint.hx b/src/octreenarrowphase/OctreePoint.hx new file mode 100644 index 00000000..6e9da413 --- /dev/null +++ b/src/octreenarrowphase/OctreePoint.hx @@ -0,0 +1,21 @@ +package octreenarrowphase; + +import dif.math.Point3F; + +class OctreePoint implements IOctreeNode { + public var point:Point3F; + + public var priority:Int; + public var position:Int; + + public var value:T; + + public function new(point:Point3F, value:T) { + this.point = point; + this.value = value; + } + + public function getNodeType() { + return 0; + } +}