diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/Line3DColliderTests.cs b/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/Line3DColliderTests.cs new file mode 100644 index 000000000..6bd329ab8 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/Line3DColliderTests.cs @@ -0,0 +1,47 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using NUnit.Framework; +using Unity.Mathematics; + +namespace VisualPinball.Unity.Test +{ + public class Line3DColliderTests + { + [Test] + public void HitNormalIsTransformedBackToWorldSpace() + { + var collider = new Line3DCollider( + new float3(0f, 0f, 0f), + new float3(10f, 0f, 0f), + new ColliderInfo { ItemId = 1 } + ); + var ball = new BallState { + Position = new float3(5f, 2f, 2f), + Velocity = new float3(0f, -1f, -1f), + Radius = 1f + }; + var collEvent = new CollisionEventData(); + + var hitTime = collider.HitTest(ref collEvent, in ball, 2f); + + Assert.That(hitTime, Is.GreaterThanOrEqualTo(0f)); + Assert.That(collEvent.HitNormal.x, Is.EqualTo(0f).Within(1e-5f)); + Assert.That(collEvent.HitNormal.y, Is.EqualTo(math.sqrt(0.5f)).Within(1e-5f)); + Assert.That(collEvent.HitNormal.z, Is.EqualTo(math.sqrt(0.5f)).Within(1e-5f)); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/Line3DColliderTests.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/Line3DColliderTests.cs.meta new file mode 100644 index 000000000..690490f87 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/Line3DColliderTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7c64a52806928274b8b974b311efc219 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/PhysicsRegressionTests.cs b/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/PhysicsRegressionTests.cs new file mode 100644 index 000000000..ce2a00dc4 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/PhysicsRegressionTests.cs @@ -0,0 +1,327 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Reflection; +using System.Reflection.Emit; +using NUnit.Framework; +using Unity.Collections; +using Unity.Mathematics; +using VisualPinball.Engine.VPT; + +namespace VisualPinball.Unity.Test +{ + /// + /// Regression coverage for physics transform and collision bugs. + /// + public class PhysicsRegressionTests + { + private const float Tolerance = 1e-5f; + + private static readonly OpCode[] OneByteOpCodes = new OpCode[0x100]; + private static readonly OpCode[] TwoByteOpCodes = new OpCode[0x100]; + + static PhysicsRegressionTests() + { + foreach (var field in typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static)) { + var opCode = (OpCode)field.GetValue(null); + var value = (ushort)opCode.Value; + if (value < 0x100) { + OneByteOpCodes[value] = opCode; + } else if ((value & 0xff00) == 0xfe00) { + TwoByteOpCodes[value & 0xff] = opCode; + } + } + } + + [Test] + public void PointColliderTransformKeepsBoundsAtTransformedPoint() + { + var collider = new PointCollider(new float3(1f, 2f, 3f), new ColliderInfo { ItemId = 1 }); + var matrix = float4x4.Translate(new float3(10f, 20f, 30f)); + + collider = collider.Transform(matrix); + + AssertFloat3(collider.P, new float3(11f, 22f, 33f)); + AssertFloat3(collider.Bounds.Aabb.Min, collider.P); + AssertFloat3(collider.Bounds.Aabb.Max, collider.P); + } + + [Test] + public void LineZColliderTransformAppliesFullMatrixToBothEndpoints() + { + var source = new LineZCollider(new float2(1f, 2f), 10f, 20f, new ColliderInfo { ItemId = 1 }); + var matrix = float4x4.TRS( + new float3(10f, 20f, 30f), + quaternion.RotateZ(math.radians(90f)), + new float3(2f, 3f, 4f) + ); + var expectedLow = matrix.MultiplyPoint(new float3(source.XY, source.ZLow)); + var expectedHigh = matrix.MultiplyPoint(new float3(source.XY, source.ZHigh)); + + var transformed = source.Transform(matrix); + + AssertFloat2(transformed.XY, expectedLow.xy); + Assert.That(transformed.ZLow, Is.EqualTo(expectedLow.z).Within(Tolerance)); + Assert.That(transformed.ZHigh, Is.EqualTo(expectedHigh.z).Within(Tolerance)); + } + + [Test] + public void GetScaleReturnsTheTrsAxisScaleAfterRotation() + { + var matrix = RotatedNonUniformScale(); + + AssertFloat3(matrix.GetScale(), new float3(2f, 1f, 3f)); + } + + [Test] + public void CircleColliderRejectsRotatedNonUniformXyScale() + { + Assert.That(CircleCollider.IsTransformable(RotatedNonUniformScale()), Is.False); + } + + [Test] + public void BallTransformScalesRadiusWithUniformScale() + { + var ball = new BallState { + Position = new float3(150f, 0f, 0f), + Radius = 25f + }; + + ball.Transform(float4x4.Scale(new float3(0.5f))); + + AssertFloat3(ball.Position, new float3(75f, 0f, 0f)); + Assert.That(ball.Radius, Is.EqualTo(12.5f).Within(Tolerance)); + } + + [Test] + public void BallTransformRoundTripPreservesRadiusForLargeRotatedUniformScale() + { + var ball = new BallState { Radius = 25f }; + var matrix = float4x4.TRS( + float3.zero, + quaternion.EulerXYZ(math.radians(new float3(10f, 62f, 17f))), + new float3(10f) + ); + + ball.Transform(math.inverse(matrix)); + Assert.That(ball.Radius, Is.EqualTo(2.5f).Within(Tolerance)); + + ball.Transform(matrix); + Assert.That(ball.Radius, Is.EqualTo(25f).Within(Tolerance)); + } + + [Test] + public void CollisionEventTransformRoundTripPreservesHitVelocity() + { + var collEvent = new CollisionEventData { HitVelocity = new float2(0f, 1f) }; + var matrix = float4x4.TRS( + float3.zero, + quaternion.RotateX(math.radians(45f)), + new float3(1f) + ); + + collEvent.Transform(matrix); + collEvent.Transform(math.inverse(matrix)); + + AssertFloat2(collEvent.HitVelocity, new float2(0f, 1f)); + } + + [Test] + public void CollisionEventClearResetsTransformedHitVelocity() + { + var collEvent = new CollisionEventData { HitVelocity = new float2(0f, 1f) }; + var matrix = float4x4.TRS( + float3.zero, + quaternion.RotateX(math.radians(45f)), + new float3(1f) + ); + + collEvent.Transform(matrix); + collEvent.ClearCollider(); + + AssertFloat2(collEvent.HitVelocity, float2.zero); + + collEvent.HitVelocity = new float2(0f, 1f); + collEvent.Transform(matrix); + + AssertFloat2(collEvent.HitVelocity, new float2(0f, math.sqrt(0.5f))); + } + + [Test] + public void Line3DColliderFiresHitEventForApproachingBallAboveThreshold() + { + var collider = new Line3DCollider( + new float3(0f, 0f, -1f), + new float3(0f, 0f, 1f), + new ColliderInfo { + ItemId = 1, + ItemType = ItemType.Primitive, + HitThreshold = 1f, + FireEvents = true + } + ); + var ball = new BallState { + Id = 1, + Position = new float3(1f, 0f, 0f), + EventPosition = new float3(100f, 0f, 0f), + Velocity = new float3(-10f, 0f, 0f), + Radius = 1f, + Mass = 1f + }; + var collEvent = new CollisionEventData { + HitNormal = new float3(1f, 0f, 0f), + HitDistance = 0f + }; + var state = new PhysicsState(); + var events = new NativeQueue(Allocator.Temp); + + try { + var writer = events.AsParallelWriter(); + collider.Collide(ref ball, ref writer, in collEvent, ref state); + + Assert.That(events.Count, Is.EqualTo(1)); + } finally { + events.Dispose(); + } + } + + [TestCase(typeof(Aabb), TestName = "AabbBoxedEqualsDelegatesToTypedOverload")] + [TestCase(typeof(ColliderHeader), TestName = "ColliderHeaderBoxedEqualsDelegatesToTypedOverload")] + public void BoxedEqualsDelegatesToTypedOverload(Type type) + { + var boxedEquals = type.GetMethod( + nameof(object.Equals), + BindingFlags.Instance | BindingFlags.Public, + null, + new[] { typeof(object) }, + null + ); + var typedEquals = type.GetMethod( + nameof(object.Equals), + BindingFlags.Instance | BindingFlags.Public, + null, + new[] { type }, + null + ); + + Assert.That(boxedEquals, Is.Not.Null); + Assert.That(typedEquals, Is.Not.Null); + Assert.That(CallsMethod(boxedEquals, typedEquals), Is.True, + $"{type.Name}.Equals(object) does not delegate to Equals({type.Name})."); + } + + private static float4x4 RotatedNonUniformScale() + { + return float4x4.TRS( + float3.zero, + quaternion.RotateZ(math.radians(45f)), + new float3(2f, 1f, 3f) + ); + } + + private static bool CallsMethod(MethodInfo caller, MethodInfo expectedCallee) + { + var body = caller.GetMethodBody(); + var il = body?.GetILAsByteArray(); + if (il == null) { + return false; + } + + var offset = 0; + while (offset < il.Length) { + var opCode = ReadOpCode(il, ref offset); + if (opCode.OperandType == OperandType.InlineMethod) { + var token = BitConverter.ToInt32(il, offset); + var calledMethod = caller.Module.ResolveMethod(token); + if (HasSameSignature(calledMethod, expectedCallee)) { + return true; + } + } + offset += GetOperandSize(opCode.OperandType, il, offset); + } + return false; + } + + private static bool HasSameSignature(MethodBase actual, MethodInfo expected) + { + if (actual.DeclaringType != expected.DeclaringType || actual.Name != expected.Name) { + return false; + } + var actualParameters = actual.GetParameters(); + var expectedParameters = expected.GetParameters(); + if (actualParameters.Length != expectedParameters.Length) { + return false; + } + for (var i = 0; i < actualParameters.Length; i++) { + if (actualParameters[i].ParameterType != expectedParameters[i].ParameterType) { + return false; + } + } + return true; + } + + private static OpCode ReadOpCode(byte[] il, ref int offset) + { + var value = il[offset++]; + return value == 0xfe ? TwoByteOpCodes[il[offset++]] : OneByteOpCodes[value]; + } + + private static int GetOperandSize(OperandType operandType, byte[] il, int offset) + { + switch (operandType) { + case OperandType.InlineNone: + return 0; + case OperandType.ShortInlineBrTarget: + case OperandType.ShortInlineI: + case OperandType.ShortInlineVar: + return 1; + case OperandType.InlineVar: + return 2; + case OperandType.InlineBrTarget: + case OperandType.InlineField: + case OperandType.InlineI: + case OperandType.InlineMethod: + case OperandType.InlineSig: + case OperandType.InlineString: + case OperandType.InlineTok: + case OperandType.InlineType: + case OperandType.ShortInlineR: + return 4; + case OperandType.InlineI8: + case OperandType.InlineR: + return 8; + case OperandType.InlineSwitch: + return 4 + BitConverter.ToInt32(il, offset) * 4; + default: + throw new ArgumentOutOfRangeException(nameof(operandType), operandType, null); + } + } + + private static void AssertFloat2(float2 actual, float2 expected) + { + Assert.That(actual.x, Is.EqualTo(expected.x).Within(Tolerance)); + Assert.That(actual.y, Is.EqualTo(expected.y).Within(Tolerance)); + } + + private static void AssertFloat3(float3 actual, float3 expected) + { + Assert.That(actual.x, Is.EqualTo(expected.x).Within(Tolerance)); + Assert.That(actual.y, Is.EqualTo(expected.y).Within(Tolerance)); + Assert.That(actual.z, Is.EqualTo(expected.z).Within(Tolerance)); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/PhysicsRegressionTests.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/PhysicsRegressionTests.cs.meta new file mode 100644 index 000000000..b46db8820 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/Physics/PhysicsRegressionTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5a240399f2c24726a78fbe54cf78b518 diff --git a/VisualPinball.Unity/VisualPinball.Unity/Common/AssemblyInfo.cs b/VisualPinball.Unity/VisualPinball.Unity/Common/AssemblyInfo.cs new file mode 100644 index 000000000..52d673487 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Common/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("VisualPinball.Unity.Test")] diff --git a/VisualPinball.Unity/VisualPinball.Unity/Common/AssemblyInfo.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Common/AssemblyInfo.cs.meta new file mode 100644 index 000000000..2744f375a --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Common/AssemblyInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 964d153f72dc44b9996b6c113577e1cc diff --git a/VisualPinball.Unity/VisualPinball.Unity/Extensions/MathExtensions.cs b/VisualPinball.Unity/VisualPinball.Unity/Extensions/MathExtensions.cs index f61445305..363cbb28b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Extensions/MathExtensions.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Extensions/MathExtensions.cs @@ -62,9 +62,9 @@ public static void RotationAroundAxis(ref this float3x3 m, float3 axis, float rS public static float3 GetScale(this float4x4 m) { return new float3( - math.length(new float3(m.c0.x, m.c1.x, m.c2.x)), - math.length(new float3(m.c0.y, m.c1.y, m.c2.y)), - math.length(new float3(m.c0.z, m.c1.z, m.c2.z)) + math.length(m.c0.xyz), + math.length(m.c1.xyz), + math.length(m.c2.xyz) ); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/CircleCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/CircleCollider.cs index 7c39fcb6d..75e5a4f2e 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/CircleCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/CircleCollider.cs @@ -248,7 +248,7 @@ public static bool IsTransformable(float4x4 matrix) var rotation = matrix.GetRotationVector(); // if xy-scale is not uniform or x/y rotation is not zero, we can't transform the collider - var uniformScale = math.abs(scale.x - scale.y) < Collider.Tolerance; + var uniformScale = Collider.HasEqualScale(scale.x, scale.y); var xyRotated = math.abs(rotation.x) > Collider.Tolerance || math.abs(rotation.y) > Collider.Tolerance; return uniformScale && !xyRotated; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Collider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Collider.cs index f0fbfd205..e10246139 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Collider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Collider.cs @@ -30,6 +30,13 @@ public struct Collider { public const float Tolerance = 1e-6f; // 1e-9f; + internal static bool HasEqualScale(float first, float second) + { + // GetScale error grows with the matrix scale, so compare relatively. + var largestMagnitude = math.max(math.abs(first), math.abs(second)); + return math.abs(first - second) <= Tolerance * largestMagnitude; + } + public ColliderHeader Header; public int Id => Header.Id; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Line3DCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Line3DCollider.cs index 6654a4d83..bf2fea71c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Line3DCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/Line3DCollider.cs @@ -126,7 +126,7 @@ private static float HitTest(ref CollisionEventData collEvent, ref Line3DCollide // transform hit normal back to world coordinate system if (hitTime >= 0) { - collEvent.HitNormal = math.mul(coll._matrix, collEvent.HitNormal); + collEvent.HitNormal = math.mul(math.transpose(coll._matrix), collEvent.HitNormal); } return hitTime; @@ -137,11 +137,11 @@ private static float HitTest(ref CollisionEventData collEvent, ref Line3DCollide public void Collide(ref BallState ball, ref NativeQueue.ParallelWriter hitEvents, in CollisionEventData collEvent, ref PhysicsState state) { - var dot = math.dot(collEvent.HitNormal, ball.Velocity); + var impactSpeed = -math.dot(collEvent.HitNormal, ball.Velocity); BallCollider.Collide3DWall(ref ball, in Header.Material, in collEvent, in collEvent.HitNormal, ref state); - if (Header.FireEvents && dot >= Header.Threshold && Header.IsPrimitive) { - // todo m_obj->m_currentHitThreshold = dot; + if (Header.FireEvents && impactSpeed >= Header.Threshold && Header.IsPrimitive) { + // todo m_obj->m_currentHitThreshold = impactSpeed; Collider.FireHitEvent(ref ball, ref hitEvents, in Header); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineZCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineZCollider.cs index d47e97400..dded4bf18 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineZCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/LineZCollider.cs @@ -196,19 +196,12 @@ public void Transform(LineZCollider lineCollider, float4x4 matrix) } #endif - var t = matrix.GetTranslation(); - var s = matrix.GetScale(); - - XY = lineCollider.XY + t.xy; - ZLow = lineCollider.ZLow + t.z; - ZHigh = lineCollider.ZHigh + t.z; - if (s.z > Collider.Tolerance) { - var height = ZHigh - ZLow; - var zMid = ZLow + height * 0.5f; - var halfHeightScaled = height * s.z * 0.5f; - ZLow = zMid - halfHeightScaled; - ZHigh = zMid + halfHeightScaled; - } + var p1 = matrix.MultiplyPoint(new float3(lineCollider.XY, lineCollider.ZLow)); + var p2 = matrix.MultiplyPoint(new float3(lineCollider.XY, lineCollider.ZHigh)); + + XY = p1.xy; + ZLow = math.min(p1.z, p2.z); + ZHigh = math.max(p1.z, p2.z); CalculateBounds(); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/PointCollider.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/PointCollider.cs index 3a7994740..8e4449706 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/PointCollider.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collider/PointCollider.cs @@ -152,7 +152,7 @@ public PointCollider Transform(float4x4 matrix) public void Transform(PointCollider point, float4x4 matrix) { P = matrix.MultiplyPoint(point.P); - TransformAabb(matrix); + Bounds = new ColliderBounds(Header.ItemId, Header.Id, new Aabb(P, P)); } public Aabb GetTransformedAabb(float4x4 matrix) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/Aabb.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/Aabb.cs index e0dfb9fb6..260b8d9f9 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/Aabb.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/Aabb.cs @@ -169,8 +169,9 @@ private static float max(params float[] values) public readonly override bool Equals(object obj) { - if (obj is Aabb) - return Equals(obj); + if (obj is Aabb other) { + return Equals(other); + } return false; } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ColliderHeader.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ColliderHeader.cs index c34af4011..7db433353 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ColliderHeader.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/ColliderHeader.cs @@ -98,8 +98,9 @@ public readonly bool Equals(ColliderHeader other) public override readonly bool Equals(object obj) { - if (obj is ColliderHeader) - return Equals(obj); + if (obj is ColliderHeader other) { + return Equals(other); + } return false; } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/CollisionEventData.cs b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/CollisionEventData.cs index 41b7ef67e..015af599b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/CollisionEventData.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Physics/Collision/CollisionEventData.cs @@ -23,6 +23,7 @@ public struct CollisionEventData public float HitTime; public float3 HitNormal; public float2 HitVelocity; + private float _hitVelocityZ; public float HitDistance; public bool HitFlag; public float HitOrgNormalVelocity; @@ -58,6 +59,13 @@ public void ClearCollider() { ColliderId = -1; BallId = 0; + ClearHitVelocity(); + } + + public void ClearHitVelocity() + { + HitVelocity = float2.zero; + _hitVelocityZ = 0f; } public bool HasCollider() @@ -68,7 +76,9 @@ public bool HasCollider() public void Transform(float4x4 matrix) { HitNormal = matrix.MultiplyVector(HitNormal); - HitVelocity = matrix.MultiplyVector(new float3(HitVelocity, 0)).xy; + var transformedHitVelocity = matrix.MultiplyVector(new float3(HitVelocity, _hitVelocityZ)); + HitVelocity = transformedHitVelocity.xy; + _hitVelocityZ = transformedHitVelocity.z; } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallState.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallState.cs index ae9d9d2fd..4f7aeabc6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallState.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ball/BallState.cs @@ -145,7 +145,7 @@ the angular velocity used here is not the angular velocity that the ball has (wh */ } - public void UpdateVelocities(float3 gravity) => BallVelocityPhysics.UpdateVelocities(ref this, gravity, float2.zero); + public void UpdateVelocities(float3 gravity) => BallVelocityPhysics.UpdateVelocities(ref this, gravity, float2.zero); public override string ToString() { @@ -154,6 +154,13 @@ public override string ToString() public void Transform(float4x4 matrix) { + var scale = matrix.GetScale(); + var hasUniformScale = Collider.HasEqualScale(scale.x, scale.y) + && Collider.HasEqualScale(scale.x, scale.z); + if (hasUniformScale) { + Radius *= scale.x; + } + Position = matrix.MultiplyPoint(Position); EventPosition = matrix.MultiplyPoint(EventPosition); Velocity = matrix.MultiplyVector(Velocity); diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs index 465759a45..74b04cac7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Kicker/KickerApi.cs @@ -218,7 +218,7 @@ private void KickXYZ(float angle, float speed, float inclination, float x, float var rotQuaternion = new quaternion(rotMatrix); ballData.Velocity = math.mul(rotQuaternion, velocity); - ballData.IsFrozen = false; + ballData.IsFrozen = false; ballData.AngularMomentum = float3.zero; // update collision event @@ -226,7 +226,7 @@ private void KickXYZ(float angle, float speed, float inclination, float x, float collEvent.HitDistance = 0.0f; collEvent.HitTime = -1.0f; collEvent.HitNormal = float3.zero; - collEvent.HitVelocity = float2.zero; + collEvent.ClearHitVelocity(); collEvent.HitFlag = false; collEvent.IsContact = false;