diff --git a/.editorconfig b/.editorconfig index f288d584..6b3f9974 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,7 @@ root = true [*.cs] dotnet_diagnostic.IDE0005.severity = error +dotnet_diagnostic.IDE0031.severity = none #### Core EditorConfig Options #### diff --git a/AGXUnity/CableTunnelingGuard.cs b/AGXUnity/CableTunnelingGuard.cs new file mode 100644 index 00000000..b714a97f --- /dev/null +++ b/AGXUnity/CableTunnelingGuard.cs @@ -0,0 +1,248 @@ +using AGXUnity.Utils; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UIElements; + +namespace AGXUnity +{ + [AddComponentMenu( "AGXUnity/Cable Tunneling Guard" )] + [DisallowMultipleComponent] + [RequireComponent( typeof( AGXUnity.Cable ) )] + [HelpURL( "https://us.download.algoryx.se/AGXUnity/documentation/current/editor_interface.html#cable-tunneling-guard" )] + public class CableTunnelingGuard : ScriptComponent + { + /// + /// Native instance of the cable tuneling guard. + /// + public agxCable.CableTunnelingGuard Native { get; private set; } + + [System.NonSerialized] + private Cable m_cable = null; + + /// + /// The Cable ScriptComponent that this CableTunnelingGuard follows + /// + [HideInInspector] + public Cable Cable { get { return m_cable ??= GetComponent(); } } + + /// + /// The mesh which is used to visualise a hull + /// + private Mesh m_mesh = null; + + [SerializeField] + private double m_hullScale = 4; + + public double HullScale + { + get { return m_hullScale; } + set + { + value = System.Math.Max( value, 1.0f ); + + if ( m_hullScale != value ) { + m_hullScale = value; + UpdateRenderingMesh(); + } + + if ( Native != null ) { + Native.setHullScale( m_hullScale ); + } + } + } + + private void UpdateRenderingMesh() + { + if ( m_mesh == null ) + m_mesh = new Mesh(); + + if ( m_pointCurveCache != null && m_pointCurveCache.Length >= 2 ) { + float segmentLength = ( Cable.GetRoutePoints()[ 0 ]-Cable.GetRoutePoints()[ 1 ] ).magnitude; + CapsuleShapeUtils.CreateCapsuleMesh( Cable.Radius * (float)m_hullScale, segmentLength, 0.7f, m_mesh ); + } + } + + // See documentation / tutorials for a more detailed description of the native parameters + + /// + /// The angle to cable ends at which and approaching contact is accepted + /// + [SerializeField] + private double m_angleThreshold = 90.0 * 0.9; + + public double AngleThreshold + { + get { return m_angleThreshold; } + set + { + m_angleThreshold = System.Math.Clamp( value, 0, 90 ); + if ( Native != null ) { + Native.setAngleThreshold( m_angleThreshold / 180.0 * Mathf.PI ); + } + } + } + + /// + /// A parameter which controls how far the estimated penetration depth must be at the enxt step to attempt to + /// prevent a tunneling occurence + /// + [SerializeField] + private double m_leniency = 0; + + public double Leniency + { + get { return m_leniency; } + set + { + m_leniency = value; + if ( Native != null ) { + Native.setLeniency( m_leniency ); + } + } + } + + /// + /// The amount of steps for which the component will continue adding contacts to the solver after a contact has + /// been predicted. + /// + [SerializeField] + private uint m_debounceSteps = 0; + + public uint DebounceSteps + { + get { return m_debounceSteps; } + set + { + m_debounceSteps = value; + if ( Native != null ) { + Native.setDebounceSteps( m_debounceSteps ); + } + } + } + + /// + /// When set to true the component will not attempt any predictions and will always add the contacts it encounters + /// through the hulls to the solver + /// + [SerializeField] + private bool m_alwaysAdd = false; + + public bool AlwaysAdd + { + get { return m_alwaysAdd; } + set + { + m_alwaysAdd = value; + if ( Native != null ) { + Native.setAlwaysAdd( m_alwaysAdd ); + } + } + } + + /// + /// When true the component will predict tunneling with its own segments as well + /// + [SerializeField] + private bool m_enableSelfInteraction = true; + + public bool EnableSelfInteraction + { + get { return m_enableSelfInteraction; } + set + { + m_enableSelfInteraction = value; + if ( Native != null ) { + Native.setEnableSelfInteraction( m_enableSelfInteraction ); + } + } + } + + protected override bool Initialize() + { + Native = new agxCable.CableTunnelingGuard( m_hullScale ); + + var cable = Cable?.GetInitialized()?.Native; + if ( cable == null ) { + Debug.LogWarning( "Unable to find Cable component for CableTunnelingGuard - cable tunneling guard instance ignored.", this ); + return false; + } + + cable.addComponent( Native ); + + return true; + } + + protected override void OnDestroy() + { + if ( GetSimulation() == null ) + return; + + var cable = Cable.Native; + if ( cable != null ) { + cable.removeComponent( Native ); + } + + Native = null; + + base.OnDestroy(); + } + + protected override void OnEnable() + { + Native?.setEnabled( true ); + } + + protected override void OnDisable() + { + Native?.setEnabled( false ); + } + + private void Reset() + { + if ( GetComponent() == null ) + Debug.LogError( "Component: CableDamage requires Cable component.", this ); + } + + private Vector3[] m_pointCurveCache = null; + + private bool CheckCableRouteChanges() + { + var routePointCahce = Cable.GetRoutePoints(); + if ( m_pointCurveCache != routePointCahce ) { + m_pointCurveCache = routePointCahce; + return true; + } + return false; + } + + private void OnDrawGizmosSelected() + { + if ( CheckCableRouteChanges() ) { + UpdateRenderingMesh(); + } + + if ( enabled ) { + // Algoryx orange + Gizmos.color = new Color32( 0xF3, 0x8B, 0x00, 0xF ); + if ( Application.isPlaying && Cable?.Native != null ) { + foreach ( var segment in Cable.Native.getSegments() ) { + Vector3 direction = (segment.getEndPosition() - segment.getBeginPosition()).ToHandedVector3(); + Vector3 center = segment.getCenterPosition().ToHandedVector3(); + Gizmos.DrawWireMesh( m_mesh, center, Quaternion.FromToRotation( Vector3.up, direction ) ); + } + } + else if ( m_pointCurveCache != null && m_pointCurveCache.Length != 0 ) { + Vector3 prevPoint = m_pointCurveCache[0]; + foreach ( var point in m_pointCurveCache.Skip( 1 ) ) { + Vector3 direction = point-prevPoint; + Vector3 center = prevPoint + direction * 0.5f; + Gizmos.DrawWireMesh( m_mesh, center, Quaternion.FromToRotation( Vector3.up, direction ) ); + prevPoint = point; + } + } + } + } + } + +} diff --git a/AGXUnity/CableTunnelingGuard.cs.meta b/AGXUnity/CableTunnelingGuard.cs.meta new file mode 100644 index 00000000..582b7dd6 --- /dev/null +++ b/AGXUnity/CableTunnelingGuard.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4709735297490e0b9ed730ac2acdbb8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: d1ef71574c167404a8c279a99853078e, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/AGXUnity/Rendering/ShapeDebugRenderData.cs b/AGXUnity/Rendering/ShapeDebugRenderData.cs index f497a8c4..0a5c6154 100644 --- a/AGXUnity/Rendering/ShapeDebugRenderData.cs +++ b/AGXUnity/Rendering/ShapeDebugRenderData.cs @@ -223,6 +223,7 @@ private bool TryInitialize( Shape shape, DebugRenderManager manager ) Cone cone = shape as Cone; HollowCone hollowCone = shape as HollowCone; HollowCylinder hollowCylinder = shape as HollowCylinder; + Capsule capsule = shape as Capsule; if ( mesh != null ) Node = InitializeMesh( mesh ); else if ( heightField != null ) @@ -242,6 +243,11 @@ private bool TryInitialize( Shape shape, DebugRenderManager manager ) Node.AddComponent().sharedMaterial = manager.ShapeRenderMaterial; Node.AddComponent().sharedMesh = ShapeVisualCone.GenerateMesh( shape ); } + else if ( capsule != null ) { + Node = new GameObject( PrefabName ); + Node.AddComponent().sharedMaterial = manager.ShapeRenderMaterial; + Node.AddComponent().sharedMesh = ShapeVisualCapsule.GenerateMesh( shape ); + } else { Node = PrefabLoader.Instantiate( PrefabName ); Node.transform.localScale = GetShape().GetScale(); diff --git a/AGXUnity/Rendering/ShapeVisual.cs b/AGXUnity/Rendering/ShapeVisual.cs index 58c6b048..b13c87b8 100644 --- a/AGXUnity/Rendering/ShapeVisual.cs +++ b/AGXUnity/Rendering/ShapeVisual.cs @@ -400,7 +400,7 @@ protected static GameObject CreateGameObject( Collide.Shape shape, bool isRender { GameObject go = null; try { - go = isRenderData || shape is Collide.Mesh || shape is Collide.HollowCylinder || shape is Collide.Cone || shape is Collide.HollowCone ? + go = isRenderData || shape is Collide.Mesh || shape is Collide.HollowCylinder || shape is Collide.Cone || shape is Collide.HollowCone || shape is Collide.Capsule ? new GameObject( "" ) : PrefabLoader.Instantiate( @"Debug/" + shape.GetType().Name + "Renderer" ); diff --git a/AGXUnity/Rendering/ShapeVisualCapsule.cs b/AGXUnity/Rendering/ShapeVisualCapsule.cs index 4e653617..9e12ca22 100644 --- a/AGXUnity/Rendering/ShapeVisualCapsule.cs +++ b/AGXUnity/Rendering/ShapeVisualCapsule.cs @@ -1,4 +1,6 @@ -using UnityEngine; +using AGXUnity.Utils; +using System.Linq; +using UnityEngine; namespace AGXUnity.Rendering { @@ -10,15 +12,40 @@ namespace AGXUnity.Rendering [HelpURL( "https://us.download.algoryx.se/AGXUnity/documentation/current/editor_interface.html#create-visual-tool-icon-small-create-visual-tool" )] public class ShapeVisualCapsule : ShapeVisual { + private Mesh m_mesh = null; + + private const float m_resolution = 1; + + /// + /// Callback when constructed. + /// + protected override void OnConstruct() + { + gameObject.AddComponent(); + gameObject.AddComponent(); + + m_mesh = GenerateMesh( Shape ); + gameObject.GetComponent().sharedMesh = m_mesh; + } + /// - /// Capsule visual is three game objects (2 x half sphere + 1 x cylinder), - /// the size has to be updated to all of the children. + /// Callback from Shape when its size has been changed. /// public override void OnSizeUpdated() { - ShapeDebugRenderData.SetCapsuleSize( gameObject, - ( Shape as Collide.Capsule ).Radius, - ( Shape as Collide.Capsule ).Height ); + transform.localScale = GetUnscaledScale(); + + m_mesh = GenerateMesh( Shape ); + gameObject.GetComponent().sharedMesh = m_mesh; + } + + /// + /// Generates custom shape mesh + /// + public static Mesh GenerateMesh( Collide.Shape shape ) + { + Collide.Capsule capsule = shape as Collide.Capsule; + return CapsuleShapeUtils.CreateCapsuleMesh( capsule.Radius, capsule.Height, m_resolution ); } } } diff --git a/AGXUnity/Utils/ShapeUtils.cs b/AGXUnity/Utils/ShapeUtils.cs index 53f1d790..22edbaba 100644 --- a/AGXUnity/Utils/ShapeUtils.cs +++ b/AGXUnity/Utils/ShapeUtils.cs @@ -1,4 +1,5 @@ using AGXUnity.Collide; +using System.Linq; using UnityEngine; namespace AGXUnity.Utils @@ -44,6 +45,31 @@ public override Vector3 GetLocalFace( Direction dir ) else return capsule.Radius * GetLocalFaceDirection( dir ); } + + public static UnityEngine.Mesh CreateCapsuleMesh( float radius, float height, float resolution, UnityEngine.Mesh destination = null ) + { + destination ??= new UnityEngine.Mesh(); + + radius = Mathf.Max( 0, radius ); + height = Mathf.Max( 0, height ); + + agxCollide.MeshData meshData = agxUtil.PrimitiveMeshGenerator.createCapsule( radius, height, resolution ).getMeshData(); + var agxIndices = meshData.getIndices(); + int[] triangles = new int[agxIndices.Count]; + + // Flip winding order + for ( int i = 0; i < triangles.Length; i+=3 ) + (triangles[ i ], triangles[ i+2 ], triangles[ i+1 ]) = ((int)agxIndices[ i ], (int)agxIndices[ i+1 ], (int)agxIndices[ i+2 ]); + + + destination.SetVertices( meshData.getVertices().Select( v => v.ToHandedVector3() ).ToArray() ); + destination.SetTriangles( triangles, 0, calculateBounds: false ); + + destination.RecalculateNormals(); + destination.RecalculateTangents(); + + return destination; + } } public class CylinderShapeUtils : RadiusHeightShapeUtils diff --git a/Editor/AGXUnityEditor/InspectorEditor.cs b/Editor/AGXUnityEditor/InspectorEditor.cs index a44b0712..9dfa74e7 100644 --- a/Editor/AGXUnityEditor/InspectorEditor.cs +++ b/Editor/AGXUnityEditor/InspectorEditor.cs @@ -323,7 +323,7 @@ private static bool HandleType( InvokeWrapper wrapper, Debug.LogWarning( "The AGXUnity Inspector wrapper currently does not support editable array types. Consider using List as an alternative." ); if ( EditorGUI.EndChangeCheck() && assignSupported ) { changed = true; - value = serializedProperty.objectReferenceValue; + value = serializedProperty.boxedValue; } } } diff --git a/Editor/CustomEditors/AGXUnity+CableTunnelingGuardEditor.cs b/Editor/CustomEditors/AGXUnity+CableTunnelingGuardEditor.cs new file mode 100644 index 00000000..4038282a --- /dev/null +++ b/Editor/CustomEditors/AGXUnity+CableTunnelingGuardEditor.cs @@ -0,0 +1,10 @@ + +using UnityEditor; + +namespace AGXUnityEditor.Editors +{ + [CustomEditor( typeof( AGXUnity.CableTunnelingGuard ) )] + [CanEditMultipleObjects] + public class AGXUnityCableTunnelingGuardEditor : InspectorEditor + { } +} diff --git a/Editor/CustomEditors/AGXUnity+CableTunnelingGuardEditor.cs.meta b/Editor/CustomEditors/AGXUnity+CableTunnelingGuardEditor.cs.meta new file mode 100644 index 00000000..0c49bb1c --- /dev/null +++ b/Editor/CustomEditors/AGXUnity+CableTunnelingGuardEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e38cbf2bd085834728f2150eb05b61e6 \ No newline at end of file diff --git a/Plugins/x86_64/agxDotNet.dll b/Plugins/x86_64/agxDotNet.dll index 8bb6ac4f..b1f264fd 100644 Binary files a/Plugins/x86_64/agxDotNet.dll and b/Plugins/x86_64/agxDotNet.dll differ