From 58d2293358ba9594de0f0e612372bc627a534867 Mon Sep 17 00:00:00 2001 From: dexy Date: Mon, 9 Dec 2019 22:33:36 +1100 Subject: [PATCH] Collision detection refactoring and improvements --- CodeWalker.Core/GameFiles/Resources/Bounds.cs | 473 +++++++++++++++++- CodeWalker.Core/Utils/Vectors.cs | 196 ++++++++ CodeWalker.Core/World/Space.cs | 440 ++-------------- WorldForm.cs | 32 +- 4 files changed, 728 insertions(+), 413 deletions(-) diff --git a/CodeWalker.Core/GameFiles/Resources/Bounds.cs b/CodeWalker.Core/GameFiles/Resources/Bounds.cs index ed50602..7dd3a8c 100644 --- a/CodeWalker.Core/GameFiles/Resources/Bounds.cs +++ b/CodeWalker.Core/GameFiles/Resources/Bounds.cs @@ -32,11 +32,11 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using CodeWalker.World; using TC = System.ComponentModel.TypeConverterAttribute; using EXP = System.ComponentModel.ExpandableObjectConverter; - namespace CodeWalker.GameFiles { @@ -231,9 +231,29 @@ namespace CodeWalker.GameFiles default: return null; // throw new Exception("Unknown bound type"); } } + + + public virtual SpaceSphereIntersectResult SphereIntersect(ref BoundingSphere sph) + { + return new SpaceSphereIntersectResult(); + } + public virtual SpaceRayIntersectResult RayIntersect(ref Ray ray, float maxdist = float.MaxValue, float itemhitdist = float.MaxValue) + { + return new SpaceRayIntersectResult(); + } + } [TC(typeof(EXP))] public class BoundSphere : Bounds - { } + { + public override SpaceSphereIntersectResult SphereIntersect(ref BoundingSphere sph) + { + return base.SphereIntersect(ref sph); + } + public override SpaceRayIntersectResult RayIntersect(ref Ray ray, float maxdist = float.MaxValue, float itemhitdist = float.MaxValue) + { + return base.RayIntersect(ref ray, maxdist, itemhitdist); + } + } [TC(typeof(EXP))] public class BoundCapsule : Bounds { public override long BlockLength @@ -274,9 +294,27 @@ namespace CodeWalker.GameFiles writer.Write(this.Unknown_78h); writer.Write(this.Unknown_7Ch); } + + public override SpaceSphereIntersectResult SphereIntersect(ref BoundingSphere sph) + { + return base.SphereIntersect(ref sph); + } + public override SpaceRayIntersectResult RayIntersect(ref Ray ray, float maxdist = float.MaxValue, float itemhitdist = float.MaxValue) + { + return base.RayIntersect(ref ray, maxdist, itemhitdist); + } } [TC(typeof(EXP))] public class BoundBox : Bounds - { } + { + public override SpaceSphereIntersectResult SphereIntersect(ref BoundingSphere sph) + { + return base.SphereIntersect(ref sph); + } + public override SpaceRayIntersectResult RayIntersect(ref Ray ray, float maxdist = float.MaxValue, float itemhitdist = float.MaxValue) + { + return base.RayIntersect(ref ray, maxdist, itemhitdist); + } + } [TC(typeof(EXP))] public class BoundDisc : Bounds { public override long BlockLength @@ -317,6 +355,15 @@ namespace CodeWalker.GameFiles writer.Write(this.Unknown_78h); writer.Write(this.Unknown_7Ch); } + + public override SpaceSphereIntersectResult SphereIntersect(ref BoundingSphere sph) + { + return base.SphereIntersect(ref sph); + } + public override SpaceRayIntersectResult RayIntersect(ref Ray ray, float maxdist = float.MaxValue, float itemhitdist = float.MaxValue) + { + return base.RayIntersect(ref ray, maxdist, itemhitdist); + } } [TC(typeof(EXP))] public class BoundCylinder : Bounds { @@ -358,6 +405,15 @@ namespace CodeWalker.GameFiles writer.Write(this.Unknown_78h); writer.Write(this.Unknown_7Ch); } + + public override SpaceSphereIntersectResult SphereIntersect(ref BoundingSphere sph) + { + return base.SphereIntersect(ref sph); + } + public override SpaceRayIntersectResult RayIntersect(ref Ray ray, float maxdist = float.MaxValue, float itemhitdist = float.MaxValue) + { + return base.RayIntersect(ref ray, maxdist, itemhitdist); + } } [TC(typeof(EXP))] public class BoundGeometry : Bounds { @@ -699,6 +755,15 @@ namespace CodeWalker.GameFiles } return list.ToArray(); } + + public override SpaceSphereIntersectResult SphereIntersect(ref BoundingSphere sph) + { + return base.SphereIntersect(ref sph); + } + public override SpaceRayIntersectResult RayIntersect(ref Ray ray, float maxdist = float.MaxValue, float itemhitdist = float.MaxValue) + { + return base.RayIntersect(ref ray, maxdist, itemhitdist); + } } [TC(typeof(EXP))] public class BoundUnknown1 : ResourceSystemBlock @@ -1186,6 +1251,345 @@ namespace CodeWalker.GameFiles if (BVH != null) list.Add(BVH); return list.ToArray(); } + + + + public override SpaceSphereIntersectResult SphereIntersect(ref BoundingSphere sph) + { + var res = new SpaceSphereIntersectResult(); + var box = new BoundingBox(); + Vector3 p1, p2, p3, p4, a1, a2, a3; + Vector3 n1 = Vector3.Zero; + var tsph = new BoundingSphere(); + var spht = new BoundingSphere(); + var sp = sph.Center; + var sr = sph.Radius; + + + if (Polygons == null) + { return res; } + if ((BVH?.Nodes?.data_items == null) || (BVH?.Trees?.data_items == null)) + { return res; } + + box.Minimum = BoundingBoxMin; + box.Maximum = BoundingBoxMax; + if (!sph.Intersects(ref box)) + { return res; } + + var q = BVH.Quantum.XYZ(); + var c = BVH.BoundingBoxCenter.XYZ(); + var cg = CenterGeom; + for (int t = 0; t < BVH.Trees.data_items.Length; t++) + { + var tree = BVH.Trees.data_items[t]; + box.Minimum = new Vector3(tree.MinX, tree.MinY, tree.MinZ) * q + c; + box.Maximum = new Vector3(tree.MaxX, tree.MaxY, tree.MaxZ) * q + c; + if (!sph.Intersects(ref box)) + { continue; } + + int nodeind = tree.NodeIndex1; + int lastind = tree.NodeIndex2; + while (nodeind < lastind) + { + var node = BVH.Nodes.data_items[nodeind]; + box.Minimum = new Vector3(node.MinX, node.MinY, node.MinZ) * q + c; + box.Maximum = new Vector3(node.MaxX, node.MaxY, node.MaxZ) * q + c; + bool nodehit = sph.Intersects(ref box); + bool nodeskip = !nodehit; + if (node.PolyCount <= 0) //intermediate node with child nodes + { + if (nodeskip) + { + nodeind += node.PolyId; //(child node count) + } + else + { + nodeind++; + } + } + else //leaf node, with polygons + { + if (!nodeskip) + { + var lastp = node.PolyId + node.PolyCount; + lastp = Math.Min(lastp, (int)PolygonsCount); + for (int p = node.PolyId; p < lastp; p++) + { + var polygon = Polygons[p]; + bool polyhit = false; + switch (polygon.Type) + { + case BoundPolygonType.Triangle: + var ptri = polygon as BoundPolygonTriangle; + p1 = GetVertex(ptri.vertIndex1) + cg; + p2 = GetVertex(ptri.vertIndex2) + cg; + p3 = GetVertex(ptri.vertIndex3) + cg; + polyhit = sph.Intersects(ref p1, ref p2, ref p3); + if (polyhit) n1 = Vector3.Normalize(Vector3.Cross(p2 - p1, p3 - p1)); + break; + case BoundPolygonType.Sphere: + var psph = polygon as BoundPolygonSphere; + tsph.Center = GetVertex(psph.sphereIndex) + cg; + tsph.Radius = psph.sphereRadius; + polyhit = sph.Intersects(ref tsph); + if (polyhit) n1 = Vector3.Normalize(sph.Center - tsph.Center); + break; + case BoundPolygonType.Capsule: + var pcap = polygon as BoundPolygonCapsule; + var tcap = new BoundingCapsule(); + tcap.PointA = GetVertex(pcap.capsuleIndex1) + cg; + tcap.PointB = GetVertex(pcap.capsuleIndex2) + cg; + tcap.Radius = pcap.capsuleRadius; + polyhit = sph.Intersects(ref tcap, out n1); + break; + case BoundPolygonType.Box: + var pbox = polygon as BoundPolygonBox; + p1 = GetVertex(pbox.boxIndex1);// + cg; //corner + p2 = GetVertex(pbox.boxIndex2);// + cg; + p3 = GetVertex(pbox.boxIndex3);// + cg; + p4 = GetVertex(pbox.boxIndex4);// + cg; + a1 = ((p3 + p4) - (p1 + p2)) * 0.5f; + a2 = p3 - (p1 + a1); + a3 = p4 - (p1 + a1); + Vector3 bs = new Vector3(a1.Length(), a2.Length(), a3.Length()); + Vector3 m1 = a1 / bs.X; + Vector3 m2 = a2 / bs.Y; + Vector3 m3 = a3 / bs.Z; + if ((bs.X < bs.Y) && (bs.X < bs.Z)) m1 = Vector3.Cross(m2, m3); + else if (bs.Y < bs.Z) m2 = Vector3.Cross(m3, m1); + else m3 = Vector3.Cross(m1, m2); + Vector3 tp = sp - (p1 + cg); + spht.Center = new Vector3(Vector3.Dot(tp, m1), Vector3.Dot(tp, m2), Vector3.Dot(tp, m3)); + spht.Radius = sph.Radius; + box.Minimum = Vector3.Zero; + box.Maximum = bs; + polyhit = spht.Intersects(ref box); + if (polyhit) + { + Vector3 smin = spht.Center - spht.Radius; + Vector3 smax = spht.Center + spht.Radius; + float eps = spht.Radius * 0.8f; + n1 = Vector3.Zero; + if (Math.Abs(smax.X) < eps) n1 -= m1; + else if (Math.Abs(smin.X - bs.X) < eps) n1 += m1; + if (Math.Abs(smax.Y) < eps) n1 -= m2; + else if (Math.Abs(smin.Y - bs.Y) < eps) n1 += m2; + if (Math.Abs(smax.Z) < eps) n1 -= m3; + else if (Math.Abs(smin.Z - bs.Z) < eps) n1 += m3; + float n1l = n1.Length(); + if (n1l > 0.0f) n1 = n1 / n1l; + else n1 = Vector3.UnitZ; + } + break; + case BoundPolygonType.Cylinder: + var pcyl = polygon as BoundPolygonCylinder; + //var tcyl = new BoundingCylinder(); + //tcyl.PointA = GetVertex(pcyl.cylinderIndex1) + cg; + //tcyl.PointB = GetVertex(pcyl.cylinderIndex2) + cg; + //tcyl.Radius = pcyl.cylinderRadius; + //////polyhit = ray.Intersects(ref tcyl, out polyhittestdist, out n1); + ////////TODO + var ttcap = new BoundingCapsule();//just use the capsule intersection for now... + ttcap.PointA = GetVertex(pcyl.cylinderIndex1) + cg; + ttcap.PointB = GetVertex(pcyl.cylinderIndex2) + cg; + ttcap.Radius = pcyl.cylinderRadius; + polyhit = sph.Intersects(ref ttcap, out n1); + break; + default: + break; + } + if (polyhit) // && (polyhittestdist < itemhitdist) && (polyhittestdist < maxdist)) + { + res.HitPolygon = polygon; + //itemhitdist = polyhittestdist; + //ybnhit = true; + res.Hit = true; + res.Normal = n1; + } + res.TestedPolyCount++; + } + } + nodeind++; + } + res.TestedNodeCount++; + } + } + + return res; + } + + public override SpaceRayIntersectResult RayIntersect(ref Ray ray, float maxdist = float.MaxValue, float itemhitdist = float.MaxValue) + { + var res = new SpaceRayIntersectResult(); + var box = new BoundingBox(); + var tsph = new BoundingSphere(); + var rayt = new Ray(); + Vector3 p1, p2, p3, p4, a1, a2, a3; + Vector3 n1 = Vector3.Zero; + var rp = ray.Position; + var rd = ray.Direction; + + + + if (Polygons == null) + { return res; } + if ((BVH?.Nodes?.data_items == null) || (BVH?.Trees?.data_items == null)) + { return res; } + + box.Minimum = BoundingBoxMin; + box.Maximum = BoundingBoxMax; + float bvhboxhittest; + if (!ray.Intersects(ref box, out bvhboxhittest)) + { return res; } + if (bvhboxhittest > itemhitdist) + { return res; } //already a closer hit. + + res.HitDist = itemhitdist; + + var q = BVH.Quantum.XYZ(); + var c = BVH.BoundingBoxCenter.XYZ(); + var cg = CenterGeom; + for (int t = 0; t < BVH.Trees.data_items.Length; t++) + { + var tree = BVH.Trees.data_items[t]; + box.Minimum = new Vector3(tree.MinX, tree.MinY, tree.MinZ) * q + c; + box.Maximum = new Vector3(tree.MaxX, tree.MaxY, tree.MaxZ) * q + c; + if (!ray.Intersects(ref box, out bvhboxhittest)) + { continue; } + if (bvhboxhittest > res.HitDist) + { continue; } //already a closer hit. + if (bvhboxhittest > maxdist) + { continue; } + + int nodeind = tree.NodeIndex1; + int lastind = tree.NodeIndex2; + while (nodeind < lastind) + { + var node = BVH.Nodes.data_items[nodeind]; + box.Minimum = new Vector3(node.MinX, node.MinY, node.MinZ) * q + c; + box.Maximum = new Vector3(node.MaxX, node.MaxY, node.MaxZ) * q + c; + bool nodehit = ray.Intersects(ref box, out bvhboxhittest); + bool nodeskip = !nodehit || (bvhboxhittest > res.HitDist); + if (node.PolyCount <= 0) //intermediate node with child nodes + { + if (nodeskip) + { + nodeind += node.PolyId; //(child node count) + } + else + { + nodeind++; + } + } + else //leaf node, with polygons + { + if (!nodeskip) + { + var lastp = node.PolyId + node.PolyCount; + lastp = Math.Min(lastp, (int)PolygonsCount); + for (int p = node.PolyId; p < lastp; p++) + { + var polygon = Polygons[p]; + float polyhittestdist = float.MaxValue; + bool polyhit = false; + switch (polygon.Type) + { + case BoundPolygonType.Triangle: + var ptri = polygon as BoundPolygonTriangle; + p1 = GetVertex(ptri.vertIndex1) + cg; + p2 = GetVertex(ptri.vertIndex2) + cg; + p3 = GetVertex(ptri.vertIndex3) + cg; + polyhit = ray.Intersects(ref p1, ref p2, ref p3, out polyhittestdist); + if (polyhit) n1 = Vector3.Normalize(Vector3.Cross(p2 - p1, p3 - p1)); + break; + case BoundPolygonType.Sphere: + var psph = polygon as BoundPolygonSphere; + tsph.Center = GetVertex(psph.sphereIndex) + cg; + tsph.Radius = psph.sphereRadius; + polyhit = ray.Intersects(ref tsph, out polyhittestdist); + if (polyhit) n1 = Vector3.Normalize((ray.Position + ray.Direction * polyhittestdist) - tsph.Center); + break; + case BoundPolygonType.Capsule: + var pcap = polygon as BoundPolygonCapsule; + var tcap = new BoundingCapsule(); + tcap.PointA = GetVertex(pcap.capsuleIndex1) + cg; + tcap.PointB = GetVertex(pcap.capsuleIndex2) + cg; + tcap.Radius = pcap.capsuleRadius; + polyhit = ray.Intersects(ref tcap, out polyhittestdist); + res.Position = (ray.Position + ray.Direction * polyhittestdist); + if (polyhit) n1 = tcap.Normal(ref res.Position); + break; + case BoundPolygonType.Box: + var pbox = polygon as BoundPolygonBox; + p1 = GetVertex(pbox.boxIndex1);// + cg; //corner + p2 = GetVertex(pbox.boxIndex2);// + cg; + p3 = GetVertex(pbox.boxIndex3);// + cg; + p4 = GetVertex(pbox.boxIndex4);// + cg; + a1 = ((p3 + p4) - (p1 + p2)) * 0.5f; + a2 = p3 - (p1 + a1); + a3 = p4 - (p1 + a1); + Vector3 bs = new Vector3(a1.Length(), a2.Length(), a3.Length()); + Vector3 m1 = a1 / bs.X; + Vector3 m2 = a2 / bs.Y; + Vector3 m3 = a3 / bs.Z; + if ((bs.X < bs.Y) && (bs.X < bs.Z)) m1 = Vector3.Cross(m2, m3); + else if (bs.Y < bs.Z) m2 = Vector3.Cross(m3, m1); + else m3 = Vector3.Cross(m1, m2); + Vector3 tp = rp - (p1 + cg); + rayt.Position = new Vector3(Vector3.Dot(tp, m1), Vector3.Dot(tp, m2), Vector3.Dot(tp, m3)); + rayt.Direction = new Vector3(Vector3.Dot(rd, m1), Vector3.Dot(rd, m2), Vector3.Dot(rd, m3)); + box.Minimum = Vector3.Zero; + box.Maximum = bs; + polyhit = rayt.Intersects(ref box, out polyhittestdist); + if (polyhit) + { + Vector3 hpt = rayt.Position + rayt.Direction * polyhittestdist; + const float eps = 0.002f; + if (Math.Abs(hpt.X) < eps) n1 = -m1; + else if (Math.Abs(hpt.X - bs.X) < eps) n1 = m1; + else if (Math.Abs(hpt.Y) < eps) n1 = -m2; + else if (Math.Abs(hpt.Y - bs.Y) < eps) n1 = m2; + else if (Math.Abs(hpt.Z) < eps) n1 = -m3; + else if (Math.Abs(hpt.Z - bs.Z) < eps) n1 = m3; + else + { n1 = Vector3.UnitZ; } //ray starts inside the box... + } + break; + case BoundPolygonType.Cylinder: + var pcyl = polygon as BoundPolygonCylinder; + var tcyl = new BoundingCylinder(); + tcyl.PointA = GetVertex(pcyl.cylinderIndex1) + cg; + tcyl.PointB = GetVertex(pcyl.cylinderIndex2) + cg; + tcyl.Radius = pcyl.cylinderRadius; + polyhit = ray.Intersects(ref tcyl, out polyhittestdist, out n1); + if (polyhit) n1.Normalize(); + break; + default: + break; + } + if (polyhit && (polyhittestdist < res.HitDist) && (polyhittestdist < maxdist)) + { + res.HitDist = polyhittestdist; + res.Hit = true; + res.Normal = n1; + res.HitPolygon = polygon; + + byte matind = ((PolygonMaterialIndices != null) && (p < PolygonMaterialIndices.Length)) ? PolygonMaterialIndices[p] : (byte)0; + BoundMaterial_s mat = ((Materials != null) && (matind < Materials.Length)) ? Materials[matind] : new BoundMaterial_s(); + res.Material = mat; + } + res.TestedPolyCount++; + } + } + nodeind++; + } + res.TestedNodeCount++; + } + } + + return res; + } + } [TC(typeof(EXP))] public class BoundComposite : Bounds { @@ -1334,6 +1738,69 @@ namespace CodeWalker.GameFiles if (BVH != null) list.Add(BVH); return list.ToArray(); } + + + + + public override SpaceSphereIntersectResult SphereIntersect(ref BoundingSphere sph) + { + var res = new SpaceSphereIntersectResult(); + + var compchilds = Children?.data_items; + if (compchilds == null) + { return res; } + + for (int i = 0; i < compchilds.Length; i++) + { + var c = compchilds[i]; + + var chit = c.SphereIntersect(ref sph); + if (chit.Hit) + { + res.Hit = true; + res.HitPolygon = chit.HitPolygon; + res.Normal = chit.Normal; + } + res.TestedPolyCount += chit.TestedPolyCount; + res.TestedNodeCount += chit.TestedNodeCount; + } + + res.TestComplete = true; + + return res; + } + + public override SpaceRayIntersectResult RayIntersect(ref Ray ray, float maxdist = float.MaxValue, float itemhitdist = float.MaxValue) + { + var res = new SpaceRayIntersectResult(); + res.HitDist = itemhitdist; + + var compchilds = Children?.data_items; + if (compchilds == null) + { return res; } + for (int i = 0; i < compchilds.Length; i++) + { + Bounds c = compchilds[i]; + if (c == null) + { continue; } + + var bghit = c.RayIntersect(ref ray, maxdist, res.HitDist); + if (bghit.Hit) + { + res.Hit = true; + res.HitDist = bghit.HitDist; + res.HitPolygon = bghit.HitPolygon; + res.Material = bghit.Material; + res.Normal = bghit.Normal; + } + res.TestedNodeCount += bghit.TestedNodeCount; + res.TestedPolyCount += bghit.TestedPolyCount; + + } + + return res; + } + } [Flags] public enum EBoundCompositeFlags diff --git a/CodeWalker.Core/Utils/Vectors.cs b/CodeWalker.Core/Utils/Vectors.cs index e86d1f4..5a78ada 100644 --- a/CodeWalker.Core/Utils/Vectors.cs +++ b/CodeWalker.Core/Utils/Vectors.cs @@ -96,4 +96,200 @@ namespace CodeWalker } } + + + + + + public struct BoundingCapsule + { + public Vector3 PointA; + public Vector3 PointB; + public float Radius; + } + + public static class BoundingCapsuleMath + { + + public static bool Intersects(this Ray r, ref BoundingCapsule capsule, out float dist) + { + // intersect capsule : http://www.iquilezles.org/www/articles/intersectors/intersectors.htm + Vector3 ba = capsule.PointB - capsule.PointA; + Vector3 oa = r.Position - capsule.PointA; + float baba = Vector3.Dot(ba,ba); + float bard = Vector3.Dot(ba,r.Direction); + float baoa = Vector3.Dot(ba,oa); + float rdoa = Vector3.Dot(r.Direction,oa); + float oaoa = Vector3.Dot(oa,oa); + + float r2 = capsule.Radius * capsule.Radius; + float a = baba - bard*bard; + float b = baba*rdoa - baoa*bard; + float c = baba*oaoa - baoa*baoa - r2*baba; + float h = b*b - a*c; + + if( h>=0.0f ) + { + float t = (-b-(float)Math.Sqrt(h))/a; + + float y = baoa + t*bard; + + // body + if (y > 0.0f && y < baba) + { + dist = t; + return true; + } + + // caps + Vector3 oc = (y<=0.0f) ? oa : r.Position - capsule.PointB; + b = Vector3.Dot(r.Direction,oc); + c = Vector3.Dot(oc,oc) - r2; + h = b*b - c; + if( h>0.0f ) + { + dist = -b - (float)Math.Sqrt(h); + return true; + } + } + dist = -1.0f; + return false; + } + public static Vector3 Normal(this BoundingCapsule c, ref Vector3 position) + { + Vector3 ba = c.PointB - c.PointA; + Vector3 pa = position - c.PointA; + float h = Math.Min(Math.Max(Vector3.Dot(pa,ba)/Vector3.Dot(ba,ba),0.0f),1.0f); + return (pa - h*ba)/c.Radius; + } + + + public static bool Intersects(this BoundingSphere sph, ref BoundingCapsule capsule, out Vector3 norm) + { + var dist = LineMath.PointSegmentDistance(ref sph.Center, ref capsule.PointA, ref capsule.PointB); + var rads = sph.Radius + capsule.Radius; + if (dist <= rads) + { + norm = LineMath.PointSegmentNormal(ref sph.Center, ref capsule.PointA, ref capsule.PointB); + return true; + } + else + { + norm = Vector3.Up; + return false; + } + } + + } + + + + public struct BoundingCylinder + { + public Vector3 PointA; + public Vector3 PointB; + public float Radius; + } + public static class BoundingCylinderMath + { + + public static bool Intersects(this Ray r, ref BoundingCylinder cylinder, out float dist, out Vector3 norm) + { + // intersect cylinder : https://www.shadertoy.com/view/4lcSRn + Vector3 ba = cylinder.PointB - cylinder.PointA; + Vector3 oc = r.Position - cylinder.PointA; + float baba = Vector3.Dot(ba, ba); + float bard = Vector3.Dot(ba, r.Direction); + float baoc = Vector3.Dot(ba, oc); + + float r2 = cylinder.Radius * cylinder.Radius; + float k2 = baba - bard * bard; + float k1 = baba * Vector3.Dot(oc, r.Direction) - baoc * bard; + float k0 = baba * Vector3.Dot(oc, oc) - baoc * baoc - r2 * baba; + + float h = k1 * k1 - k2 * k0; + if (h < 0.0f) + { + dist = -1.0f; + norm = Vector3.Up; + return false; + } + h = (float)Math.Sqrt(h); + float t = (-k1 - h) / k2; + + // body + float y = baoc + t * bard; + if (y > 0.0f && y < baba) + { + dist = t; + norm = (oc + t * r.Direction - ba * y / baba) / cylinder.Radius; + return true; + } + + // caps + t = (((y < 0.0f) ? 0.0f : baba) - baoc) / bard; + if (Math.Abs(k1 + k2 * t) < h) + { + dist = t; + norm = ba * Math.Sign(y) / baba; + return true; + } + + dist = -1.0f; + norm = Vector3.Up; + return false; + } + + } + + + + public static class LineMath + { + + + public static float PointSegmentDistance(ref Vector3 v, ref Vector3 a, ref Vector3 b) + { + //https://stackoverflow.com/questions/4858264/find-the-distance-from-a-3d-point-to-a-line-segment + Vector3 ab = b - a; + Vector3 av = v - a; + + if (Vector3.Dot(av, ab) <= 0.0f)// Point is lagging behind start of the segment, so perpendicular distance is not viable. + { + return av.Length(); // Use distance to start of segment instead. + } + + Vector3 bv = v - b; + if (Vector3.Dot(bv, ab) >= 0.0f)// Point is advanced past the end of the segment, so perpendicular distance is not viable. + { + return bv.Length(); // Use distance to end of the segment instead. + } + + return Vector3.Cross(ab, av).Length() / ab.Length();// Perpendicular distance of point to segment. + } + + public static Vector3 PointSegmentNormal(ref Vector3 v, ref Vector3 a, ref Vector3 b) + { + Vector3 ab = b - a; + Vector3 av = v - a; + + if (Vector3.Dot(av, ab) <= 0.0f) + { + return Vector3.Normalize(av); + } + + Vector3 bv = v - b; + if (Vector3.Dot(bv, ab) >= 0.0f) + { + return Vector3.Normalize(bv); + } + + return Vector3.Normalize(Vector3.Cross(Vector3.Cross(ab, av), ab)); + } + + + } + + + } \ No newline at end of file diff --git a/CodeWalker.Core/World/Space.cs b/CodeWalker.Core/World/Space.cs index 1ae815f..9afe85d 100644 --- a/CodeWalker.Core/World/Space.cs +++ b/CodeWalker.Core/World/Space.cs @@ -860,14 +860,14 @@ namespace CodeWalker.World if (!r.SphereHit.Hit) { - if (absdisp > (e.Radius * 2.0f)) //fast-moving... do a ray test to make sure it's not tunnelling + if (absdisp > e.Radius) //fast-moving... do a ray test to make sure it's not tunnelling { Ray rayt = new Ray(sphpos, r.HitVelDir); float rayl = absdisp + e.Radius * 4.0f; //include some extra incase of glancing hit var rayhit = RayIntersect(rayt, rayl); if (rayhit.Hit) //looks like it is tunnelling... need to find the sphere hit point { - sph.Center = rayhit.Position; + sph.Center = rayhit.Position - (r.HitVelDir*Math.Min(e.Radius*0.5f, rayhit.HitDist)); float hitd = rayhit.HitDist; r.HitT = hitd / absdisp; if (r.HitT > 1.0f) @@ -1066,24 +1066,9 @@ namespace CodeWalker.World { var res = new SpaceRayIntersectResult(); if (GameFileCache == null) return res; - int polytestcount = 0; - int nodetestcount = 0; bool testcomplete = true; + res.HitDist = float.MaxValue; var box = new BoundingBox(); - var tsph = new BoundingSphere(); - var rayt = new Ray(); - var rp = ray.Position; - var rd = ray.Direction; - var boxhitdist = float.MaxValue; - var itemhitdist = float.MaxValue; - Vector3 p1, p2, p3, p4, a1, a2, a3; - Vector3 n1 = Vector3.Zero; - float polyhittestdist = 0; - bool hit = false; - BoundPolygon hitpoly = null; - BoundMaterial_s hitmat = new BoundMaterial_s(); - Vector3 hitnorm = Vector3.Zero; - Vector3 hitpos = Vector3.Zero; if (BoundsStore == null) return res; var boundslist = BoundsStore.GetItems(ref ray); @@ -1101,216 +1086,51 @@ namespace CodeWalker.World float boxhitdisttest; if (ray.Intersects(ref box, out boxhitdisttest)) { + if (boxhitdisttest > res.HitDist) + { continue; } //already a closer hit + if (boxhitdisttest > maxdist) + { continue; } + YbnFile ybn = GameFileCache.GetYbn(bound.Name); if (ybn == null) { continue; } //ybn not found? if (!ybn.Loaded) { testcomplete = false; continue; } //ybn not loaded yet... - bool ybnhit = false; var b = ybn.Bounds; - box.Minimum = b.BoundingBoxMin; - box.Maximum = b.BoundingBoxMax; - float itemboxhitdisttest; - if (!ray.Intersects(ref box, out itemboxhitdisttest)) - { continue; } //ray doesn't hit this ybn - if (itemboxhitdisttest > itemhitdist) - { continue; } //already a closer hit. - if (itemboxhitdisttest > maxdist) - { continue; } - switch (b.Type) + //box.Minimum = b.BoundingBoxMin; + //box.Maximum = b.BoundingBoxMax; + //float itemboxhitdisttest; + //if (!ray.Intersects(ref box, out itemboxhitdisttest)) + //{ continue; } //ray doesn't hit this ybn + //if (itemboxhitdisttest > res.HitDist) + //{ continue; } //already a closer hit. + //if (itemboxhitdisttest > maxdist) + //{ continue; } + + var bhit = b.RayIntersect(ref ray, maxdist, res.HitDist); + if (bhit.Hit) { - case 10: //BoundComposite - BoundComposite boundcomp = b as BoundComposite; - if (boundcomp == null) - { continue; } - var compchilds = boundcomp.Children?.data_items; - if (compchilds == null) - { continue; } - for (int i = 0; i < compchilds.Length; i++) - { - BoundBVH bgeom = compchilds[i] as BoundBVH; - if (bgeom == null) - { continue; } - if (bgeom.Polygons == null) - { continue; } - if ((bgeom.BVH?.Nodes?.data_items == null) || (bgeom.BVH?.Trees?.data_items == null)) - { continue; } - - box.Minimum = bgeom.BoundingBoxMin; - box.Maximum = bgeom.BoundingBoxMax; - float bvhboxhittest; - if (!ray.Intersects(ref box, out bvhboxhittest)) - { continue; } - if (bvhboxhittest > itemhitdist) - { continue; } //already a closer hit. - - var q = bgeom.BVH.Quantum.XYZ(); - var c = bgeom.BVH.BoundingBoxCenter.XYZ(); - var cg = bgeom.CenterGeom; - for (int t = 0; t < bgeom.BVH.Trees.data_items.Length; t++) - { - var tree = bgeom.BVH.Trees.data_items[t]; - box.Minimum = new Vector3(tree.MinX, tree.MinY, tree.MinZ) * q + c; - box.Maximum = new Vector3(tree.MaxX, tree.MaxY, tree.MaxZ) * q + c; - if (!ray.Intersects(ref box, out bvhboxhittest)) - { continue; } - if (bvhboxhittest > itemhitdist) - { continue; } //already a closer hit. - if (bvhboxhittest > maxdist) - { continue; } - - int nodeind = tree.NodeIndex1; - int lastind = tree.NodeIndex2; - while (nodeind < lastind) - { - var node = bgeom.BVH.Nodes.data_items[nodeind]; - box.Minimum = new Vector3(node.MinX, node.MinY, node.MinZ) * q + c; - box.Maximum = new Vector3(node.MaxX, node.MaxY, node.MaxZ) * q + c; - bool nodehit = ray.Intersects(ref box, out bvhboxhittest); - bool nodeskip = !nodehit || (bvhboxhittest > itemhitdist); - if (node.PolyCount <= 0) //intermediate node with child nodes - { - if (nodeskip) - { - nodeind += node.PolyId; //(child node count) - } - else - { - nodeind++; - } - } - else //leaf node, with polygons - { - if (!nodeskip) - { - var lastp = node.PolyId + node.PolyCount; - lastp = Math.Min(lastp, (int)bgeom.PolygonsCount); - for (int p = node.PolyId; p < lastp; p++) - { - var polygon = bgeom.Polygons[p]; - bool polyhit = false; - switch (polygon.Type) - { - case BoundPolygonType.Triangle: - var ptri = polygon as BoundPolygonTriangle; - p1 = bgeom.GetVertex(ptri.vertIndex1) + cg; - p2 = bgeom.GetVertex(ptri.vertIndex2) + cg; - p3 = bgeom.GetVertex(ptri.vertIndex3) + cg; - polyhit = ray.Intersects(ref p1, ref p2, ref p3, out polyhittestdist); - if (polyhit) n1 = Vector3.Normalize(Vector3.Cross(p2 - p1, p3 - p1)); - break; - case BoundPolygonType.Sphere: - var psph = polygon as BoundPolygonSphere; - tsph.Center = bgeom.GetVertex(psph.sphereIndex) + cg; - tsph.Radius = psph.sphereRadius; - polyhit = ray.Intersects(ref tsph, out polyhittestdist); - if (polyhit) n1 = Vector3.Normalize((ray.Position + ray.Direction * polyhittestdist) - tsph.Center); - break; - case BoundPolygonType.Capsule: - //TODO - break; - case BoundPolygonType.Box: - var pbox = polygon as BoundPolygonBox; - p1 = bgeom.GetVertex(pbox.boxIndex1);// + cg; //corner - p2 = bgeom.GetVertex(pbox.boxIndex2);// + cg; - p3 = bgeom.GetVertex(pbox.boxIndex3);// + cg; - p4 = bgeom.GetVertex(pbox.boxIndex4);// + cg; - a1 = ((p3 + p4) - (p1 + p2)) * 0.5f; - a2 = p3 - (p1 + a1); - a3 = p4 - (p1 + a1); - Vector3 bs = new Vector3(a1.Length(), a2.Length(), a3.Length()); - Vector3 m1 = a1 / bs.X; - Vector3 m2 = a2 / bs.Y; - Vector3 m3 = a3 / bs.Z; - if ((bs.X < bs.Y) && (bs.X < bs.Z)) m1 = Vector3.Cross(m2, m3); - else if (bs.Y < bs.Z) m2 = Vector3.Cross(m1, m3); - else m3 = Vector3.Cross(m1, m2); - Vector3 tp = rp - (p1 + cg); - rayt.Position = new Vector3(Vector3.Dot(tp, m1), Vector3.Dot(tp, m2), Vector3.Dot(tp, m3)); - rayt.Direction = new Vector3(Vector3.Dot(rd, m1), Vector3.Dot(rd, m2), Vector3.Dot(rd, m3)); - box.Minimum = Vector3.Zero; - box.Maximum = bs; - polyhit = rayt.Intersects(ref box, out polyhittestdist); - if (polyhit) - { - Vector3 hpt = rayt.Position + rayt.Direction * polyhittestdist; - const float eps = 0.002f; - if (Math.Abs(hpt.X) < eps) n1 = -m1; - else if (Math.Abs(hpt.X - bs.X) < eps) n1 = m1; - else if (Math.Abs(hpt.Y) < eps) n1 = -m2; - else if (Math.Abs(hpt.Y - bs.Y) < eps) n1 = m2; - else if (Math.Abs(hpt.Z) < eps) n1 = -m3; - else if (Math.Abs(hpt.Z - bs.Z) < eps) n1 = m3; - else - { n1 = Vector3.UnitZ; } //ray starts inside the box... - } - break; - case BoundPolygonType.Cylinder: - //TODO - break; - } - if (polyhit && (polyhittestdist < itemhitdist) && (polyhittestdist < maxdist)) - { - itemhitdist = polyhittestdist; - ybnhit = true; - hit = true; - hitnorm = n1; - hitpoly = polygon; - - byte matind = ((bgeom.PolygonMaterialIndices != null) && (p < bgeom.PolygonMaterialIndices.Length)) ? bgeom.PolygonMaterialIndices[p] : (byte)0; - BoundMaterial_s mat = ((bgeom.Materials != null) && (matind < bgeom.Materials.Length)) ? bgeom.Materials[matind] : new BoundMaterial_s(); - hitmat = mat; - } - polytestcount++; - } - } - nodeind++; - } - nodetestcount++; - } - } - } - break; - case 3: //BoundBox - found in drawables - TODO - BoundBox boundbox = b as BoundBox; - if (boundbox == null) - { continue; } - break; - case 0: //BoundSphere - found in drawables - TODO - BoundSphere boundsphere = b as BoundSphere; - if (boundsphere == null) - { continue; } - break; - default: - break; + res.Hit = true; + res.HitDist = bhit.HitDist; + res.HitPolygon = bhit.HitPolygon; + res.Material = bhit.Material; + res.Normal = bhit.Normal; } - - if (ybnhit) - { - boxhitdist = boxhitdisttest; - //hit = true; - } + res.TestedNodeCount += bhit.TestedNodeCount; + res.TestedPolyCount += bhit.TestedPolyCount; } } - if (hit) + if (res.Hit) { - hitpos = ray.Position + ray.Direction * itemhitdist; + res.Position = ray.Position + ray.Direction * res.HitDist; } - res.TestedNodeCount = nodetestcount; - res.TestedPolyCount = polytestcount; res.TestComplete = testcomplete; - res.Hit = hit; - res.HitDist = itemhitdist; - res.HitPolygon = hitpoly; - res.Material = hitmat; - res.Position = hitpos; - res.Normal = hitnorm; return res; } @@ -1325,20 +1145,6 @@ namespace CodeWalker.World Vector3 sphmin = sph.Center - sph.Radius; Vector3 sphmax = sph.Center + sph.Radius; var box = new BoundingBox(); - var tsph = new BoundingSphere(); - var spht = new BoundingSphere(); - var sp = sph.Center; - var sr = sph.Radius; - //var boxhitdist = float.MaxValue; - //var itemhitdist = float.MaxValue; - Vector3 p1, p2, p3, p4, a1, a2, a3; - Vector3 n1 = Vector3.Zero; - //float polyhittestdist = 0; - bool hit = false; - BoundPolygon hitpoly = null; - Vector3 hitnorm = Vector3.Zero; - Vector3 hitpos = Vector3.Zero; - if (BoundsStore == null) return res; var boundslist = BoundsStore.GetItems(ref sphmin, ref sphmax); @@ -1356,184 +1162,25 @@ namespace CodeWalker.World if (!ybn.Loaded) { testcomplete = false; continue; } //ybn not loaded yet... - //bool ybnhit = false; var b = ybn.Bounds; - box.Minimum = b.BoundingBoxMin; - box.Maximum = b.BoundingBoxMax; - if (!sph.Intersects(ref box)) - { continue; } //ray doesn't hit this ybn - switch (b.Type) + //box.Minimum = b.BoundingBoxMin; + //box.Maximum = b.BoundingBoxMax; + //if (!sph.Intersects(ref box)) + //{ continue; } //ray doesn't hit this ybn + + var bhit = b.SphereIntersect(ref sph); + if (bhit.Hit) { - case 10: //BoundComposite - BoundComposite boundcomp = b as BoundComposite; - if (boundcomp == null) - { continue; } - var compchilds = boundcomp.Children?.data_items; - if (compchilds == null) - { continue; } - for (int i = 0; i < compchilds.Length; i++) - { - BoundBVH bgeom = compchilds[i] as BoundBVH; - if (bgeom == null) - { continue; } - if (bgeom.Polygons == null) - { continue; } - if ((bgeom.BVH?.Nodes?.data_items == null) || (bgeom.BVH?.Trees?.data_items == null)) - { continue; } - - box.Minimum = bgeom.BoundingBoxMin; - box.Maximum = bgeom.BoundingBoxMax; - if (!sph.Intersects(ref box)) - { continue; } - - var q = bgeom.BVH.Quantum.XYZ(); - var c = bgeom.BVH.BoundingBoxCenter.XYZ(); - var cg = bgeom.CenterGeom; - for (int t = 0; t < bgeom.BVH.Trees.data_items.Length; t++) - { - var tree = bgeom.BVH.Trees.data_items[t]; - box.Minimum = new Vector3(tree.MinX, tree.MinY, tree.MinZ) * q + c; - box.Maximum = new Vector3(tree.MaxX, tree.MaxY, tree.MaxZ) * q + c; - if (!sph.Intersects(ref box)) - { continue; } - - int nodeind = tree.NodeIndex1; - int lastind = tree.NodeIndex2; - while (nodeind < lastind) - { - var node = bgeom.BVH.Nodes.data_items[nodeind]; - box.Minimum = new Vector3(node.MinX, node.MinY, node.MinZ) * q + c; - box.Maximum = new Vector3(node.MaxX, node.MaxY, node.MaxZ) * q + c; - bool nodehit = sph.Intersects(ref box); - bool nodeskip = !nodehit; - if (node.PolyCount <= 0) //intermediate node with child nodes - { - if (nodeskip) - { - nodeind += node.PolyId; //(child node count) - } - else - { - nodeind++; - } - } - else //leaf node, with polygons - { - if (!nodeskip) - { - var lastp = node.PolyId + node.PolyCount; - lastp = Math.Min(lastp, (int)bgeom.PolygonsCount); - for (int p = node.PolyId; p < lastp; p++) - { - var polygon = bgeom.Polygons[p]; - bool polyhit = false; - switch (polygon.Type) - { - case BoundPolygonType.Triangle: - var ptri = polygon as BoundPolygonTriangle; - p1 = bgeom.GetVertex(ptri.vertIndex1) + cg; - p2 = bgeom.GetVertex(ptri.vertIndex2) + cg; - p3 = bgeom.GetVertex(ptri.vertIndex3) + cg; - polyhit = sph.Intersects(ref p1, ref p2, ref p3); - if (polyhit) n1 = Vector3.Normalize(Vector3.Cross(p2 - p1, p3 - p1)); - break; - case BoundPolygonType.Sphere: - var psph = polygon as BoundPolygonSphere; - tsph.Center = bgeom.GetVertex(psph.sphereIndex) + cg; - tsph.Radius = psph.sphereRadius; - polyhit = sph.Intersects(ref tsph); - if (polyhit) n1 = Vector3.Normalize(sph.Center - tsph.Center); - break; - case BoundPolygonType.Capsule: - //TODO - break; - case BoundPolygonType.Box: - var pbox = polygon as BoundPolygonBox; - p1 = bgeom.GetVertex(pbox.boxIndex1);// + cg; //corner - p2 = bgeom.GetVertex(pbox.boxIndex2);// + cg; - p3 = bgeom.GetVertex(pbox.boxIndex3);// + cg; - p4 = bgeom.GetVertex(pbox.boxIndex4);// + cg; - a1 = ((p3 + p4) - (p1 + p2)) * 0.5f; - a2 = p3 - (p1 + a1); - a3 = p4 - (p1 + a1); - Vector3 bs = new Vector3(a1.Length(), a2.Length(), a3.Length()); - Vector3 m1 = a1 / bs.X; - Vector3 m2 = a2 / bs.Y; - Vector3 m3 = a3 / bs.Z; - if ((bs.X < bs.Y) && (bs.X < bs.Z)) m1 = Vector3.Cross(m2, m3); - else if (bs.Y < bs.Z) m2 = Vector3.Cross(m1, m3); - else m3 = Vector3.Cross(m1, m2); - Vector3 tp = sp - (p1 + cg); - spht.Center = new Vector3(Vector3.Dot(tp, m1), Vector3.Dot(tp, m2), Vector3.Dot(tp, m3)); - spht.Radius = sph.Radius; - box.Minimum = Vector3.Zero; - box.Maximum = bs; - polyhit = spht.Intersects(ref box); - if (polyhit) - { - Vector3 smin = spht.Center - spht.Radius; - Vector3 smax = spht.Center + spht.Radius; - float eps = spht.Radius * 0.8f; - n1 = Vector3.Zero; - if (Math.Abs(smax.X) < eps) n1 -= m1; - else if (Math.Abs(smin.X - bs.X) < eps) n1 += m1; - if (Math.Abs(smax.Y) < eps) n1 -= m2; - else if (Math.Abs(smin.Y - bs.Y) < eps) n1 += m2; - if (Math.Abs(smax.Z) < eps) n1 -= m3; - else if (Math.Abs(smin.Z - bs.Z) < eps) n1 += m3; - float n1l = n1.Length(); - if (n1l > 0.0f) n1 = n1 / n1l; - else n1 = Vector3.UnitZ; - } - break; - case BoundPolygonType.Cylinder: - //TODO - break; - } - if (polyhit) // && (polyhittestdist < itemhitdist) && (polyhittestdist < maxdist)) - { - hitpoly = polygon; - //itemhitdist = polyhittestdist; - //ybnhit = true; - hit = true; - hitnorm = n1; - } - polytestcount++; - } - } - nodeind++; - } - nodetestcount++; - } - } - } - break; - case 3: //BoundBox - found in drawables - TODO - BoundBox boundbox = b as BoundBox; - if (boundbox == null) - { continue; } - break; - case 0: //BoundSphere - found in drawables - TODO - BoundSphere boundsphere = b as BoundSphere; - if (boundsphere == null) - { continue; } - break; - default: - break; + res.Hit = true; + res.HitPolygon = bhit.HitPolygon; + res.Normal = bhit.Normal; } - - - //if (ybnhit) - //{ - // //boxhitdist = boxhitdisttest; - // //hit = true; - //} - + polytestcount += bhit.TestedPolyCount; + nodetestcount += bhit.TestedNodeCount; } } - //if (hit) //{ // hitpos = ray.Position + ray.Direction * itemhitdist; @@ -1542,11 +1189,6 @@ namespace CodeWalker.World res.TestedNodeCount = nodetestcount; res.TestedPolyCount = polytestcount; res.TestComplete = testcomplete; - res.Hit = hit; - res.HitDist = 0;// itemhitdist; - res.HitPolygon = hitpoly; - res.Position = hitpos; - res.Normal = hitnorm; return res; } diff --git a/WorldForm.cs b/WorldForm.cs index 1215d59..b2781fb 100644 --- a/WorldForm.cs +++ b/WorldForm.cs @@ -154,7 +154,7 @@ namespace CodeWalker MapSelection PrevMouseHit = new MapSelection(); bool MouseRayCollisionEnabled = true; - bool MouseRayCollisionVisible = true; + bool MouseRayCollisionVisible = false; SpaceRayIntersectResult MouseRayCollision = new SpaceRayIntersectResult(); string SelectionModeStr = "Entity"; @@ -628,7 +628,7 @@ namespace CodeWalker Vector3 move = lftxy * movecontrol.X + fwdxy * movecontrol.Y; Vector2 movexy = new Vector2(move.X, move.Y); - movexy *= (1.0f + (Input.xblt * 15.0f)); //boost with left trigger + movexy *= (1.0f + (Math.Min(Math.Max(Input.xblt, 0.0f), 1.0f) * 15.0f)); //boost with left trigger pedEntity.ControlMovement = movexy; pedEntity.ControlJump = Input.kbjump || Input.ControllerButtonPressed(GamepadButtonFlags.X); @@ -1281,13 +1281,15 @@ namespace CodeWalker const uint caqu = 4294967040;// (uint)new Color4(0.0f, 1.0f, 1.0f, 1.0f).ToRgba(); //const uint cyel = 4278255615;// - if (MouseRayCollisionEnabled && MouseRayCollisionVisible) + if (ControlBrushEnabled && MouseRayCollision.Hit) { - if (MouseRayCollision.Hit) - { - var arup = GetPerpVec(MouseRayCollision.Normal); - Renderer.RenderBrushRadiusOutline(MouseRayCollision.Position, MouseRayCollision.Normal, arup, ProjectForm.GetInstanceBrushRadius(), cgrn); - } + var arup = GetPerpVec(MouseRayCollision.Normal); + Renderer.RenderBrushRadiusOutline(MouseRayCollision.Position, MouseRayCollision.Normal, arup, ProjectForm.GetInstanceBrushRadius(), cgrn); + } + if (MouseRayCollisionVisible && MouseRayCollision.Hit) + { + var arup = GetPerpVec(MouseRayCollision.Normal); + Renderer.RenderSelectionArrowOutline(MouseRayCollision.Position, MouseRayCollision.Normal, arup, Quaternion.Identity, 1.0f, 0.05f, cgrn); } if (!ShowSelectionBounds) @@ -2200,13 +2202,21 @@ namespace CodeWalker if (Input.CtrlPressed && ProjectForm != null && ProjectForm.CanPaintInstances()) { ControlBrushEnabled = true; - MouseRayCollisionEnabled = true; + MouseRayCollisionVisible = false; MouseRayCollision = GetSpaceMouseRay(); } - else if (MouseRayCollisionEnabled) + else { ControlBrushEnabled = false; - MouseRayCollisionEnabled = false; + if (Input.CtrlPressed && MouseRayCollisionEnabled) + { + MouseRayCollisionVisible = true; + MouseRayCollision = GetSpaceMouseRay(); + } + else + { + MouseRayCollisionVisible = false; + } }