diff --git a/AGXUnity/ContactMaterial.cs b/AGXUnity/ContactMaterial.cs index bdd701d2..67b8d52f 100644 --- a/AGXUnity/ContactMaterial.cs +++ b/AGXUnity/ContactMaterial.cs @@ -128,6 +128,7 @@ public FrictionModel FrictionModel /// Get or set Young's modulus of this contact material. /// [ClampAboveZeroInInspector] + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [Tooltip( "This specifies the stiffness of the contact between the interacting geometries/bodies" )] public float YoungsModulus { @@ -150,6 +151,7 @@ public float YoungsModulus /// Get or set surface viscosity of this contact material. /// [ClampAboveZeroInInspector( true )] + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [Tooltip( "The viscosity of a surface material is the same thing as compliance but for friction in contacts" )] public Vector2 SurfaceViscosity { @@ -174,6 +176,7 @@ public Vector2 SurfaceViscosity /// Get or set friction coefficients of this contact material. /// [ClampAboveZeroInInspector( true )] + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [Tooltip( "The coefficients of friction in the friction frame (default: world frame)" )] public Vector2 FrictionCoefficients { @@ -198,6 +201,7 @@ public Vector2 FrictionCoefficients /// Get or set restitution of this contact material. /// [ClampAboveZeroInInspector( true )] + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [Tooltip( "This defines the \"bounciness\" of a contact." )] public float Restitution { @@ -242,6 +246,7 @@ public float Damping /// Adhesive force of the contacts with this contact material. /// [ClampAboveZeroInInspector( true )] + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [Tooltip( "Determines a force used for keeping colliding objects together" )] public float AdhesiveForce { @@ -266,6 +271,7 @@ public float AdhesiveForce /// at higher overlap, the (usual) contact force. /// [ClampAboveZeroInInspector( true )] + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [Tooltip( "allowed overlap from surface for resting contact. At this overlap, no force is applied. At lower overlap, the adhesion force will work, at higher overlap, the (usual) contact force" )] public float AdhesiveOverlap { @@ -287,6 +293,7 @@ public float AdhesiveOverlap /// /// Enable/disable contact area approach of contacts using this contact material. /// + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [Tooltip( "If set to “true”, an approximation to the contact area will be geometrically computed for each contact involving this contact material. For each contact, its area will then be evenly distributed between its contact points. The contact compliance will be scaled with the inverse of the area for each contact point." )] public bool UseContactArea { @@ -308,6 +315,7 @@ public bool UseContactArea /// /// Contact reduction mode, default Geometry. /// + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [Tooltip( " Specifies at which level the contact reduction algorithm should be run. None, Geometry, or Geometry and Rigidbody" )] public ContactReductionType ContactReductionMode { @@ -329,6 +337,7 @@ public ContactReductionType ContactReductionMode /// /// Contact reduction level when contact reduction is enabled (ContactReductionMode != None). /// + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [Tooltip( "Contact reduction level when contact reduction is enabled (ContactReductionMode != None)" )] public ContactReductionLevelType ContactReductionLevel { @@ -362,6 +371,7 @@ public ContactReductionLevelType ContactReductionLevel /// The primary (x) friction coefficient is used along the wire and the secondary (y) is /// along the contact edge on the object the wire interacts with. /// + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [ClampAboveZeroInInspector( true )] [Tooltip( "Wire friction coefficients of this contact material, used by the contact nodes on a wire. The primary (x) friction coefficient is used along the wire and the secondary (y) is along the contact edge on the object the wire interacts with." )] public Vector2 WireFrictionCoefficients @@ -377,6 +387,8 @@ public Vector2 WireFrictionCoefficients } } + private bool IsNotTerrainWheelForceModel => (FrictionModel != null) ? FrictionModel.IsNotTerrainWheelForceModel : true; + public ContactMaterial RestoreLocalDataFrom( agx.ContactMaterial contactMaterial ) { YoungsModulus = Convert.ToSingle( contactMaterial.getYoungsModulus() ); @@ -478,7 +490,13 @@ protected override bool Initialize() m_contactMaterial = GetSimulation().getMaterialManager().getOrCreateContactMaterial( m1, m2 ); if ( FrictionModel != null ) { - m_contactMaterial.setFrictionModel( FrictionModel.GetInitialized().Native ); + if ( FrictionModel.Type == FrictionModel.EType.TerrainWheelForceModel ) { + agxTerrain.TerrainWheel.configureContactMaterial( m_contactMaterial ); + } + else { + m_contactMaterial.setFrictionModel( FrictionModel.GetInitialized().Native ); + } + // When the user changes friction model type (enum = BoxFriction, ScaleBoxFriction etc.) // the friction model object will create a new native instance. We'll receive callbacks // when this happens so we can assign it to our native contact material. diff --git a/AGXUnity/FrictionModel.cs b/AGXUnity/FrictionModel.cs index c703f97d..8590a2e1 100644 --- a/AGXUnity/FrictionModel.cs +++ b/AGXUnity/FrictionModel.cs @@ -22,7 +22,8 @@ public enum EType IterativeProjectedFriction = 0, ScaleBoxFriction, BoxFriction, - ConstantNormalForceBoxFriction + ConstantNormalForceBoxFriction, + TerrainWheelForceModel } public enum PrimaryDirection @@ -86,6 +87,8 @@ public static EType FindType( agx.FrictionModel native ) { if ( native == null || native.asIterativeProjectedConeFriction() != null ) return EType.IterativeProjectedFriction; + else if ( native.asTerrainWheelForceModel() != null ) + return EType.TerrainWheelForceModel; else if ( native.asScaleBoxFrictionModel() != null ) return EType.ScaleBoxFriction; else if ( native.asConstantNormalForceOrientedBoxFrictionModel() != null ) @@ -112,6 +115,7 @@ public static EType FindType( agx.FrictionModel native ) /// /// Get or set solve type of this friction model. /// + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] [Tooltip( "Get or set solve type of this friction model." )] public ESolveType SolveType { @@ -148,12 +152,14 @@ public ESolveType SolveType /// * Scale Box - Will attempt to scale the friction bounds throughout the solve stage. /// * Box - Has fixed friction bounds throughout the solve stage, performant but low accuracy. Makes a guess at the normal force to solve for friction. /// * Constant Normal Force Box - Like Box, but will use a provided constant normal force magnitude. + /// * Terrain Wheel Force Model - Special model used for cylinder / terrain frictions. Used with TerrainWheel component. /// [Tooltip( "Specifies the friction model to use for this contact materials using this asset.\n" + "* Iterative Projected Cone - The default model. Provides a good base model which handles anisotripic frictions better than the box models.\n" + "* Scale Box - Will attempt to scale the friction bounds throughout the solve stage.\n" + "* Box - Has fixed friction bounds throughout the solve stage, performant but low accuracy. Makes a guess at the normal force to solve for friction.\n" + - "* Constant Normal Force Box - Like Box, but will use a provided constant normal force magnitude." )] + "* Constant Normal Force Box - Like Box, but will use a provided constant normal force magnitude." + + "* Terrain Wheel Force Model - Special model used for cylinder / terrain frictions. Used with TerrainWheel component." )] public EType Type { get { return m_type; } @@ -177,6 +183,8 @@ public EType Type /// Enable to mark that this friction model will be used for ContactMaterials involving track. This allows the friction frame to be set up automatically for these materials. /// [Tooltip( "Enable to mark that this friction model will be used for ContactMaterials involving track. This allows the friction frame to be set up automatically for these materials" )] + [DynamicallyShowInInspector( nameof( IsNotTerrainWheelForceModel ) )] + public bool TrackFrictionModel { get; set; } = false; /// @@ -186,7 +194,10 @@ public EType Type [SerializeField] private float m_normalForceMagnitude = 100.0f; + [HideInInspector] private bool IsConstantNormalForceModel => Type == EType.ConstantNormalForceBoxFriction; + [HideInInspector] + public bool IsNotTerrainWheelForceModel => Type != EType.TerrainWheelForceModel; /// /// Normal force magnitude used in ConstantNormalForceBoxFriction. @@ -271,7 +282,10 @@ public agx.FrictionModel CreateNative( EType type, agx.FrictionModel frictionModel = null; - if ( TrackFrictionModel ) { + if ( type == EType.TerrainWheelForceModel) { + frictionModel = new agx.TerrainWheelForceModel(); + } + else if ( TrackFrictionModel ) { frictionModel = type switch { EType.IterativeProjectedFriction => new agxVehicle.TrackIterativeProjectedConeFrictionModel( Convert( solveType ) ), diff --git a/AGXUnity/Model/DeformableTerrainMaterial.cs b/AGXUnity/Model/DeformableTerrainMaterial.cs index f0831f6c..db42c77e 100644 --- a/AGXUnity/Model/DeformableTerrainMaterial.cs +++ b/AGXUnity/Model/DeformableTerrainMaterial.cs @@ -996,6 +996,331 @@ public float ExcavationStiffnessMultiplier } #endregion + #region Terramechanics Properties + [SerializeField] + private float m_sinkageExponentParameterA = 0.0f; + + /// + /// Sinkage exponent parameter A for the terramechanics pressure-sinkage model. + /// + [InspectorGroupBegin( Name = "Terramechanics Properties" )] + [Tooltip( "Sinkage exponent parameter A for the terramechanics pressure-sinkage model." )] + public float SinkageExponentParameterA + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getSinkageExponentParameterA() ) : + m_sinkageExponentParameterA; + } + set + { + m_sinkageExponentParameterA = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setSinkageExponentParameterA( m_sinkageExponentParameterA ); + } + } + + [SerializeField] + private float m_sinkageExponentParameterB = 0.0f; + + /// + /// Sinkage exponent parameter B for the terramechanics pressure-sinkage model. + /// + [Tooltip( "Sinkage exponent parameter B for the terramechanics pressure-sinkage model." )] + public float SinkageExponentParameterB + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getSinkageExponentParameterB() ) : + m_sinkageExponentParameterB; + } + set + { + m_sinkageExponentParameterB = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setSinkageExponentParameterB( m_sinkageExponentParameterB ); + } + } + + [SerializeField] + private float m_shearModulusTangentialParameterA = 0.0f; + + /// + /// Tangential (longitudinal) shear modulus parameter A for the terramechanics model. + /// + [Tooltip( "Tangential (longitudinal) shear modulus parameter A for the terramechanics model." )] + public float ShearModulusTangentialParameterA + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getShearModulusTangentialParameterA() ) : + m_shearModulusTangentialParameterA; + } + set + { + m_shearModulusTangentialParameterA = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setShearModulusTangentialParameterA( m_shearModulusTangentialParameterA ); + } + } + + [SerializeField] + private float m_shearModulusTangentialParameterB = 0.0f; + + /// + /// Tangential (longitudinal) shear modulus parameter B for the terramechanics model. + /// + [Tooltip( "Tangential (longitudinal) shear modulus parameter B for the terramechanics model." )] + public float ShearModulusTangentialParameterB + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getShearModulusTangentialParameterB() ) : + m_shearModulusTangentialParameterB; + } + set + { + m_shearModulusTangentialParameterB = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setShearModulusTangentialParameterB( m_shearModulusTangentialParameterB ); + } + } + + [SerializeField] + private float m_shearModulusLateralParameterA = 0.0f; + + /// + /// Lateral shear modulus parameter A for the terramechanics model. + /// + [Tooltip( "Lateral shear modulus parameter A for the terramechanics model." )] + public float ShearModulusLateralParameterA + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getShearModulusLateralParameterA() ) : + m_shearModulusLateralParameterA; + } + set + { + m_shearModulusLateralParameterA = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setShearModulusLateralParameterA( m_shearModulusLateralParameterA ); + } + } + + [SerializeField] + private float m_shearModulusLateralParameterB = 0.0f; + + /// + /// Lateral shear modulus parameter B for the terramechanics model. + /// + [Tooltip( "Lateral shear modulus parameter B for the terramechanics model." )] + public float ShearModulusLateralParameterB + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getShearModulusLateralParameterB() ) : + m_shearModulusLateralParameterB; + } + set + { + m_shearModulusLateralParameterB = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setShearModulusLateralParameterB( m_shearModulusLateralParameterB ); + } + } + + [SerializeField] + private float m_cohesiveModulusBekker = 0.0f; + + /// + /// Cohesive modulus (kc) in the Bekker pressure-sinkage model (Pa/m^n). + /// + [Tooltip( "Cohesive modulus (kc) in the Bekker pressure-sinkage model." )] + public float CohesiveModulusBekker + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getCohesiveModulusBekker() ) : + m_cohesiveModulusBekker; + } + set + { + m_cohesiveModulusBekker = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setCohesiveModulusBekker( m_cohesiveModulusBekker ); + } + } + + [SerializeField] + private float m_frictionalModulusBekker = 0.0f; + + /// + /// Frictional modulus (kphi) in the Bekker pressure-sinkage model (Pa/m^(n+1)). + /// + [Tooltip( "Frictional modulus (kphi) in the Bekker pressure-sinkage model." )] + public float FrictionalModulusBekker + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getFrictionalModulusBekker() ) : + m_frictionalModulusBekker; + } + set + { + m_frictionalModulusBekker = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setFrictionalModulusBekker( m_frictionalModulusBekker ); + } + } + + [SerializeField] + private float m_cohesiveModulusReece = 0.0f; + + /// + /// Cohesive modulus (ck) in the Reece pressure-sinkage model (Pa). + /// + [Tooltip( "Cohesive modulus (ck) in the Reece pressure-sinkage model." )] + public float CohesiveModulusReece + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getCohesiveModulusReece() ) : + m_cohesiveModulusReece; + } + set + { + m_cohesiveModulusReece = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setCohesiveModulusReece( m_cohesiveModulusReece ); + } + } + + [SerializeField] + private float m_frictionalModulusReece = 0.0f; + + /// + /// Frictional modulus (cgamma) in the Reece pressure-sinkage model (Pa). + /// + [Tooltip( "Frictional modulus (cgamma) in the Reece pressure-sinkage model." )] + public float FrictionalModulusReece + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getFrictionalModulusReece() ) : + m_frictionalModulusReece; + } + set + { + m_frictionalModulusReece = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setFrictionalModulusReece( m_frictionalModulusReece ); + } + } + + [SerializeField] + private float m_maximumNormalStressAngleParameterA = 0.0f; + + /// + /// Parameter A for the maximum normal stress angle in the terramechanics model. + /// + [Tooltip( "Parameter A for the maximum normal stress angle in the terramechanics model." )] + public float MaximumNormalStressAngleParameterA + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getMaximumNormalStressAngleParameterA() ) : + m_maximumNormalStressAngleParameterA; + } + set + { + m_maximumNormalStressAngleParameterA = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setMaximumNormalStressAngleParameterA( m_maximumNormalStressAngleParameterA ); + } + } + + [SerializeField] + private float m_maximumNormalStressAngleParameterB = 0.0f; + + /// + /// Parameter B for the maximum normal stress angle in the terramechanics model. + /// + [Tooltip( "Parameter B for the maximum normal stress angle in the terramechanics model." )] + public float MaximumNormalStressAngleParameterB + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getMaximumNormalStressAngleParameterB() ) : + m_maximumNormalStressAngleParameterB; + } + set + { + m_maximumNormalStressAngleParameterB = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setMaximumNormalStressAngleParameterB( m_maximumNormalStressAngleParameterB ); + } + } + + [SerializeField] + private float m_rearAngleParameterA = 0.0f; + + /// + /// Parameter A for the rear contact angle in the terramechanics model. + /// + [Tooltip( "Parameter A for the rear contact angle in the terramechanics model." )] + public float RearAngleParameterA + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getRearAngleParameterA() ) : + m_rearAngleParameterA; + } + set + { + m_rearAngleParameterA = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setRearAngleParameterA( m_rearAngleParameterA ); + } + } + + [SerializeField] + private float m_rearAngleParameterB = 0.0f; + + /// + /// Parameter B for the rear contact angle in the terramechanics model. + /// + [Tooltip( "Parameter B for the rear contact angle in the terramechanics model." )] + public float RearAngleParameterB + { + get + { + return m_temporaryNative != null ? + Convert.ToSingle( m_temporaryNative.getTerramechanicsProperties().getRearAngleParameterB() ) : + m_rearAngleParameterB; + } + set + { + m_rearAngleParameterB = value; + if ( Native != null ) + Native.getTerramechanicsProperties().setRearAngleParameterB( m_rearAngleParameterB ); + } + } + #endregion + /// /// Assign new preset name without updating any values. /// diff --git a/AGXUnity/Model/DeformableTerrainWheel.cs b/AGXUnity/Model/DeformableTerrainWheel.cs new file mode 100644 index 00000000..2d64e1f4 --- /dev/null +++ b/AGXUnity/Model/DeformableTerrainWheel.cs @@ -0,0 +1,265 @@ +using System.Linq; +using UnityEngine; +using UnityEngine.Serialization; + +namespace AGXUnity.Model +{ + [AddComponentMenu( "AGXUnity/Deformable Terrain Wheel" )] + [DisallowMultipleComponent] + [RequireComponent( typeof( RigidBody ) )] + [HelpURL( "https://us.download.algoryx.se/AGXUnity/documentation/current/editor_interface.html#deformable-terrain-wheel" )] + public class DeformableTerrainWheel : ScriptComponent + { + /// + /// Native instance of this terrain wheel. + /// + [HideInInspector] + public agxTerrain.TerrainWheel Native { get; private set; } = null; + + /// + /// Rigid body component of this terrain wheel. + /// + public RigidBody RigidBody { get { return m_rb ?? ( m_rb = GetComponent() ); } } + + [SerializeField] + private DeformableTerrainWheelSettings m_settings = null; + + [AllowRecursiveEditing] + public DeformableTerrainWheelSettings Settings + { + get { return m_settings; } + set + { + if ( Native != null && m_settings != null && m_settings != value ) + m_settings.Unregister( this ); + + m_settings = value; + + if ( Native != null && m_settings != null ) + m_settings.Register( this ); + } + } + + /// + /// Helper that will output a warning if the contact material in use by the terrain wheel doesn't have the correct force model. + /// NB: if using multiple shape materials on the terrain this is not reliable! + /// + [SerializeField] + [FormerlySerializedAs( "WarnIfNotUsingCorrectForceModel" )] + private bool m_warnIfNotUsingCorrectForceModel = false; + + [Tooltip( "Helper that will output a warning if the contact material in use by the terrain wheel doesn't have the correct force model." )] + public bool WarnIfNotUsingCorrectForceModel + { + get { return m_warnIfNotUsingCorrectForceModel; } + set { m_warnIfNotUsingCorrectForceModel = value; } + } + + protected override bool Initialize() + { + var rb = RigidBody?.GetInitialized()?.Native; + if ( rb == null ) { + Debug.LogWarning( "Unable to find RigidBody component for DeformableTerrainWheel - wheel instance ignored.", this ); + return false; + } + + var cylinders = RigidBody.GetComponentsInChildren() + .Where( c => c.GetComponentInParent() == RigidBody ) + .ToArray(); + + if ( cylinders.Length != 1 ) { + Debug.LogWarning( $"DeformableTerrainWheel requires exactly 1 Cylinder shape in the RigidBody, found {cylinders.Length}.", this ); + return false; + } + + cylinders[ 0 ].GetInitialized(); + var cylinder = cylinders[ 0 ].Native; + if ( cylinder == null ) { + Debug.LogWarning( "Unable to initialize Cylinder shape for DeformableTerrainWheel.", this ); + return false; + } + + // TODO - this is needed because of a bug in agx. Remove this is that is fixed. + var shapeMaterial = cylinder.getGeometry().getMaterial(); + if ( shapeMaterial == null ) { + Debug.LogWarning( "Unable to initialize Cylinder shape for DeformableTerrainWheel - ShapeMaterial needs to be set!", this ); + return false; + } + + Native = new agxTerrain.TerrainWheel( cylinder ); + + if ( Settings == null ) + Settings = CreateSettingsFromLegacyValues(); + + GetSimulation().add( Native ); + + return true; + } + + protected override void OnEnable() + { + base.OnEnable(); + } + + protected override void OnDisable() + { + base.OnDisable(); + } + + private void LateUpdate() + { + if ( !WarnIfNotUsingCorrectForceModel || Native?.getActiveTerrain() == null ) + return; + + if ( !ActiveContactMaterialUsesTerrainWheelForceModel ) + Debug.LogWarning( "Active Contact Material is NOT using terrainWheelForceModel!" ); + } + + private bool ActiveContactMaterialUsesTerrainWheelForceModel => GetActiveContactMaterial()?.getFrictionModel()?.asTerrainWheelForceModel() != null; + + private agx.ContactMaterial GetActiveContactMaterial() + { + if ( Native == null || GetSimulation() == null ) + return null; + + var wheelShapeMaterial = Native.getWheelGeometry()?.getMaterial(); + var terrainShapeMaterial = Native.getActiveTerrain()?.getMaterial(); + + if ( wheelShapeMaterial == null || terrainShapeMaterial == null ) + return null; + + var cm = GetSimulation()?.getMaterialManager()?.getContactMaterial( wheelShapeMaterial, terrainShapeMaterial ); + return cm; + } + + protected override void OnDestroy() + { + if ( Settings != null ) + Settings.Unregister( this ); + + if ( Simulation.HasInstance ) + GetSimulation().remove( Native ); + + Native = null; + + base.OnDestroy(); + } + + private void Reset() + { + if ( GetComponent() == null ) + Debug.LogError( "Component: DeformableTerrainWheel requires a RigidBody component.", this ); + } + + private DeformableTerrainWheelSettings CreateSettingsFromLegacyValues() + { + var settings = ScriptAsset.Create(); + settings.name = "[Temporary]Wheel Settings"; + settings.EnableTerrainDeformation = m_enableTerrainDeformation; + settings.EnableTerrainDisplacement = m_enableTerrainDisplacement; + settings.SlipDependenceBulldozing = m_slipDependenceBulldozing; + settings.SlipDependenceSlipDisplacement = m_slipDependenceSlipDisplacement; + settings.ForwardDisplacementWeight = m_forwardDisplacementWeight; + settings.BulldozeDisplacementAmountFactor = m_bulldozeDisplacementAmountFactor; + settings.LateralDisplacementDistScaling = m_lateralDisplacementDistScaling; + settings.ForwardDisplacementDistScaling = m_forwardDisplacementDistScaling; + settings.BackwardDisplacementDistScaling = m_backwardDisplacementDistScaling; + settings.AngularIntegrationStep = m_angularIntegrationStep; + settings.PressureSinkageModel = m_pressureSinkageModel; + settings.EnableComputeRearAngleFromFrontAngle = m_enableComputeRearAngleFromFrontAngle; + settings.EnableComputeMaximumNormalStressAngleFromFrontAngle = m_enableComputeMaximumNormalStressAngleFromFrontAngle; + settings.RearAndFrontAngleMaxMagnitude = m_rearAndFrontAngleMaxMagnitude; + settings.SlipRatioVxThreshold = m_slipRatioVxThreshold; + settings.SlipRatioOmegaYRThreshold = m_slipRatioOmegaYRThreshold; + settings.SlipRatioSmoothingSpeed = m_slipRatioSmoothingSpeed; + settings.SlipRatioMaxMagnitude = m_slipRatioMaxMagnitude; + settings.SlipRatioFallbackValue = m_slipRatioFallbackValue; + settings.SlipAngleVxAngularEquivalentThreshold = m_slipAngleVxAngularEquivalentThreshold; + settings.SlipAngleVyAngularEquivalentThreshold = m_slipAngleVyAngularEquivalentThreshold; + settings.SlipAngleMaxMagnitude = m_slipAngleMaxMagnitude; + settings.SlipAngleFallbackValue = m_slipAngleFallbackValue; + settings.RollingModeVxAngularEquivalentThreshold = m_rollingModeVxAngularEquivalentThreshold; + settings.RollingModeOmegaYThreshold = m_rollingModeOmegaYThreshold; + return settings; + } + + private RigidBody m_rb = null; + + #region Legacy Serialized Settings + [SerializeField, HideInInspector] + private bool m_enableTerrainDeformation = true; + + [SerializeField, HideInInspector] + private bool m_enableTerrainDisplacement = true; + + [SerializeField, HideInInspector] + private bool m_slipDependenceBulldozing = false; + + [SerializeField, HideInInspector] + private bool m_slipDependenceSlipDisplacement = true; + + [SerializeField, HideInInspector] + private float m_forwardDisplacementWeight = 0.5f; + + [SerializeField, HideInInspector] + private float m_bulldozeDisplacementAmountFactor = 0.5f; + + [SerializeField, HideInInspector] + private float m_lateralDisplacementDistScaling = 0.5f; + + [SerializeField, HideInInspector] + private float m_forwardDisplacementDistScaling = 0.5f; + + [SerializeField, HideInInspector] + private float m_backwardDisplacementDistScaling = 0.5f; + + [SerializeField, HideInInspector] + private float m_angularIntegrationStep = 0.001f * Mathf.Rad2Deg; + + [SerializeField, HideInInspector] + private agxTerrain.TerrainWheelSettings.PressureSinkageModel m_pressureSinkageModel = agxTerrain.TerrainWheelSettings.PressureSinkageModel.BEKKER; + + [SerializeField, HideInInspector] + private bool m_enableComputeRearAngleFromFrontAngle = false; + + [SerializeField, HideInInspector] + private bool m_enableComputeMaximumNormalStressAngleFromFrontAngle = true; + + [SerializeField, HideInInspector] + private float m_rearAndFrontAngleMaxMagnitude = 90.0f; + + [SerializeField, HideInInspector] + private float m_slipRatioVxThreshold = 0.01f * Mathf.Rad2Deg; + + [SerializeField, HideInInspector] + private float m_slipRatioOmegaYRThreshold = 0.01f * Mathf.Rad2Deg; + + [SerializeField, HideInInspector] + private float m_slipRatioSmoothingSpeed = 0.0001f * Mathf.Rad2Deg; + + [SerializeField, HideInInspector] + private float m_slipRatioMaxMagnitude = 1.0f; + + [SerializeField, HideInInspector] + private float m_slipRatioFallbackValue = 0.1f; + + [SerializeField, HideInInspector] + private float m_slipAngleVxAngularEquivalentThreshold = 0.017453293f * Mathf.Rad2Deg; + + [SerializeField, HideInInspector] + private float m_slipAngleVyAngularEquivalentThreshold = 0.017453293f * Mathf.Rad2Deg; + + [SerializeField, HideInInspector] + private float m_slipAngleMaxMagnitude = 45.0f; + + [SerializeField, HideInInspector] + private float m_slipAngleFallbackValue = 4.5f; + + [SerializeField, HideInInspector] + private float m_rollingModeVxAngularEquivalentThreshold = 0.017453293f * Mathf.Rad2Deg; + + [SerializeField, HideInInspector] + private float m_rollingModeOmegaYThreshold = 0.017453293f * Mathf.Rad2Deg; + #endregion + } +} diff --git a/AGXUnity/Model/DeformableTerrainWheel.cs.meta b/AGXUnity/Model/DeformableTerrainWheel.cs.meta new file mode 100644 index 00000000..a4dc6c0f --- /dev/null +++ b/AGXUnity/Model/DeformableTerrainWheel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ef20e2b7f2fff54596db2b96fc1f69e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: cff35f033a352284f861a60a27b750c2, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/AGXUnity/Model/DeformableTerrainWheelSettings.cs b/AGXUnity/Model/DeformableTerrainWheelSettings.cs new file mode 100644 index 00000000..140f2edf --- /dev/null +++ b/AGXUnity/Model/DeformableTerrainWheelSettings.cs @@ -0,0 +1,442 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace AGXUnity.Model +{ + [HelpURL( "https://us.download.algoryx.se/AGXUnity/documentation/current/editor_interface.html#deformable-terrain-wheel" )] + public class DeformableTerrainWheelSettings : ScriptAsset + { + #region Wheel Deformation Properties + [SerializeField] + private bool m_enableTerrainDeformation = true; + + [InspectorGroupBegin( Name = "Wheel Deformation Properties" )] + [Tooltip( "Determines whether this terrain wheel deforms the terrain it is in contact with." )] + public bool EnableTerrainDeformation + { + get { return m_enableTerrainDeformation; } + set + { + m_enableTerrainDeformation = value; + Propagate( wheel => wheel.getWheelDeformationProperties().setEnableDeformation( m_enableTerrainDeformation ) ); + } + } + + [SerializeField] + private bool m_enableTerrainDisplacement = true; + + [Tooltip( "Determines whether this terrain wheel displaces terrain soil to create ridges." )] + public bool EnableTerrainDisplacement + { + get { return m_enableTerrainDisplacement; } + set + { + m_enableTerrainDisplacement = value; + Propagate( wheel => wheel.getWheelDeformationProperties().setEnableDisplacement( m_enableTerrainDisplacement ) ); + } + } + + [SerializeField] + private bool m_slipDependenceBulldozing = false; + + [Tooltip( "Determines whether bulldozing displacement in front and lateral directions depends on wheel slip ratio." )] + public bool SlipDependenceBulldozing + { + get { return m_slipDependenceBulldozing; } + set + { + m_slipDependenceBulldozing = value; + Propagate( wheel => wheel.getWheelDeformationProperties().setSlipDependenceBulldozing( m_slipDependenceBulldozing ) ); + } + } + + [SerializeField] + private bool m_slipDependenceSlipDisplacement = true; + + [Tooltip( "Determines whether slip-based displacement to the rear depends on wheel slip ratio." )] + public bool SlipDependenceSlipDisplacement + { + get { return m_slipDependenceSlipDisplacement; } + set + { + m_slipDependenceSlipDisplacement = value; + Propagate( wheel => wheel.getWheelDeformationProperties().setSlipDependenceSlipDisplacement( m_slipDependenceSlipDisplacement ) ); + } + } + + [SerializeField] + [Range( 0.0f, 1.0f )] + private float m_forwardDisplacementWeight = 0.5f; + + [Tooltip( "Weight [0, 1] determining how much of the bulldozed mass is distributed forward vs. laterally." )] + public float ForwardDisplacementWeight + { + get { return m_forwardDisplacementWeight; } + set + { + m_forwardDisplacementWeight = Mathf.Clamp01(value); + Propagate( wheel => wheel.getWheelDeformationProperties().setForwardDisplacementWeight( m_forwardDisplacementWeight ) ); + } + } + + [SerializeField] + [Range( 0.0f, 1.0f )] + private float m_bulldozeDisplacementAmountFactor = 0.5f; + + [Tooltip( "Fraction [0, 1] of removed mass allocated to bulldozing. The remainder is used for slip displacement." )] + public float BulldozeDisplacementAmountFactor + { + get { return m_bulldozeDisplacementAmountFactor; } + set + { + m_bulldozeDisplacementAmountFactor = Mathf.Clamp01(value); + Propagate( wheel => wheel.getWheelDeformationProperties().setBulldozeDisplacementAmountFactor( m_bulldozeDisplacementAmountFactor ) ); + } + } + + [SerializeField] + private float m_lateralDisplacementDistScaling = 0.5f; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Scaling factor for lateral displacement distance, multiplied by wheel width." )] + public float LateralDisplacementDistScaling + { + get { return m_lateralDisplacementDistScaling; } + set + { + m_lateralDisplacementDistScaling = value; + Propagate( wheel => wheel.getWheelDeformationProperties().setLateralDisplacementDistScaling( m_lateralDisplacementDistScaling ) ); + } + } + + [SerializeField] + private float m_forwardDisplacementDistScaling = 0.5f; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Scaling factor for forward bulldozing displacement distance, multiplied by wheel radius." )] + public float ForwardDisplacementDistScaling + { + get { return m_forwardDisplacementDistScaling; } + set + { + m_forwardDisplacementDistScaling = value; + Propagate( wheel => wheel.getWheelDeformationProperties().setForwardDisplacementDistScaling( m_forwardDisplacementDistScaling ) ); + } + } + + [SerializeField] + private float m_backwardDisplacementDistScaling = 0.5f; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Scaling factor for slip-based rearward displacement distance, multiplied by wheel radius." )] + public float BackwardDisplacementDistScaling + { + get { return m_backwardDisplacementDistScaling; } + set + { + m_backwardDisplacementDistScaling = value; + Propagate( wheel => wheel.getWheelDeformationProperties().setBackwardDisplacementDistScaling( m_backwardDisplacementDistScaling ) ); + } + } + #endregion + + #region Terrain Wheel Settings + [SerializeField] + private float m_angularIntegrationStep = 0.001f * Mathf.Rad2Deg; + + [InspectorGroupBegin( Name = "Terrain Wheel Settings" )] + [ClampAboveZeroInInspector] + [Tooltip( "Angular integration step size in degrees." )] + public float AngularIntegrationStep + { + get { return m_angularIntegrationStep; } + set + { + m_angularIntegrationStep = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setAngularIntegrationStep( Mathf.Deg2Rad * m_angularIntegrationStep ) ); + } + } + + [SerializeField] + private agxTerrain.TerrainWheelSettings.PressureSinkageModel m_pressureSinkageModel = agxTerrain.TerrainWheelSettings.PressureSinkageModel.BEKKER; + + [Tooltip( "Pressure-sinkage model used by the terrain wheel." )] + public agxTerrain.TerrainWheelSettings.PressureSinkageModel PressureSinkageModel + { + get { return m_pressureSinkageModel; } + set + { + m_pressureSinkageModel = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setPressureSinkageModel( m_pressureSinkageModel ) ); + } + } + + [SerializeField] + private bool m_enableComputeRearAngleFromFrontAngle = false; + + [Tooltip( "When enabled, the rear contact angle theta_r is derived from the front contact angle theta_f." )] + public bool EnableComputeRearAngleFromFrontAngle + { + get { return m_enableComputeRearAngleFromFrontAngle; } + set + { + m_enableComputeRearAngleFromFrontAngle = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setEnableComputeRearAngleFromFrontAngle( m_enableComputeRearAngleFromFrontAngle ) ); + } + } + + [SerializeField] + private bool m_enableComputeMaximumNormalStressAngleFromFrontAngle = true; + + [Tooltip( "When enabled, the maximum normal stress angle is derived from the front contact angle." )] + public bool EnableComputeMaximumNormalStressAngleFromFrontAngle + { + get { return m_enableComputeMaximumNormalStressAngleFromFrontAngle; } + set + { + m_enableComputeMaximumNormalStressAngleFromFrontAngle = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setEnableComputeMaximumNormalStressAngleFromFrontAngle( m_enableComputeMaximumNormalStressAngleFromFrontAngle ) ); + } + } + + [SerializeField] + private float m_rearAndFrontAngleMaxMagnitude = 90.0f; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Maximum allowed magnitude for the rear and front contact angles in degrees." )] + public float RearAndFrontAngleMaxMagnitude + { + get { return m_rearAndFrontAngleMaxMagnitude; } + set + { + m_rearAndFrontAngleMaxMagnitude = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setRearAndFrontAngleMaxMagnitude( Mathf.Deg2Rad * m_rearAndFrontAngleMaxMagnitude ) ); + } + } + + [SerializeField] + private float m_slipRatioVxThreshold = 0.01f * Mathf.Rad2Deg; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Longitudinal velocity threshold for the slip-ratio dead-band in degrees/s angular equivalent." )] + public float SlipRatioVxThreshold + { + get { return m_slipRatioVxThreshold; } + set + { + m_slipRatioVxThreshold = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setSlipRatioVxAngularEquivalentThreshold( Mathf.Deg2Rad * m_slipRatioVxThreshold ) ); + } + } + + [SerializeField] + private float m_slipRatioOmegaYRThreshold = 0.01f * Mathf.Rad2Deg; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Rotational speed threshold for the slip-ratio dead-band in degrees/s." )] + public float SlipRatioOmegaYRThreshold + { + get { return m_slipRatioOmegaYRThreshold; } + set + { + m_slipRatioOmegaYRThreshold = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setSlipRatioOmegaYThreshold( Mathf.Deg2Rad * m_slipRatioOmegaYRThreshold ) ); + } + } + + [SerializeField] + private float m_slipRatioSmoothingSpeed = 0.0001f * Mathf.Rad2Deg; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Minimum angular speed used to smooth the slip-ratio computation near standstill in degrees/s." )] + public float SlipRatioSmoothingSpeed + { + get { return m_slipRatioSmoothingSpeed; } + set + { + m_slipRatioSmoothingSpeed = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setSlipRatioSmoothingAngularSpeed( Mathf.Deg2Rad * m_slipRatioSmoothingSpeed ) ); + } + } + + [SerializeField] + private float m_slipRatioMaxMagnitude = 1.0f; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Maximum allowed magnitude of the computed slip ratio." )] + public float SlipRatioMaxMagnitude + { + get { return m_slipRatioMaxMagnitude; } + set + { + m_slipRatioMaxMagnitude = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setSlipRatioMaxMagnitude( m_slipRatioMaxMagnitude ) ); + } + } + + [SerializeField] + private float m_slipRatioFallbackValue = 0.1f; + + [Tooltip( "Slip ratio fallback value when the angular equivalent of vX and omegaY are below their thresholds." )] + public float SlipRatioFallbackValue + { + get { return m_slipRatioFallbackValue; } + set + { + m_slipRatioFallbackValue = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setSlipRatioFallbackValue( m_slipRatioFallbackValue ) ); + } + } + + [SerializeField] + private float m_slipAngleVxAngularEquivalentThreshold = 0.017453293f * Mathf.Rad2Deg; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Longitudinal velocity angular equivalent threshold used by slip-angle computation in degrees/s." )] + public float SlipAngleVxAngularEquivalentThreshold + { + get { return m_slipAngleVxAngularEquivalentThreshold; } + set + { + m_slipAngleVxAngularEquivalentThreshold = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setSlipAngleVxAngularEquivalentThreshold( Mathf.Deg2Rad * m_slipAngleVxAngularEquivalentThreshold ) ); + } + } + + [SerializeField] + private float m_slipAngleVyAngularEquivalentThreshold = 0.017453293f * Mathf.Rad2Deg; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Lateral velocity angular equivalent threshold used by slip-angle computation in degrees/s." )] + public float SlipAngleVyAngularEquivalentThreshold + { + get { return m_slipAngleVyAngularEquivalentThreshold; } + set + { + m_slipAngleVyAngularEquivalentThreshold = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setSlipAngleVyAngularEquivalentThreshold( Mathf.Deg2Rad * m_slipAngleVyAngularEquivalentThreshold ) ); + } + } + + [SerializeField] + private float m_slipAngleMaxMagnitude = 45.0f; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Maximum allowed slip-angle magnitude in degrees." )] + public float SlipAngleMaxMagnitude + { + get { return m_slipAngleMaxMagnitude; } + set + { + m_slipAngleMaxMagnitude = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setSlipAngleMaxMagnitude( Mathf.Deg2Rad * m_slipAngleMaxMagnitude ) ); + } + } + + [SerializeField] + private float m_slipAngleFallbackValue = 4.5f; + + [Tooltip( "Slip-angle fallback value in degrees when vX and vY are below their thresholds." )] + public float SlipAngleFallbackValue + { + get { return m_slipAngleFallbackValue; } + set + { + m_slipAngleFallbackValue = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setSlipAngleFallbackValue( Mathf.Deg2Rad * m_slipAngleFallbackValue ) ); + } + } + + [SerializeField] + private float m_rollingModeVxAngularEquivalentThreshold = 0.017453293f * Mathf.Rad2Deg; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Longitudinal velocity angular equivalent threshold used for rolling-mode classification in degrees/s." )] + public float RollingModeVxAngularEquivalentThreshold + { + get { return m_rollingModeVxAngularEquivalentThreshold; } + set + { + m_rollingModeVxAngularEquivalentThreshold = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setRollingModeVxAngularEquivalentThreshold( Mathf.Deg2Rad * m_rollingModeVxAngularEquivalentThreshold ) ); + } + } + + [SerializeField] + private float m_rollingModeOmegaYThreshold = 0.017453293f * Mathf.Rad2Deg; + + [ClampAboveZeroInInspector( true )] + [Tooltip( "Wheel axle angular velocity magnitude threshold used for rolling-mode classification in degrees/s." )] + public float RollingModeOmegaYThreshold + { + get { return m_rollingModeOmegaYThreshold; } + set + { + m_rollingModeOmegaYThreshold = value; + Propagate( wheel => wheel.getTerrainWheelSettings().setRollingModeOmegaYThreshold( Mathf.Deg2Rad * m_rollingModeOmegaYThreshold ) ); + } + } + #endregion + + public void Synchronize( DeformableTerrainWheel wheel ) + { + try { + m_singleSynchronizeInstance = wheel; + Utils.PropertySynchronizer.Synchronize( this ); + } + finally { + m_singleSynchronizeInstance = null; + } + } + + public void Register( DeformableTerrainWheel wheel ) + { + if ( !m_wheels.Contains( wheel ) ) + m_wheels.Add( wheel ); + + Synchronize( wheel ); + } + + public void Unregister( DeformableTerrainWheel wheel ) + { + m_wheels.Remove( wheel ); + } + + public override void Destroy() + { + m_wheels.Clear(); + } + + protected override void Construct() + { + } + + protected override bool Initialize() + { + return true; + } + + private void Propagate( Action action ) + { + if ( action == null ) + return; + + if ( m_singleSynchronizeInstance != null ) { + if ( m_singleSynchronizeInstance.Native != null ) + action( m_singleSynchronizeInstance.Native ); + return; + } + + foreach ( var wheel in m_wheels ) + if ( wheel.Native != null ) + action( wheel.Native ); + } + + [NonSerialized] + private List m_wheels = new List(); + + [NonSerialized] + private DeformableTerrainWheel m_singleSynchronizeInstance = null; + } +} diff --git a/AGXUnity/Model/DeformableTerrainWheelSettings.cs.meta b/AGXUnity/Model/DeformableTerrainWheelSettings.cs.meta new file mode 100644 index 00000000..9f756a5f --- /dev/null +++ b/AGXUnity/Model/DeformableTerrainWheelSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac76a784c1584e52b09763599d934c64 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 738be52fee040d444913f70ee4f84e93, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/AGXUnity/Rendering/DebugTerrainHeightChangeRenderer.cs b/AGXUnity/Rendering/DebugTerrainHeightChangeRenderer.cs new file mode 100644 index 00000000..2aab4921 --- /dev/null +++ b/AGXUnity/Rendering/DebugTerrainHeightChangeRenderer.cs @@ -0,0 +1,435 @@ +using AGXUnity.Model; +using AGXUnity.Utils; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.Profiling; + +namespace AGXUnity.Rendering +{ + [RequireComponent( typeof( DeformableTerrainBase ) )] + [DisallowMultipleComponent] + [AddComponentMenu( "AGXUnity/Rendering/Debug Terrain Height Change Renderer" )] + public class DebugTerrainHeightChangeRenderer : ScriptComponent + { + private class TerrainDebugData + { + public Terrain Terrain; + public TerrainData TerrainData; + public TerrainLayer[] InitialLayers; + public float[,,] InitialAlphamaps; + public float[,] InitialHeights; + public float[,,] Alphamaps; + public int InitialLayerCount; + public int LoweredLayerIndex; + public int RaisedLayerIndex; + public bool IsDirty; + + public TerrainDebugData( Terrain terrain, TerrainLayer loweredLayer, TerrainLayer raisedLayer ) + { + s_createTerrainDataSampler.Begin(); + try { + Terrain = terrain; + TerrainData = terrain.terrainData; + InitialLayers = TerrainData.terrainLayers; + InitialLayerCount = InitialLayers.Length; + InitialAlphamaps = TerrainData.GetAlphamaps( 0, 0, TerrainData.alphamapWidth, TerrainData.alphamapHeight ); + InitialHeights = TerrainData.GetHeights( 0, 0, TerrainData.heightmapResolution, TerrainData.heightmapResolution ); + + var layers = InitialLayers.ToList(); + if ( !layers.Contains( loweredLayer ) ) + layers.Add( loweredLayer ); + LoweredLayerIndex = layers.IndexOf( loweredLayer ); + + if ( !layers.Contains( raisedLayer ) ) + layers.Add( raisedLayer ); + RaisedLayerIndex = layers.IndexOf( raisedLayer ); + + TerrainData.terrainLayers = layers.ToArray(); + Alphamaps = TerrainData.GetAlphamaps( 0, 0, TerrainData.alphamapWidth, TerrainData.alphamapHeight ); + RestoreAlphamapBuffer(); + } + finally { + s_createTerrainDataSampler.End(); + } + } + + public void RestoreAlphamapBuffer() + { + s_restoreAlphamapBufferSampler.Begin(); + try { + for ( int y = 0; y < TerrainData.alphamapHeight; y++ ) { + for ( int x = 0; x < TerrainData.alphamapWidth; x++ ) { + for ( int layer = 0; layer < Alphamaps.GetLength( 2 ); layer++ ) + Alphamaps[ y, x, layer ] = layer < InitialLayerCount ? InitialAlphamaps[ y, x, layer ] : 0.0f; + } + } + } + finally { + s_restoreAlphamapBufferSampler.End(); + } + } + + public void ResetTerrainData() + { + if ( TerrainData == null ) + return; + + s_resetTerrainDataSampler.Begin(); + try { + TerrainData.terrainLayers = InitialLayers; + TerrainData.SetAlphamaps( 0, 0, InitialAlphamaps ); + } + finally { + s_resetTerrainDataSampler.End(); + } + } + } + + [SerializeField] + private TerrainLayer m_loweredLayer = null; + + [IgnoreSynchronization] + [DisableInRuntimeInspector] + public TerrainLayer LoweredLayer + { + get => m_loweredLayer; + set => m_loweredLayer = value; + } + + [SerializeField] + private TerrainLayer m_raisedLayer = null; + + [IgnoreSynchronization] + [DisableInRuntimeInspector] + public TerrainLayer RaisedLayer + { + get => m_raisedLayer; + set => m_raisedLayer = value; + } + + [field: SerializeField] + [DisableInRuntimeInspector] + [Tooltip( "Fallback color used when Lowered Layer is not assigned." )] + public Color LoweredColor { get; set; } = new Color( 0.85f, 0.18f, 0.08f, 1.0f ); + + [field: SerializeField] + [DisableInRuntimeInspector] + [Tooltip( "Fallback color used when Raised Layer is not assigned." )] + public Color RaisedColor { get; set; } = new Color( 0.12f, 0.45f, 1.0f, 1.0f ); + + [field: SerializeField] + [Tooltip( "Whether lowered terrain should be painted." )] + public bool EnableLowered { get; set; } = true; + + [field: SerializeField] + [Tooltip( "Whether raised terrain should be painted." )] + public bool EnableRaised { get; set; } = true; + + [field: SerializeField] + [ClampAboveZeroInInspector( true )] + [Tooltip( "Minimum absolute height change in meters required before a terrain texel is painted." )] + public float HeightThreshold { get; set; } = 0.01f; + + [field: SerializeField] + [Range( 0.0f, 1.0f )] + [Tooltip( "Blend weight of the debug layer over the original terrain paint." )] + public float PaintStrength = 0.7f; + + [field: SerializeField] + [ClampAboveZeroInInspector( true )] + [Tooltip( "Absolute height change in meters where the debug layer reaches Paint Strength." )] + public float FullStrengthHeightChange { get; set; } = 0.1f; + + [field: SerializeField] + [Min( 0 )] + [Tooltip( "Paint radius in heightmap samples around each modified terrain vertex." )] + public int BrushRadius = 0; + + public void Clear() + { + s_clearSampler.Begin(); + try { + foreach ( var data in m_terrainData.Values ) { + data.RestoreAlphamapBuffer(); + data.TerrainData.SetAlphamaps( 0, 0, data.Alphamaps ); + data.IsDirty = false; + } + m_dirtyTerrains.Clear(); + } + finally { + s_clearSampler.End(); + } + } + + protected override bool Initialize() + { + if ( GetComponent() != null ) { + Debug.LogError( "DebugTerrainHeightChangeRenderer cannot be used together with TerrainPatchRenderer on the same terrain.", this ); + return false; + } + + m_terrain = gameObject.GetInitializedComponent(); + if ( m_terrain == null ) + return false; + + var rootTerrain = GetComponent(); + if ( rootTerrain == null ) { + Debug.LogError( "DebugTerrainHeightChangeRenderer requires a Unity Terrain and does not support mesh based terrains.", this ); + return false; + } + + if ( !CreateDefaultLayers() ) + return false; + + foreach ( var terrain in TerrainUtils.CollectTerrains( rootTerrain ) ) + EnsureTerrainData( terrain ); + + Subscribe(); + + return true; + } + + protected override void OnApplicationQuit() + { + RestoreTerrains(); + } + + protected override void OnDestroy() + { + Unsubscribe(); + RestoreTerrains(); + DestroyRuntimeLayer( m_runtimeLoweredLayer ); + DestroyRuntimeLayer( m_runtimeRaisedLayer ); + + base.OnDestroy(); + } + + protected override void OnDisable() + { + Unsubscribe(); + base.OnDisable(); + } + + protected override void OnEnable() + { + if ( State == States.INITIALIZED ) + Subscribe(); + + base.OnEnable(); + } + + private bool CreateDefaultLayers() + { + if ( m_loweredLayer == null ) { + m_runtimeLoweredLayer = CreateRuntimeLayer( "Debug Lowered Terrain Layer", LoweredColor ); + m_loweredLayer = m_runtimeLoweredLayer; + } + + if ( m_raisedLayer == null ) { + m_runtimeRaisedLayer = CreateRuntimeLayer( "Debug Raised Terrain Layer", RaisedColor ); + m_raisedLayer = m_runtimeRaisedLayer; + } + + if ( m_loweredLayer == m_raisedLayer ) { + Debug.LogError( "DebugTerrainHeightChangeRenderer requires separate terrain layers for lowered and raised terrain.", this ); + return false; + } + + return true; + } + + private static TerrainLayer CreateRuntimeLayer( string layerName, Color color ) + { + var texture = new Texture2D( 1, 1, TextureFormat.RGBA32, false ); + texture.name = layerName + " Texture"; + texture.hideFlags = HideFlags.HideAndDontSave; + texture.SetPixel( 0, 0, color ); + texture.Apply(); + + var layer = new TerrainLayer(); + layer.name = layerName; + layer.hideFlags = HideFlags.HideAndDontSave; + layer.diffuseTexture = texture; + layer.tileSize = Vector2.one; + return layer; + } + + private static void DestroyRuntimeLayer( TerrainLayer layer ) + { + if ( layer == null ) + return; + + var texture = layer.diffuseTexture; + if ( Application.isPlaying ) { + Object.Destroy( layer ); + if ( texture != null ) + Object.Destroy( texture ); + } + else { + Object.DestroyImmediate( layer ); + if ( texture != null ) + Object.DestroyImmediate( texture ); + } + } + + private void Subscribe() + { + if ( m_isSubscribed || m_terrain == null || !Simulation.HasInstance ) + return; + + Simulation.Instance.StepCallbacks.SimulationPost += PostStep; + m_terrain.OnModification += UpdateTextureAt; + m_isSubscribed = true; + } + + private void Unsubscribe() + { + if ( !m_isSubscribed ) + return; + + if ( Simulation.HasInstance ) { + Simulation.Instance.StepCallbacks.SimulationPost -= PostStep; + if ( m_terrain != null ) + m_terrain.OnModification -= UpdateTextureAt; + } + m_isSubscribed = false; + } + + private TerrainDebugData EnsureTerrainData( Terrain terrain ) + { + if ( terrain == null ) + return null; + + if ( m_terrainData.TryGetValue( terrain, out var data ) ) + return data; + + data = new TerrainDebugData( terrain, m_loweredLayer, m_raisedLayer ); + m_terrainData.Add( terrain, data ); + return data; + } + + private void RestoreTerrains() + { + foreach ( var data in m_terrainData.Values ) + data.ResetTerrainData(); + + m_dirtyTerrains.Clear(); + } + + private void PostStep() + { + s_postStepSampler.Begin(); + try { + foreach ( var data in m_dirtyTerrains ) { + if ( data.TerrainData != null ) { + s_setAlphamapsSampler.Begin(); + try { + data.TerrainData.SetAlphamaps( 0, 0, data.Alphamaps ); + } + finally { + s_setAlphamapsSampler.End(); + } + } + data.IsDirty = false; + } + + m_dirtyTerrains.Clear(); + } + finally { + s_postStepSampler.End(); + } + } + + private void UpdateTextureAt( agxTerrain.Terrain aTerr, agx.Vec2i aIdx, Terrain uTerr, Vector2Int uIdx ) + { + s_updateTextureAtSampler.Begin(); + try { + var data = EnsureTerrainData( uTerr ); + if ( data == null ) + return; + + var td = data.TerrainData; + var baselineHeight = data.InitialHeights[ uIdx.y, uIdx.x ] * td.heightmapScale.y; + var currentHeight = td.GetHeight( uIdx.x, uIdx.y ); + var delta = currentHeight - baselineHeight; + + var debugLayer = -1; + var absDelta = Mathf.Abs( delta ); + if ( EnableLowered && delta < -HeightThreshold ) + debugLayer = data.LoweredLayerIndex; + else if ( EnableRaised && delta > HeightThreshold ) + debugLayer = data.RaisedLayerIndex; + + var fullStrengthHeightChange = Mathf.Max( FullStrengthHeightChange, HeightThreshold + Mathf.Epsilon ); + var strength = debugLayer >= 0 ? + Mathf.Clamp01( PaintStrength ) * + Mathf.InverseLerp( HeightThreshold, fullStrengthHeightChange, absDelta ) : + 0.0f; + + Paint( data, uIdx, debugLayer, strength ); + + if ( !data.IsDirty ) { + data.IsDirty = true; + m_dirtyTerrains.Add( data ); + } + } + finally { + s_updateTextureAtSampler.End(); + } + } + + private void Paint( TerrainDebugData data, Vector2Int heightIndex, int debugLayer, float strength ) + { + s_paintSampler.Begin(); + try { + var td = data.TerrainData; + var alphamapWidth = td.alphamapWidth; + var alphamapHeight = td.alphamapHeight; + var heightmapMaxX = td.heightmapResolution - 1; + var heightmapMaxY = td.heightmapResolution - 1; + + var minAlphaX = Mathf.FloorToInt( ( heightIndex.x - 0.5f - BrushRadius ) / heightmapMaxX * alphamapWidth ); + var minAlphaY = Mathf.FloorToInt( ( heightIndex.y - 0.5f - BrushRadius ) / heightmapMaxY * alphamapHeight ); + var maxAlphaX = Mathf.CeilToInt( ( heightIndex.x + 0.5f + BrushRadius ) / heightmapMaxX * alphamapWidth ); + var maxAlphaY = Mathf.CeilToInt( ( heightIndex.y + 0.5f + BrushRadius ) / heightmapMaxY * alphamapHeight ); + + minAlphaX = Mathf.Clamp( minAlphaX, 0, alphamapWidth ); + minAlphaY = Mathf.Clamp( minAlphaY, 0, alphamapHeight ); + maxAlphaX = Mathf.Clamp( Mathf.Max( maxAlphaX, minAlphaX + 1 ), 0, alphamapWidth ); + maxAlphaY = Mathf.Clamp( Mathf.Max( maxAlphaY, minAlphaY + 1 ), 0, alphamapHeight ); + + for ( int y = minAlphaY; y < maxAlphaY; y++ ) { + for ( int x = minAlphaX; x < maxAlphaX; x++ ) { + for ( int layer = 0; layer < data.Alphamaps.GetLength( 2 ); layer++ ) { + var original = layer < data.InitialLayerCount ? data.InitialAlphamaps[ y, x, layer ] : 0.0f; + data.Alphamaps[ y, x, layer ] = original * ( 1.0f - strength ); + } + + if ( debugLayer >= 0 ) + data.Alphamaps[ y, x, debugLayer ] += strength; + } + } + } + finally { + s_paintSampler.End(); + } + } + + private static readonly CustomSampler s_createTerrainDataSampler = CustomSampler.Create( "DebugTerrainHeightChangeRenderer.CreateTerrainData" ); + private static readonly CustomSampler s_restoreAlphamapBufferSampler = CustomSampler.Create( "DebugTerrainHeightChangeRenderer.RestoreAlphamapBuffer" ); + private static readonly CustomSampler s_resetTerrainDataSampler = CustomSampler.Create( "DebugTerrainHeightChangeRenderer.ResetTerrainData" ); + private static readonly CustomSampler s_clearSampler = CustomSampler.Create( "DebugTerrainHeightChangeRenderer.Clear" ); + private static readonly CustomSampler s_postStepSampler = CustomSampler.Create( "DebugTerrainHeightChangeRenderer.PostStep" ); + private static readonly CustomSampler s_setAlphamapsSampler = CustomSampler.Create( "DebugTerrainHeightChangeRenderer.SetAlphamaps" ); + private static readonly CustomSampler s_updateTextureAtSampler = CustomSampler.Create( "DebugTerrainHeightChangeRenderer.UpdateTextureAt" ); + private static readonly CustomSampler s_paintSampler = CustomSampler.Create( "DebugTerrainHeightChangeRenderer.Paint" ); + + private readonly Dictionary m_terrainData = new Dictionary(); + private readonly List m_dirtyTerrains = new List(); + private DeformableTerrainBase m_terrain = null; + private TerrainLayer m_runtimeLoweredLayer = null; + private TerrainLayer m_runtimeRaisedLayer = null; + private bool m_isSubscribed = false; + } +} diff --git a/AGXUnity/Rendering/DebugTerrainHeightChangeRenderer.cs.meta b/AGXUnity/Rendering/DebugTerrainHeightChangeRenderer.cs.meta new file mode 100644 index 00000000..d165d03f --- /dev/null +++ b/AGXUnity/Rendering/DebugTerrainHeightChangeRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4ca1207248854d04bb8a5dcb622e88d1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 9f93534f916729d498ce510fd82d6448, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/AGXUnityEditor/Menus/AssetsMenu.cs b/Editor/AGXUnityEditor/Menus/AssetsMenu.cs index ba267439..28eb4e10 100644 --- a/Editor/AGXUnityEditor/Menus/AssetsMenu.cs +++ b/Editor/AGXUnityEditor/Menus/AssetsMenu.cs @@ -81,6 +81,12 @@ public static Object CrateDeformableTerrainShovelSettings() return Selection.activeObject = Utils.AssetFactory.Create( "deformable terrain shovel settings" ); } + [MenuItem( "Assets/AGXUnity/Deformable Terrain Wheel Settings", priority = 700 )] + public static Object CrateDeformableTerrainWheelSettings() + { + return Selection.activeObject = Utils.AssetFactory.Create( "deformable terrain wheel settings" ); + } + [MenuItem( "Assets/AGXUnity/Track Properties", priority = 720 )] public static Object CreateTrackProperties() { diff --git a/Editor/AGXUnityEditor/Menus/TopMenu.cs b/Editor/AGXUnityEditor/Menus/TopMenu.cs index f32d2a4f..47a6ed13 100644 --- a/Editor/AGXUnityEditor/Menus/TopMenu.cs +++ b/Editor/AGXUnityEditor/Menus/TopMenu.cs @@ -299,6 +299,13 @@ public static GameObject CreateTrack( MenuCommand command ) return Selection.activeGameObject = CreateModel( command ); } + [MenuItem( "AGXUnity/Model/Deformable Terrain Wheel", priority = 50 )] + [MenuItem( "GameObject/AGXUnity/Model/Deformable Terrain Wheel", validate = false, priority = 10 )] + public static GameObject CreateDeformableTerrainWheel( MenuCommand command ) + { + return Selection.activeGameObject = CreateModel( command ); + } + [MenuItem( "AGXUnity/Model/Deformable Terrain", priority = 50 )] [MenuItem( "GameObject/AGXUnity/Model/Deformable Terrain", validate = false, priority = 10 )] public static GameObject CreateDeformableTerrain( MenuCommand command ) diff --git a/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelEditor.cs b/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelEditor.cs new file mode 100644 index 00000000..d902f6a3 --- /dev/null +++ b/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelEditor.cs @@ -0,0 +1,9 @@ +using UnityEditor; + +namespace AGXUnityEditor.Editors +{ + [CustomEditor( typeof( AGXUnity.Model.DeformableTerrainWheel ) )] + [CanEditMultipleObjects] + public class AGXUnityModelDeformableTerrainWheelEditor : InspectorEditor + { } +} diff --git a/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelEditor.cs.meta b/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelEditor.cs.meta new file mode 100644 index 00000000..a37fb5e8 --- /dev/null +++ b/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 70585192bc24f4a40ad7f9d25af315d1 \ No newline at end of file diff --git a/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelSettingsEditor.cs b/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelSettingsEditor.cs new file mode 100644 index 00000000..2115b48c --- /dev/null +++ b/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelSettingsEditor.cs @@ -0,0 +1,9 @@ +using UnityEditor; + +namespace AGXUnityEditor.Editors +{ + [CustomEditor( typeof( AGXUnity.Model.DeformableTerrainWheelSettings ) )] + [CanEditMultipleObjects] + public class AGXUnityModelDeformableTerrainWheelSettingsEditor : InspectorEditor + { } +} diff --git a/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelSettingsEditor.cs.meta b/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelSettingsEditor.cs.meta new file mode 100644 index 00000000..dc93f94f --- /dev/null +++ b/Editor/CustomEditors/AGXUnity+Model+DeformableTerrainWheelSettingsEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 20011a9409d8424f994d245b8b103e4e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/CustomEditors/AGXUnity+Rendering+DebugTerrainHeightChangeRendererEditor.cs b/Editor/CustomEditors/AGXUnity+Rendering+DebugTerrainHeightChangeRendererEditor.cs new file mode 100644 index 00000000..66779a64 --- /dev/null +++ b/Editor/CustomEditors/AGXUnity+Rendering+DebugTerrainHeightChangeRendererEditor.cs @@ -0,0 +1,9 @@ +using UnityEditor; + +namespace AGXUnityEditor.Editors +{ + [CustomEditor( typeof( AGXUnity.Rendering.DebugTerrainHeightChangeRenderer ) )] + [CanEditMultipleObjects] + public class AGXUnityRenderingDebugTerrainHeightChangeRendererEditor : InspectorEditor + { } +} diff --git a/Editor/CustomEditors/AGXUnity+Rendering+DebugTerrainHeightChangeRendererEditor.cs.meta b/Editor/CustomEditors/AGXUnity+Rendering+DebugTerrainHeightChangeRendererEditor.cs.meta new file mode 100644 index 00000000..d9a51e4c --- /dev/null +++ b/Editor/CustomEditors/AGXUnity+Rendering+DebugTerrainHeightChangeRendererEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 880c0a523f138aa468e43927ac18b6ee \ No newline at end of file diff --git a/Plugins/x86_64/agxDotNet.dll b/Plugins/x86_64/agxDotNet.dll index 42dc4f39..baf26ac6 100644 Binary files a/Plugins/x86_64/agxDotNet.dll and b/Plugins/x86_64/agxDotNet.dll differ diff --git a/Tests/Runtime/DeformableTerrainWheelTests.cs b/Tests/Runtime/DeformableTerrainWheelTests.cs new file mode 100644 index 00000000..55323558 --- /dev/null +++ b/Tests/Runtime/DeformableTerrainWheelTests.cs @@ -0,0 +1,144 @@ +using AGXUnity; +using AGXUnity.Collide; +using AGXUnity.Model; +using NUnit.Framework; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; + +namespace AGXUnityTesting.Runtime +{ + public class DeformableTerrainWheelTests : AGXUnityFixture + { + private const float WheelRadius = 0.2f; + private const float WheelHeight = 0.2f; + private const float WheelClearance = 0.05f; + + [UnityTest] + public IEnumerator CreateAndDestroyWithoutCylinder() + { + var go = Factory.Create(); + var wheel = go.AddComponent(); + + LogAssert.Expect( LogType.Warning, "DeformableTerrainWheel requires exactly 1 Cylinder shape in the RigidBody, found 0." ); + + TestUtils.InitializeAll(); + + Assert.That( wheel.Native, Is.Null ); + + yield return TestUtils.DestroyAndWait( go ); + } + + [UnityTest] + public IEnumerator CreateAndDestroyWithCylinder() + { + var cylinderGO = Factory.Create(); + var cylinder = cylinderGO.GetComponent(); + cylinder.Material = ShapeMaterial.CreateInstance(); + + var go = Factory.Create( cylinderGO ); + var wheel = go.AddComponent(); + + TestUtils.InitializeAll(); + + Assert.That( wheel.Native, Is.Not.Null ); + + yield return TestUtils.DestroyAndWait( go ); + + Assert.That( wheel.Native, Is.Null ); + } + + [UnityTest] + public IEnumerator WheelWithoutMaterialsDoesntWork() + { + var (terrain, wheel) = CreateTerrainAndWheel(); + + TestUtils.InitializeAll(); + + for ( int i = 0; i < 20; ++i ) + yield return TestUtils.Step(); + + Assert.That( terrain.Native, Is.Not.Null ); + Assert.That( wheel.Native, Is.Not.Null ); + Assert.That( wheel.ActiveContactMaterialUsesTerrainWheelForceModel, Is.False ); + } + + [UnityTest] + public IEnumerator WheelWithWrongFrictionModel() + { + var (terrain, wheel) = CreateTerrainWheelAndContactMaterial(); + + TestUtils.InitializeAll(); + + for ( int i = 0; i < 20; ++i ) + yield return TestUtils.Step(); + + Assert.That( terrain.Native, Is.Not.Null ); + Assert.That( wheel.Native, Is.Not.Null ); + Assert.That( wheel.ActiveContactMaterialUsesTerrainWheelForceModel, Is.False ); + } + + [UnityTest] + public IEnumerator WheelWithCorrectFrictionModel() + { + var frictionModel = FrictionModel.CreateInstance(); + frictionModel.Type = FrictionModel.EType.TerrainWheelForceModel; + var (terrain, wheel) = CreateTerrainWheelAndContactMaterial( frictionModel ); + + TestUtils.InitializeAll(); + + for ( int i = 0; i < 20; ++i ) + yield return TestUtils.Step(); + + Assert.That( terrain.Native, Is.Not.Null ); + Assert.That( wheel.Native, Is.Not.Null ); + Assert.That( wheel.ActiveContactMaterialUsesTerrainWheelForceModel, Is.True ); + } + + private (DeformableTerrain terrain, DeformableTerrainWheel wheel) CreateTerrainAndWheel() + { + var terrainGO = new GameObject( "Deformable Terrain" ); + var unityTerrain = terrainGO.AddComponent(); + unityTerrain.terrainData = new TerrainData(); + unityTerrain.terrainData.size = new Vector3( 2.0f, 1.0f, 2.0f ); + unityTerrain.terrainData.heightmapResolution = 33; + + var terrain = terrainGO.AddComponent(); + terrain.MaximumDepth = 0.5f; + + var cylinderGO = Factory.Create(); + var cylinder = cylinderGO.GetComponent(); + cylinder.Radius = WheelRadius; + cylinder.Height = WheelHeight; + cylinder.Material = ShapeMaterial.CreateInstance(); + + var wheelGO = Factory.Create( cylinderGO ); + wheelGO.transform.position = new Vector3( 1.0f, WheelRadius + WheelClearance, 1.0f ); + wheelGO.transform.rotation = Quaternion.Euler( 90.0f, 0.0f, 0.0f ); + + var wheel = wheelGO.AddComponent(); + + return (terrain, wheel); + } + + private (DeformableTerrain terrain, DeformableTerrainWheel wheel) CreateTerrainWheelAndContactMaterial( FrictionModel frictionModel = null ) + { + var (terrain, wheel) = CreateTerrainAndWheel(); + + var wheelMaterial = ShapeMaterial.CreateInstance(); + var terrainMaterial = ShapeMaterial.CreateInstance(); + + wheel.RigidBody.GetComponentInChildren().Material = wheelMaterial; + terrain.Material = terrainMaterial; + + var contactMaterial = ContactMaterial.CreateInstance(); + contactMaterial.Material1 = wheelMaterial; + contactMaterial.Material2 = terrainMaterial; + contactMaterial.FrictionModel = frictionModel; + + ContactMaterialManager.Instance.Add( contactMaterial ); + + return (terrain, wheel); + } + } +} diff --git a/Tests/Runtime/DeformableTerrainWheelTests.cs.meta b/Tests/Runtime/DeformableTerrainWheelTests.cs.meta new file mode 100644 index 00000000..5a31d46f --- /dev/null +++ b/Tests/Runtime/DeformableTerrainWheelTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5f4fb99dc4f245d4cb8e09f3808ab546 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: