diff --git a/sources/editor/Xenko.Assets.Presentation/Preview/HeightmapPreview.cs b/sources/editor/Xenko.Assets.Presentation/Preview/HeightmapPreview.cs new file mode 100644 index 0000000000..012a13e740 --- /dev/null +++ b/sources/editor/Xenko.Assets.Presentation/Preview/HeightmapPreview.cs @@ -0,0 +1,95 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using Xenko.Assets.Physics; +using Xenko.Assets.Presentation.Preview.Views; +using Xenko.Core.Mathematics; +using Xenko.Editor.Preview; +using Xenko.Graphics; +using Xenko.Physics; + +namespace Xenko.Assets.Presentation.Preview +{ + [AssetPreview(typeof(HeightmapAsset), typeof(HeightmapPreviewView))] + public class HeightmapPreview : PreviewFromSpriteBatch + { + private Heightmap heightmap; + private Texture heightmapTexture; + private BlendStateDescription adequateBlendState; + + public int Width => heightmap?.Size.X ?? 0; + public int Length => heightmap?.Size.Y ?? 0; + + /// + /// Gets or sets a callback that will be invoked when the texture is loaded. + /// + public Action NotifyHeightmapLoaded { get; set; } + + protected override Vector2 SpriteSize + { + get + { + if (heightmapTexture == null) + return base.SpriteSize; + + return new Vector2(heightmapTexture.Width, heightmapTexture.Height); + } + } + + protected virtual Vector2 ImageCenter + { + get + { + if (heightmapTexture == null) + return Vector2.Zero; + + var imageSize = new Vector2(heightmapTexture.Width, heightmapTexture.Height); + + return imageSize / 2f; + } + } + + protected override void LoadContent() + { + heightmap = LoadAsset(AssetItem.Location); + + heightmapTexture = heightmap?.CreateTexture(Game.GraphicsDevice); + + adequateBlendState = BlendStates.Opaque; + + NotifyHeightmapLoaded?.Invoke(); + + // Always use LDR + RenderingMode = RenderingMode.LDR; + } + + protected override void UnloadContent() + { + if (heightmapTexture != null) + { + heightmapTexture.Dispose(); + heightmapTexture = null; + } + + if (heightmap != null) + { + UnloadAsset(heightmap); + heightmap = null; + } + } + + protected override void RenderSprite() + { + if (heightmapTexture == null) + return; + + var origin = ImageCenter - SpriteOffsets; + var region = new RectangleF(0, 0, heightmapTexture.Width, heightmapTexture.Height); + var orientation = ImageOrientation.AsIs; + + SpriteBatch.Begin(Game.GraphicsContext, SpriteSortMode.Texture, adequateBlendState); + SpriteBatch.Draw(heightmapTexture, WindowSize / 2, region, Color.White, 0, origin, SpriteScale, SpriteEffects.None, orientation, swizzle: SwizzleMode.RRR1); + SpriteBatch.End(); + } + } +} diff --git a/sources/editor/Xenko.Assets.Presentation/Preview/Views/HeightmapPreviewView.cs b/sources/editor/Xenko.Assets.Presentation/Preview/Views/HeightmapPreviewView.cs new file mode 100644 index 0000000000..0360b49f93 --- /dev/null +++ b/sources/editor/Xenko.Assets.Presentation/Preview/Views/HeightmapPreviewView.cs @@ -0,0 +1,15 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System.Windows; +using Xenko.Editor.Preview.View; + +namespace Xenko.Assets.Presentation.Preview.Views +{ + public class HeightmapPreviewView : XenkoPreviewView + { + static HeightmapPreviewView() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(HeightmapPreviewView), new FrameworkPropertyMetadata(typeof(HeightmapPreviewView))); + } + } +} diff --git a/sources/editor/Xenko.Assets.Presentation/Templates/Assets/Physics/ColliderShapeHeightfield.xktpl b/sources/editor/Xenko.Assets.Presentation/Templates/Assets/Physics/ColliderShapeHeightfield.xktpl new file mode 100644 index 0000000000..7d205a0f58 --- /dev/null +++ b/sources/editor/Xenko.Assets.Presentation/Templates/Assets/Physics/ColliderShapeHeightfield.xktpl @@ -0,0 +1,10 @@ +!TemplateAssetFactory +Id: 1D3C9128-C94B-413B-9ABB-80ADDD372F61 +AssetTypeName: ColliderShapeAsset +Name: Heightfield +Scope: Asset +Description: A heightfield collider +Group: Physics +Icon: ..\.xktpl\ColliderPlane.png +DefaultOutputName: ColliderHeightfield +FactoryTypeName: ColliderShapeHeightfieldFactory diff --git a/sources/editor/Xenko.Assets.Presentation/Templates/Assets/Physics/Heightmap.xktpl b/sources/editor/Xenko.Assets.Presentation/Templates/Assets/Physics/Heightmap.xktpl new file mode 100644 index 0000000000..a9d7b17fe3 --- /dev/null +++ b/sources/editor/Xenko.Assets.Presentation/Templates/Assets/Physics/Heightmap.xktpl @@ -0,0 +1,12 @@ +!TemplateAssetFactory +Id: 127EC64F-6E15-4964-98F4-DB735B39AE09 +AssetTypeName: HeightmapAsset +Name: Heightmap +Scope: Asset +Description: A heightmap for heightfield +Group: Physics +Order: 100 +Icon: ..\.xktpl\TextureGray.png +DefaultOutputName: Heightmap +FactoryTypeName: HeightmapFactory +ImportSource: true diff --git a/sources/editor/Xenko.Assets.Presentation/Templates/HeightmapFactoryTemplateGenerator.cs b/sources/editor/Xenko.Assets.Presentation/Templates/HeightmapFactoryTemplateGenerator.cs new file mode 100644 index 0000000000..37f27119d0 --- /dev/null +++ b/sources/editor/Xenko.Assets.Presentation/Templates/HeightmapFactoryTemplateGenerator.cs @@ -0,0 +1,45 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xenko.Assets.Textures; +using Xenko.Core; +using Xenko.Core.Assets.Editor.Settings; +using Xenko.Core.Assets.IO; +using Xenko.Core.Assets.Templates; +using Xenko.Core.IO; +using Xenko.Core.Reflection; + +namespace Xenko.Assets.Presentation.Templates +{ + public class HeightmapFactoryTemplateGenerator : AssetFromFileTemplateGenerator + { + public new static readonly HeightmapFactoryTemplateGenerator Default = new HeightmapFactoryTemplateGenerator(); + + public static readonly Guid TemplateId = new Guid("127EC64F-6E15-4964-98F4-DB735B39AE09"); + + public override bool IsSupportingTemplate(TemplateDescription templateDescription) + { + if (templateDescription == null) throw new ArgumentNullException(nameof(templateDescription)); + return templateDescription.Id == TemplateId; + } + + protected override async Task> BrowseForSourceFiles(TemplateAssetDescription description, bool allowMultiSelection) + { + var assetType = description.GetAssetType(); + var assetTypeName = TypeDescriptorFactory.Default.AttributeRegistry.GetAttribute(assetType)?.Name ?? assetType.Name; + var extensions = new FileExtensionCollection($"Source files for {assetTypeName}", TextureImporter.FileExtensions); + var result = await BrowseForFiles(extensions, allowMultiSelection, true, InternalSettings.FileDialogLastImportDirectory.GetValue()); + if (result != null) + { + var list = result.ToList(); + InternalSettings.FileDialogLastImportDirectory.SetValue(list.First()); + InternalSettings.Save(); + return list; + } + return null; + } + } +} diff --git a/sources/editor/Xenko.Assets.Presentation/Templates/XenkoTemplates.cs b/sources/editor/Xenko.Assets.Presentation/Templates/XenkoTemplates.cs index 7da97da3e4..5c7919ce70 100644 --- a/sources/editor/Xenko.Assets.Presentation/Templates/XenkoTemplates.cs +++ b/sources/editor/Xenko.Assets.Presentation/Templates/XenkoTemplates.cs @@ -16,6 +16,7 @@ public static void Register() TemplateManager.Register(AssetFactoryTemplateGenerator.Default); TemplateManager.Register(AssetFromFileTemplateGenerator.Default); // Specific asset templates must be registered after AssetFactoryTemplateGenerator + TemplateManager.Register(HeightmapFactoryTemplateGenerator.Default); TemplateManager.Register(ColliderShapeHullFactoryTemplateGenerator.Default); TemplateManager.Register(ProceduralModelFactoryTemplateGenerator.Default); TemplateManager.Register(SkyboxFactoryTemplateGenerator.Default); diff --git a/sources/editor/Xenko.Assets.Presentation/Themes/Generic.xaml b/sources/editor/Xenko.Assets.Presentation/Themes/Generic.xaml index 2d7fe197e6..9558453c1e 100644 --- a/sources/editor/Xenko.Assets.Presentation/Themes/Generic.xaml +++ b/sources/editor/Xenko.Assets.Presentation/Themes/Generic.xaml @@ -397,4 +397,61 @@ + + diff --git a/sources/editor/Xenko.Assets.Presentation/Thumbnails/HeightmapThumbnailCompiler.cs b/sources/editor/Xenko.Assets.Presentation/Thumbnails/HeightmapThumbnailCompiler.cs new file mode 100644 index 0000000000..c9872d02a3 --- /dev/null +++ b/sources/editor/Xenko.Assets.Presentation/Thumbnails/HeightmapThumbnailCompiler.cs @@ -0,0 +1,87 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System.Collections.Generic; +using Xenko.Assets.Physics; +using Xenko.Core.Assets; +using Xenko.Core.Assets.Compiler; +using Xenko.Core.Mathematics; +using Xenko.Core.Serialization.Contents; +using Xenko.Editor.Thumbnails; +using Xenko.Graphics; +using Xenko.Physics; +using Xenko.Rendering; + +namespace Xenko.Assets.Presentation.Thumbnails +{ + [AssetCompiler(typeof(HeightmapAsset), typeof(ThumbnailCompilationContext))] + public class HeightmapThumbnailCompiler : ThumbnailCompilerBase + { + public HeightmapThumbnailCompiler() + { + IsStatic = false; + Priority = 10050; + } + + public override IEnumerable GetInputFiles(AssetItem assetItem) + { + var asset = (HeightmapAsset)assetItem.Asset; + var url = asset.Source.FullPath; + if (!string.IsNullOrEmpty(url)) + { + yield return new ObjectUrl(UrlType.File, url); + } + } + + protected override void CompileThumbnail(ThumbnailCompilerContext context, string thumbnailStorageUrl, AssetItem assetItem, Package originalPackage, AssetCompilerResult result) + { + result.BuildSteps.Add(new ThumbnailBuildStep(new HeightmapThumbnailCommand(context, assetItem, originalPackage, thumbnailStorageUrl, + new ThumbnailCommandParameters(assetItem.Asset, thumbnailStorageUrl, context.ThumbnailResolution)) + { InputFilesGetter = () => GetInputFiles(assetItem) })); + } + + /// + /// Command used to build the thumbnail of the texture in the storage + /// + public class HeightmapThumbnailCommand : ThumbnailFromSpriteBatchCommand + { + private Texture texture; + + public HeightmapThumbnailCommand(ThumbnailCompilerContext context, AssetItem assetItem, IAssetFinder assetFinder, string url, ThumbnailCommandParameters parameters) + : base(context, assetItem, assetFinder, url, parameters) + { + parameters.ColorSpace = ColorSpace.Linear; + } + + protected override void PreloadAsset() + { + base.PreloadAsset(); + + texture = LoadedAsset?.CreateTexture(GraphicsDevice); + } + + protected override void UnloadAsset() + { + if (texture != null) + { + texture.Dispose(); + texture = null; + } + + base.UnloadAsset(); + } + + protected override void RenderSprites(RenderDrawContext context) + { + if (LoadedAsset == null) + return; + + if (texture != null) + { + var destinationRectangle = new RectangleF(0, 0, Parameters.ThumbnailSize.X, Parameters.ThumbnailSize.Y); + + SpriteBatch.Draw(texture, destinationRectangle, new RectangleF(0, 0, texture.Width, texture.Height), Color.White, 0f, new Vector2(0, 0), SpriteEffects.None, swizzle: SwizzleMode.RRR1); + } + } + } + } +} diff --git a/sources/editor/Xenko.Assets.Presentation/ViewModel/Preview/HeightmapPreviewViewModel.cs b/sources/editor/Xenko.Assets.Presentation/ViewModel/Preview/HeightmapPreviewViewModel.cs new file mode 100644 index 0000000000..6ed79b697d --- /dev/null +++ b/sources/editor/Xenko.Assets.Presentation/ViewModel/Preview/HeightmapPreviewViewModel.cs @@ -0,0 +1,40 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Assets.Presentation.Preview; +using Xenko.Core.Assets.Editor.ViewModel; +using Xenko.Editor.Preview; +using Xenko.Editor.Preview.ViewModel; + +namespace Xenko.Assets.Presentation.ViewModel.Preview +{ + [AssetPreviewViewModel(typeof(HeightmapPreview))] + public class HeightmapPreviewViewModel : TextureBasePreviewViewModel + { + private HeightmapPreview heightmapPreview; + private int previewHeightmapLength; + private int previewHeightmapWidth; + + public HeightmapPreviewViewModel(SessionViewModel session) + : base(session) + { + } + + public int PreviewHeightmapLength { get { return previewHeightmapLength; } private set { SetValue(ref previewHeightmapLength, value); } } + + public int PreviewHeightmapWidth { get { return previewHeightmapWidth; } private set { SetValue(ref previewHeightmapWidth, value); } } + + public override void AttachPreview(IAssetPreview preview) + { + heightmapPreview = (HeightmapPreview)preview; + heightmapPreview.NotifyHeightmapLoaded += UpdateHeightmapInfo; + UpdateHeightmapInfo(); + AttachPreviewTexture(preview); + } + + private void UpdateHeightmapInfo() + { + PreviewHeightmapWidth = heightmapPreview.Width; + PreviewHeightmapLength = heightmapPreview.Length; + } + } +} diff --git a/sources/editor/Xenko.Assets.Presentation/Xenko.Assets.Presentation.xkpkg b/sources/editor/Xenko.Assets.Presentation/Xenko.Assets.Presentation.xkpkg index 28a607fd6d..2c04441615 100644 --- a/sources/editor/Xenko.Assets.Presentation/Xenko.Assets.Presentation.xkpkg +++ b/sources/editor/Xenko.Assets.Presentation/Xenko.Assets.Presentation.xkpkg @@ -53,6 +53,8 @@ TemplateFolders: - !file Templates/Assets/Physics/ColliderShapePlane.xktpl - !file Templates/Assets/Physics/ColliderShapeSphere.xktpl - !file Templates/Assets/Physics/ColliderShapeCone.xktpl + - !file Templates/Assets/Physics/ColliderShapeHeightfield.xktpl + - !file Templates/Assets/Physics/Heightmap.xktpl - !file Templates/Assets/Scenes/DefaultPrefab.xktpl - !file Templates/Assets/Scenes/DefaultNavigationMesh.xktpl - !file Templates/Assets/Scenes/DefaultScene.xktpl diff --git a/sources/engine/Xenko.Assets/AllAssets.Display.cs b/sources/engine/Xenko.Assets/AllAssets.Display.cs index 817fc60638..e6ee3737d6 100644 --- a/sources/engine/Xenko.Assets/AllAssets.Display.cs +++ b/sources/engine/Xenko.Assets/AllAssets.Display.cs @@ -1,4 +1,4 @@ -// Copyright (c) Xenko contributors (https://xenko.com) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Copyright (c) Xenko contributors (https://xenko.com) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using Xenko.Core; @@ -102,6 +102,11 @@ namespace Physics partial class ColliderShapeAsset { } + + [Display((int)AssetDisplayPriority.Physics + 50, "Heightmap")] + partial class HeightmapAsset + { + } } namespace Rendering diff --git a/sources/engine/Xenko.Assets/Navigation/NavigationMeshAssetCompiler.cs b/sources/engine/Xenko.Assets/Navigation/NavigationMeshAssetCompiler.cs index 1df8b8813f..ce617c0712 100644 --- a/sources/engine/Xenko.Assets/Navigation/NavigationMeshAssetCompiler.cs +++ b/sources/engine/Xenko.Assets/Navigation/NavigationMeshAssetCompiler.cs @@ -31,6 +31,7 @@ public override IEnumerable GetInputTypes(AssetItem assetIt { yield return new BuildDependencyInfo(typeof(SceneAsset), typeof(AssetCompilationContext), BuildDependencyType.CompileAsset); yield return new BuildDependencyInfo(typeof(ColliderShapeAsset), typeof(AssetCompilationContext), BuildDependencyType.CompileContent); + yield return new BuildDependencyInfo(typeof(HeightmapAsset), typeof(AssetCompilationContext), BuildDependencyType.CompileContent); } public override IEnumerable GetInputFiles(AssetItem assetItem) @@ -63,6 +64,21 @@ public override IEnumerable GetInputFiles(AssetItem assetItem) yield return new ObjectUrl(UrlType.Content, assetReference.Url); } } + else if (desc is HeightfieldColliderShapeDesc) + { + var heightfieldDesc = desc as HeightfieldColliderShapeDesc; + var heightmapSource = heightfieldDesc?.HeightStickArraySource as HeightStickArraySourceFromHeightmap; + + if (heightmapSource?.Heightmap != null) + { + var url = AttachedReferenceManager.GetUrl(heightmapSource.Heightmap); + + if (!string.IsNullOrEmpty(url)) + { + yield return new ObjectUrl(UrlType.Content, url); + } + } + } } } } @@ -82,6 +98,7 @@ private class NavmeshBuildCommand : AssetCommand { private readonly ContentManager contentManager = new ContentManager(MicrothreadLocalDatabases.ProviderService); private readonly Dictionary loadedColliderShapes = new Dictionary(); + private readonly Dictionary loadedHeightfieldInitialDatas = new Dictionary(); private NavigationMesh oldNavigationMesh; @@ -155,6 +172,10 @@ protected override Task DoCommandOverride(ICommandContext commandC { contentManager.Unload(pair.Key); } + foreach (var pair in loadedHeightfieldInitialDatas) + { + contentManager.Unload(pair.Key); + } if (!result.Success) return Task.FromResult(ResultStatus.Failed); @@ -279,7 +300,7 @@ private void EnsureClonedSceneAndHash() { staticColliderDatas.Add(new StaticColliderData { - Component = colliderComponent + Component = colliderComponent, }); if (colliderComponent.Enabled && !colliderComponent.IsTrigger && ((int)asset.IncludedCollisionGroups & (int)colliderComponent.CollisionGroup) != 0) @@ -299,6 +320,23 @@ private void EnsureClonedSceneAndHash() } shapeAssetDesc.Shape = loadedColliderShape; } + else if (desc is HeightfieldColliderShapeDesc) + { + var heightfieldDesc = desc as HeightfieldColliderShapeDesc; + var heightmapSource = heightfieldDesc?.HeightStickArraySource as HeightStickArraySourceFromHeightmap; + + if (heightmapSource?.Heightmap != null) + { + var assetReference = AttachedReferenceManager.GetAttachedReference(heightmapSource.Heightmap); + object loadedHeightfieldInitialData; + if (!loadedHeightfieldInitialDatas.TryGetValue(assetReference.Url, out loadedHeightfieldInitialData)) + { + loadedHeightfieldInitialData = contentManager.Load(typeof(Heightmap), assetReference.Url); + loadedHeightfieldInitialDatas.Add(assetReference.Url, loadedHeightfieldInitialData); + } + heightmapSource.Heightmap = loadedHeightfieldInitialData as Heightmap; + } + } } } diff --git a/sources/engine/Xenko.Assets/Physics/ByteHeightmapHeightConversionParameters.cs b/sources/engine/Xenko.Assets/Physics/ByteHeightmapHeightConversionParameters.cs new file mode 100644 index 0000000000..5dfcad26fd --- /dev/null +++ b/sources/engine/Xenko.Assets/Physics/ByteHeightmapHeightConversionParameters.cs @@ -0,0 +1,31 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Core; +using Xenko.Core.Annotations; +using Xenko.Core.Mathematics; +using Xenko.Physics; + +namespace Xenko.Assets.Physics +{ + [DataContract] + [Display("Byte")] + public class ByteHeightmapHeightConversionParameters : IHeightmapHeightConversionParameters + { + [DataMemberIgnore] + public HeightfieldTypes HeightType => HeightfieldTypes.Byte; + + [DataMember(10)] + public Vector2 HeightRange { get; set; } = new Vector2(0, 10); + + [DataMemberIgnore] + public float HeightScale => HeightScaleCalculator.Calculate(this); + + /// + /// Select how to calculate HeightScale. + /// + [DataMember(20)] + [NotNull] + [Display("HeightScale", Expand = ExpandRule.Always)] + public IHeightScaleCalculator HeightScaleCalculator { get; set; } = new HeightScaleCalculator(); + } +} diff --git a/sources/engine/Xenko.Assets/Physics/ColliderShapeAssetCompiler.cs b/sources/engine/Xenko.Assets/Physics/ColliderShapeAssetCompiler.cs index 7caea4f0df..6bc8a83d22 100644 --- a/sources/engine/Xenko.Assets/Physics/ColliderShapeAssetCompiler.cs +++ b/sources/engine/Xenko.Assets/Physics/ColliderShapeAssetCompiler.cs @@ -40,6 +40,10 @@ public override IEnumerable GetInputTypes(AssetItem assetIt { yield return new BuildDependencyInfo(type, typeof(AssetCompilationContext), BuildDependencyType.CompileContent); } + foreach (var type in AssetRegistry.GetAssetTypes(typeof(Heightmap))) + { + yield return new BuildDependencyInfo(type, typeof(AssetCompilationContext), BuildDependencyType.CompileContent); + } } public override IEnumerable GetInputTypesToExclude(AssetItem assetItem) @@ -64,6 +68,21 @@ public override IEnumerable GetInputFiles(AssetItem assetItem) { var url = AttachedReferenceManager.GetUrl(convexHullDesc.Model); + if (!string.IsNullOrEmpty(url)) + { + yield return new ObjectUrl(UrlType.Content, url); + } + } + } + else if (desc is HeightfieldColliderShapeDesc) + { + var heightfieldDesc = desc as HeightfieldColliderShapeDesc; + var heightmapSource = heightfieldDesc?.HeightStickArraySource as HeightStickArraySourceFromHeightmap; + + if (heightmapSource?.Heightmap != null) + { + var url = AttachedReferenceManager.GetUrl(heightmapSource.Heightmap); + if (!string.IsNullOrEmpty(url)) { yield return new ObjectUrl(UrlType.Content, url); diff --git a/sources/engine/Xenko.Assets/Physics/ColliderShapeFactories.cs b/sources/engine/Xenko.Assets/Physics/ColliderShapeFactories.cs index ec16ceba86..06bda887a8 100644 --- a/sources/engine/Xenko.Assets/Physics/ColliderShapeFactories.cs +++ b/sources/engine/Xenko.Assets/Physics/ColliderShapeFactories.cs @@ -95,4 +95,17 @@ public override ColliderShapeAsset New() return Create(); } } + + public class ColliderShapeHeightfieldFactory : AssetFactory + { + public static ColliderShapeAsset Create() + { + return new ColliderShapeAsset { ColliderShapes = { new HeightfieldColliderShapeDesc() } }; + } + + public override ColliderShapeAsset New() + { + return Create(); + } + } } diff --git a/sources/engine/Xenko.Assets/Physics/FloatHeightmapHeightConversionParamters.cs b/sources/engine/Xenko.Assets/Physics/FloatHeightmapHeightConversionParamters.cs new file mode 100644 index 0000000000..73cff7cb8a --- /dev/null +++ b/sources/engine/Xenko.Assets/Physics/FloatHeightmapHeightConversionParamters.cs @@ -0,0 +1,22 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Core; +using Xenko.Core.Mathematics; +using Xenko.Physics; + +namespace Xenko.Assets.Physics +{ + [DataContract] + [Display("Float")] + public class FloatHeightmapHeightConversionParamters : IHeightmapHeightConversionParameters + { + [DataMemberIgnore] + public HeightfieldTypes HeightType => HeightfieldTypes.Float; + + [DataMember(10)] + public Vector2 HeightRange { get; set; } = new Vector2(-10, 10); + + [DataMemberIgnore] + public float HeightScale => 1f; + } +} diff --git a/sources/engine/Xenko.Assets/Physics/HeightmapAsset.cs b/sources/engine/Xenko.Assets/Physics/HeightmapAsset.cs new file mode 100644 index 0000000000..686a13ea50 --- /dev/null +++ b/sources/engine/Xenko.Assets/Physics/HeightmapAsset.cs @@ -0,0 +1,87 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Core; +using Xenko.Core.Annotations; +using Xenko.Core.Assets; +using Xenko.Core.Mathematics; +using Xenko.Physics; + +namespace Xenko.Assets.Physics +{ + [DataContract("HeightmapAsset")] + [CategoryOrder(10, "Image File", Expand = ExpandRule.Always)] + [CategoryOrder(20, "Convert", Expand = ExpandRule.Always)] + [AssetDescription(FileExtension)] + [AssetContentType(typeof(Heightmap))] + [AssetFormatVersion(XenkoConfig.PackageName, CurrentVersion, "3.0.0.0")] + public partial class HeightmapAsset : AssetWithSource + { + private const string CurrentVersion = "3.0.0.0"; + + public const string FileExtension = ".xkhmap"; + + /// + /// Parameters to convert pixels to heights. + /// + [DataMember(10)] + [Display("Height", category: "Convert", Expand = ExpandRule.Always)] + [NotNull] + public IHeightmapHeightConversionParameters HeightConversionParameters { get; set; } = new FloatHeightmapHeightConversionParamters(); + + /// + /// The size of the heightmap. + /// + /// + /// X is width and Y is length. + /// They should be greater than or equal to 2. + /// The heightmap size will be same to the image size, if disabled. + /// + [DataMember(20, "Resize")] + [Display(category: "Convert")] + public HeightmapResizingParameters Resizing { get; set; } = new HeightmapResizingParameters + { + Enabled = false, + Size = new Int2(1024, 1024), + }; + + /// + /// Enable if needed to load the image file as sRGB. + /// + [DataMember(50, "sRGB sampling")] + [Display(category: "Image File")] + public bool IsSRgb { get; set; } = false; + + /// + /// If enabled, scale each of the heights to the height range before they are stored as heightmap. + /// + /// + /// By the default, they are considered to be in [-1 .. 1] when the pixel format of the image file is floating-point component format. + /// Match FloatingPointComponentRange to actual range. + /// + [DataMember(30)] + [Display(category: "Convert")] + public bool ScaleToHeightRange { get; set; } = true; + + /// + /// The range of the floating-point component. + /// + /// + /// Determine the range of the floating-point components. + /// This property affects nothing when not floating-point component. + /// + [DataMember(40)] + [Display(category: "Image File")] + public Vector2 FloatingPointComponentRange { get; set; } = new Vector2(-1, 1); + + /// + /// The range of the signed short component. + /// + /// + /// Enable if the R components are signed short integer in [-32767 .. 32767]. + /// This property affects nothing when not signed short integer component. + /// + [DataMember(41)] + [Display(category: "Image File")] + public bool IsSymmetricShortComponent { get; set; } = false; + } +} diff --git a/sources/engine/Xenko.Assets/Physics/HeightmapAssetCompiler.cs b/sources/engine/Xenko.Assets/Physics/HeightmapAssetCompiler.cs new file mode 100644 index 0000000000..36438edac8 --- /dev/null +++ b/sources/engine/Xenko.Assets/Physics/HeightmapAssetCompiler.cs @@ -0,0 +1,346 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xenko.Assets.Textures; +using Xenko.Core.Assets; +using Xenko.Core.Assets.Analysis; +using Xenko.Core.Assets.Compiler; +using Xenko.Core.BuildEngine; +using Xenko.Core.Mathematics; +using Xenko.Core.Serialization.Contents; +using Xenko.Graphics; +using Xenko.Physics; +using Xenko.TextureConverter; + +namespace Xenko.Assets.Physics +{ + [AssetCompiler(typeof(HeightmapAsset), typeof(AssetCompilationContext))] + internal class HeightmapAssetCompiler : AssetCompilerBase + { + public override IEnumerable GetInputTypes(AssetItem assetItem) + { + yield return new BuildDependencyInfo(typeof(TextureAsset), typeof(AssetCompilationContext), BuildDependencyType.CompileContent); + } + + public override IEnumerable GetInputFiles(AssetItem assetItem) + { + var asset = (HeightmapAsset)assetItem.Asset; + var url = asset.Source.FullPath; + if (!string.IsNullOrEmpty(url)) + { + yield return new ObjectUrl(UrlType.File, url); + } + } + + protected override void Prepare(AssetCompilerContext context, AssetItem assetItem, string targetUrlInStorage, AssetCompilerResult result) + { + var asset = (HeightmapAsset)assetItem.Asset; + + result.BuildSteps = new AssetBuildStep(assetItem); + result.BuildSteps.Add(new HeightmapConvertCommand(targetUrlInStorage, asset, assetItem.Package) { InputFilesGetter = () => GetInputFiles(assetItem) }); + } + + public class HeightmapConvertCommand : AssetCommand + { + public HeightmapConvertCommand(string url, HeightmapAsset parameters, IAssetFinder assetFinder) + : base(url, parameters, assetFinder) + { + } + + protected override Task DoCommandOverride(ICommandContext commandContext) + { + var assetManager = new ContentManager(MicrothreadLocalDatabases.ProviderService); + + Heightmap heightmap = null; + + var heightType = Parameters.HeightConversionParameters.HeightType; + var heightScale = Parameters.HeightConversionParameters.HeightScale; + var heightRange = Parameters.HeightConversionParameters.HeightRange; + + // Heights + + var source = Parameters.Source; + + using (var textureTool = new TextureTool()) + using (var texImage = textureTool.Load(source, Parameters.IsSRgb)) + { + // Resize if needed. + + var size = Parameters.Resizing.Enabled ? + Parameters.Resizing.Size : + new Int2(texImage.Width, texImage.Height); + + HeightmapUtils.CheckHeightParameters(size, heightType, heightRange, heightScale, true); + + // Convert the pixel format to single component one. + + var isConvertedR16 = false; + + switch (texImage.Format) + { + case PixelFormat.R32_Float: + case PixelFormat.R16_SNorm: + case PixelFormat.R8_UNorm: + break; + + case PixelFormat.R32G32B32A32_Float: + case PixelFormat.R16G16B16A16_Float: + case PixelFormat.R16_Float: + textureTool.Convert(texImage, PixelFormat.R32_Float); + break; + + case PixelFormat.R16_UNorm: + case PixelFormat.R16G16B16A16_UNorm: + case PixelFormat.R16G16_UNorm: + textureTool.Convert(texImage, PixelFormat.R16_SNorm); + isConvertedR16 = true; + break; + + case PixelFormat.R16G16B16A16_SNorm: + case PixelFormat.R16G16_SNorm: + textureTool.Convert(texImage, PixelFormat.R16_SNorm); + break; + + case PixelFormat.R8_SNorm: + case PixelFormat.B8G8R8A8_UNorm: + case PixelFormat.B8G8R8X8_UNorm: + case PixelFormat.R8G8B8A8_UNorm: + case PixelFormat.R8G8_UNorm: + textureTool.Convert(texImage, PixelFormat.R8_UNorm); + break; + + case PixelFormat.R8G8B8A8_SNorm: + case PixelFormat.R8G8_SNorm: + textureTool.Convert(texImage, PixelFormat.R8_UNorm); + break; + + case PixelFormat.B8G8R8A8_UNorm_SRgb: + case PixelFormat.B8G8R8X8_UNorm_SRgb: + case PixelFormat.R8G8B8A8_UNorm_SRgb: + textureTool.Convert(texImage, PixelFormat.R8_UNorm); + break; + + default: + throw new Exception($"{ texImage.Format } format is not supported."); + } + + // Convert pixels to heights + + using (var image = textureTool.ConvertToXenkoImage(texImage)) + { + var pixelBuffer = image.PixelBuffer[0]; + var pixelBufferSize = new Int2(pixelBuffer.Width, pixelBuffer.Height); + + var minFloat = Parameters.FloatingPointComponentRange.X; + var maxFloat = Parameters.FloatingPointComponentRange.Y; + var isSNorm = (Math.Abs(-1 - minFloat) < float.Epsilon) && (Math.Abs(1 - maxFloat) < float.Epsilon); + + if (maxFloat < minFloat) + { + throw new Exception($"{ nameof(Parameters.FloatingPointComponentRange) }.{ nameof(Parameters.FloatingPointComponentRange.Y) } should be greater than { nameof(Parameters.FloatingPointComponentRange.X) }."); + } + + var useScaleToRange = Parameters.ScaleToHeightRange; + + switch (heightType) + { + case HeightfieldTypes.Float: + { + float[] floats = null; + + switch (image.Description.Format) + { + case PixelFormat.R32_Float: + floats = HeightmapUtils.Resize(pixelBuffer.GetPixels(), pixelBufferSize, size); + floats = isSNorm ? + floats : + HeightmapUtils.ConvertToFloatHeights(floats, minFloat, maxFloat); + break; + + case PixelFormat.R16_SNorm: + var shorts = HeightmapUtils.Resize(pixelBuffer.GetPixels(), pixelBufferSize, size); + floats = !isConvertedR16 && Parameters.IsSymmetricShortComponent ? + HeightmapUtils.ConvertToFloatHeights(shorts, -short.MaxValue, short.MaxValue) : + HeightmapUtils.ConvertToFloatHeights(shorts); + break; + + case PixelFormat.R8_UNorm: + var bytes = HeightmapUtils.Resize(pixelBuffer.GetPixels(), pixelBufferSize, size); + floats = HeightmapUtils.ConvertToFloatHeights(bytes); + break; + } + + if (useScaleToRange) + { + ScaleToHeightRange(floats, -1, 1, heightRange, heightScale, commandContext); + } + + heightmap = Heightmap.Create(size, heightType, heightRange, heightScale, floats); + } + break; + + case HeightfieldTypes.Short: + { + short[] shorts = null; + + switch (image.Description.Format) + { + case PixelFormat.R32_Float: + var floats = HeightmapUtils.Resize(pixelBuffer.GetPixels(), pixelBufferSize, size); + shorts = HeightmapUtils.ConvertToShortHeights(floats, minFloat, maxFloat); + break; + + case PixelFormat.R16_SNorm: + shorts = HeightmapUtils.Resize(pixelBuffer.GetPixels(), pixelBufferSize, size); + shorts = !isConvertedR16 && Parameters.IsSymmetricShortComponent ? + shorts : + HeightmapUtils.ConvertToShortHeights(shorts); + break; + + case PixelFormat.R8_UNorm: + var bytes = HeightmapUtils.Resize(pixelBuffer.GetPixels(), pixelBufferSize, size); + shorts = HeightmapUtils.ConvertToShortHeights(bytes); + break; + } + + if (useScaleToRange) + { + ScaleToHeightRange(shorts, short.MinValue, short.MaxValue, heightRange, heightScale, commandContext); + } + + heightmap = Heightmap.Create(size, heightType, heightRange, heightScale, shorts); + } + break; + + case HeightfieldTypes.Byte: + { + byte[] bytes = null; + + switch (image.Description.Format) + { + case PixelFormat.R32_Float: + var floats = HeightmapUtils.Resize(pixelBuffer.GetPixels(), pixelBufferSize, size); + bytes = HeightmapUtils.ConvertToByteHeights(floats, minFloat, maxFloat); + break; + + case PixelFormat.R16_SNorm: + var shorts = HeightmapUtils.Resize(pixelBuffer.GetPixels(), pixelBufferSize, size); + bytes = !isConvertedR16 && Parameters.IsSymmetricShortComponent ? + HeightmapUtils.ConvertToByteHeights(shorts, -short.MaxValue, short.MaxValue) : + HeightmapUtils.ConvertToByteHeights(shorts); + break; + + case PixelFormat.R8_UNorm: + bytes = HeightmapUtils.Resize(pixelBuffer.GetPixels(), pixelBufferSize, size); + break; + } + + if (useScaleToRange) + { + ScaleToHeightRange(bytes, byte.MinValue, byte.MaxValue, heightRange, heightScale, commandContext); + } + + heightmap = Heightmap.Create(size, heightType, heightRange, heightScale, bytes); + } + break; + + default: + throw new Exception($"{ heightType } height type is not supported."); + } + + commandContext.Logger.Info($"[{Url}] Convert Image(Format={ texImage.Format }, Width={ texImage.Width }, Height={ texImage.Height }) " + + $"to Heightmap(HeightType={ heightType }, MinHeight={ heightRange.X }, MaxHeight={ heightRange.Y }, HeightScale={ heightScale }, Size={ size })."); + } + } + + if (heightmap == null) + { + throw new Exception($"Failed to compile { Url }."); + } + + assetManager.Save(Url, heightmap); + + return Task.FromResult(ResultStatus.Successful); + } + + private void CalculateByteOrShortRange(Vector2 heightRage, float heightScale, out float min, out float max) + { + var minHeight = heightRage.X; + var maxHeight = heightRage.Y; + + min = (float)Math.Round((minHeight / heightScale), MidpointRounding.AwayFromZero); + max = (float)Math.Round((maxHeight / heightScale), MidpointRounding.AwayFromZero); + + if (heightScale < 0) + { + min = (min * heightScale) < minHeight ? min - 1 : min; + max = (max * heightScale) > maxHeight ? max + 1 : max; + Core.Utilities.Swap(ref min, ref max); + } + else + { + min = (min * heightScale) < minHeight ? min + 1 : min; + max = (max * heightScale) > maxHeight ? max - 1 : max; + } + } + + private void ScaleToHeightRange(T[] heights, float minT, float maxT, Vector2 heightRange, float heightScale, ICommandContext commandContext) where T : struct + { + float min; + float max; + + var typeOfT = typeof(T); + + if (typeOfT == typeof(float)) + { + min = heightRange.X; + max = heightRange.Y; + } + else if (typeOfT == typeof(short) || typeOfT == typeof(byte)) + { + CalculateByteOrShortRange(heightRange, heightScale, out min, out max); + + if (!MathUtil.IsInRange(min, minT, maxT) || + !MathUtil.IsInRange(max, minT, maxT)) + { + throw new Exception( + $"{ nameof(ScaleToHeightRange) } failed to scale { minT }..{ maxT } to { min }..{ max }. Check HeightScale and HeightRange are proper."); + } + } + else + { + throw new NotSupportedException($"{ typeof(T[]) } type is not supported."); + } + + commandContext?.Logger.Info($"[{Url}] ScaleToHeightRange : { minT }..{ maxT } -> { min }..{ max }"); + + if (typeOfT == typeof(float)) + { + float[] floats = heights as float[]; + for (var i = 0; i < floats.Length; ++i) + { + floats[i] = MathUtil.Clamp(MathUtil.Lerp(min, max, MathUtil.InverseLerp(minT, maxT, floats[i])), min, max); + } + } + else if (typeOfT == typeof(short)) + { + short[] shorts = heights as short[]; + for (var i = 0; i < shorts.Length; ++i) + { + shorts[i] = (short)MathUtil.Clamp(MathUtil.Lerp(min, max, MathUtil.InverseLerp(minT, maxT, shorts[i])), min, max); + } + } + else if (typeOfT == typeof(byte)) + { + byte[] bytes = heights as byte[]; + for (var i = 0; i < bytes.Length; ++i) + { + bytes[i] = (byte)MathUtil.Clamp(MathUtil.Lerp(min, max, MathUtil.InverseLerp(minT, maxT, bytes[i])), min, max); + } + } + } + } + } +} diff --git a/sources/engine/Xenko.Assets/Physics/HeightmapFactory.cs b/sources/engine/Xenko.Assets/Physics/HeightmapFactory.cs new file mode 100644 index 0000000000..0e42a1efbb --- /dev/null +++ b/sources/engine/Xenko.Assets/Physics/HeightmapFactory.cs @@ -0,0 +1,19 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Core.Assets; + +namespace Xenko.Assets.Physics +{ + public class HeightmapFactory : AssetFactory + { + public static HeightmapAsset Create() + { + return new HeightmapAsset(); + } + + public override HeightmapAsset New() + { + return Create(); + } + } +} diff --git a/sources/engine/Xenko.Assets/Physics/HeightmapResizingParameters.cs b/sources/engine/Xenko.Assets/Physics/HeightmapResizingParameters.cs new file mode 100644 index 0000000000..ef7252612c --- /dev/null +++ b/sources/engine/Xenko.Assets/Physics/HeightmapResizingParameters.cs @@ -0,0 +1,22 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Core; +using Xenko.Core.Annotations; +using Xenko.Core.Mathematics; + +namespace Xenko.Assets.Physics +{ + [DataContract] + public struct HeightmapResizingParameters + { + [DataMember(0)] + public bool Enabled { get; set; } + + /// + /// New size of the heightmap. + /// + [DataMember(10)] + [InlineProperty] + public Int2 Size { get; set; } + } +} diff --git a/sources/engine/Xenko.Assets/Physics/IHeightmapHeightConversionParameters.cs b/sources/engine/Xenko.Assets/Physics/IHeightmapHeightConversionParameters.cs new file mode 100644 index 0000000000..22bbbf703f --- /dev/null +++ b/sources/engine/Xenko.Assets/Physics/IHeightmapHeightConversionParameters.cs @@ -0,0 +1,10 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Physics; + +namespace Xenko.Assets.Physics +{ + public interface IHeightmapHeightConversionParameters : IHeightStickParameters + { + } +} diff --git a/sources/engine/Xenko.Assets/Physics/ShortHeightmapHeightConversionParameters.cs b/sources/engine/Xenko.Assets/Physics/ShortHeightmapHeightConversionParameters.cs new file mode 100644 index 0000000000..262cb65cc3 --- /dev/null +++ b/sources/engine/Xenko.Assets/Physics/ShortHeightmapHeightConversionParameters.cs @@ -0,0 +1,31 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Core; +using Xenko.Core.Annotations; +using Xenko.Core.Mathematics; +using Xenko.Physics; + +namespace Xenko.Assets.Physics +{ + [DataContract] + [Display("Short")] + public class ShortHeightmapHeightConversionParameters : IHeightmapHeightConversionParameters + { + [DataMemberIgnore] + public HeightfieldTypes HeightType => HeightfieldTypes.Short; + + [DataMember(10)] + public Vector2 HeightRange { get; set; } = new Vector2(-10, 10); + + [DataMemberIgnore] + public float HeightScale => HeightScaleCalculator.Calculate(this); + + /// + /// Select how to calculate HeightScale. + /// + [DataMember(20)] + [NotNull] + [Display("HeightScale", Expand = ExpandRule.Always)] + public IHeightScaleCalculator HeightScaleCalculator { get; set; } = new HeightScaleCalculator(); + } +} diff --git a/sources/engine/Xenko.Assets/Textures/TextureImporter.cs b/sources/engine/Xenko.Assets/Textures/TextureImporter.cs index 55becb22e5..82be3eb16d 100644 --- a/sources/engine/Xenko.Assets/Textures/TextureImporter.cs +++ b/sources/engine/Xenko.Assets/Textures/TextureImporter.cs @@ -7,6 +7,7 @@ using Xenko.Core.Assets; using Xenko.Core.IO; using Xenko.Assets.Sprite; +using Xenko.Assets.Physics; namespace Xenko.Assets.Textures { @@ -28,6 +29,7 @@ public override IEnumerable RootAssetTypes { yield return typeof(TextureAsset); yield return typeof(SpriteSheetAsset); // TODO: this is temporary, until we can make the asset templates ask compilers instead of importer which type they support + yield return typeof(HeightmapAsset); } } diff --git a/sources/engine/Xenko.Navigation/NavigationMeshBuildUtils.cs b/sources/engine/Xenko.Navigation/NavigationMeshBuildUtils.cs index 7539416871..b0f4847703 100644 --- a/sources/engine/Xenko.Navigation/NavigationMeshBuildUtils.cs +++ b/sources/engine/Xenko.Navigation/NavigationMeshBuildUtils.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Xenko.Core.Mathematics; +using Xenko.Engine; using Xenko.Physics; namespace Xenko.Navigation @@ -163,5 +164,48 @@ public static int HashEntityCollider(StaticColliderComponent collider, Collision } return hash; } + + /// + /// Checks if a static collider has latest collider shape + /// + /// The collider to check + /// true if the collider has latest collider shape, false otherwise + public static bool HasLatestColliderShape(StaticColliderComponent collider) + { + if (collider.ColliderShape == null) + { + return false; + } + else + { + if (collider.ColliderShapes.Count == 1) + { + if (!collider.ColliderShapes[0].Match(collider.ColliderShape.Description)) + { + return false; + } + } + else + { + var compound = collider.ColliderShape as CompoundColliderShape; + if ((compound != null) && (compound.Count == collider.ColliderShapes.Count)) + { + for (int i = 0; i < compound.Count; ++i) + { + if (!collider.ColliderShapes[i].Match(compound[i].Description)) + { + return false; + } + } + } + else + { + return false; + } + } + } + + return true; + } } } diff --git a/sources/engine/Xenko.Navigation/NavigationMeshBuilder.cs b/sources/engine/Xenko.Navigation/NavigationMeshBuilder.cs index e8bc242d46..b3168e90d7 100644 --- a/sources/engine/Xenko.Navigation/NavigationMeshBuilder.cs +++ b/sources/engine/Xenko.Navigation/NavigationMeshBuilder.cs @@ -438,7 +438,8 @@ private void BuildInput(StaticColliderData[] collidersLocal, CollisionFilterGrou colliderData.Previous = null; if (lastCache?.Objects.TryGetValue(colliderData.Component.Id, out colliderData.Previous) ?? false) { - if (colliderData.Previous.ParameterHash == colliderData.ParameterHash) + if ((!colliderData.Component.AlwaysUpdateNaviMeshCache) && + (colliderData.Previous.ParameterHash == colliderData.ParameterHash)) { // In this case, we don't need to recalculate the geometry for this shape, since it wasn't changed // here we take the triangle mesh from the previous build as the current @@ -466,7 +467,10 @@ private void BuildInput(StaticColliderData[] collidersLocal, CollisionFilterGrou } // Make sure shape is up to date - colliderData.Component.ComposeShape(); + if (!NavigationMeshBuildUtils.HasLatestColliderShape(colliderData.Component)) + { + colliderData.Component.ComposeShape(); + } // Interate through all the colliders shapes while queueing all shapes in compound shapes to process those as well Queue shapesToProcess = new Queue(); @@ -571,6 +575,50 @@ private void BuildInput(StaticColliderData[] collidersLocal, CollisionFilterGrou entityNavigationMeshInputBuilder.AppendArrays(mesh.Vertices.ToArray(), indices, transform); } + else if (shapeType == typeof(HeightfieldColliderShape)) + { + var heightfield = (HeightfieldColliderShape)shape; + + var halfRange = (heightfield.MaxHeight - heightfield.MinHeight) * 0.5f; + var offset = -(heightfield.MinHeight + halfRange); + Matrix transform = Matrix.Translation(new Vector3(0, offset, 0)) * heightfield.PositiveCenterMatrix * entityWorldMatrix; + + var width = heightfield.HeightStickWidth - 1; + var length = heightfield.HeightStickLength - 1; + var mesh = GeometricPrimitive.Plane.New(width, length, width, length, normalDirection: NormalDirection.UpY, toLeftHanded: true); + + var arrayLength = heightfield.HeightStickWidth * heightfield.HeightStickLength; + + using (heightfield.LockToReadHeights()) + { + switch (heightfield.HeightType) + { + case HeightfieldTypes.Short: + if (heightfield.ShortArray == null) continue; + for (int i = 0; i < arrayLength; ++i) + { + mesh.Vertices[i].Position.Y = heightfield.ShortArray[i] * heightfield.HeightScale; + } + break; + case HeightfieldTypes.Byte: + if (heightfield.ByteArray == null) continue; + for (int i = 0; i < arrayLength; ++i) + { + mesh.Vertices[i].Position.Y = heightfield.ByteArray[i] * heightfield.HeightScale; + } + break; + case HeightfieldTypes.Float: + if (heightfield.FloatArray == null) continue; + for (int i = 0; i < arrayLength; ++i) + { + mesh.Vertices[i].Position.Y = heightfield.FloatArray[i]; + } + break; + } + } + + entityNavigationMeshInputBuilder.AppendMeshData(mesh, transform); + } else if (shapeType == typeof(CompoundColliderShape)) { // Unroll compound collider shapes diff --git a/sources/engine/Xenko.Navigation/Processors/StaticColliderProcessor.cs b/sources/engine/Xenko.Navigation/Processors/StaticColliderProcessor.cs index 2d83a37347..487325a1ea 100644 --- a/sources/engine/Xenko.Navigation/Processors/StaticColliderProcessor.cs +++ b/sources/engine/Xenko.Navigation/Processors/StaticColliderProcessor.cs @@ -16,7 +16,7 @@ internal class StaticColliderProcessor : EntityProcessor protected override StaticColliderData GenerateComponentData(Entity entity, StaticColliderComponent component) { - return new StaticColliderData { Component = component }; + return new StaticColliderData { Component = component, }; } /// diff --git a/sources/engine/Xenko.Physics/ByteHeightStickArraySource.cs b/sources/engine/Xenko.Physics/ByteHeightStickArraySource.cs new file mode 100644 index 0000000000..180377553f --- /dev/null +++ b/sources/engine/Xenko.Physics/ByteHeightStickArraySource.cs @@ -0,0 +1,73 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using Xenko.Core; +using Xenko.Core.Annotations; +using Xenko.Core.Mathematics; + +namespace Xenko.Physics +{ + [DataContract] + [Display("Byte")] + public class ByteHeightStickArraySource : IHeightStickArraySource + { + [DataMemberIgnore] + public HeightfieldTypes HeightType => HeightfieldTypes.Byte; + + [DataMember(10)] + [Display("Size")] + public Int2 HeightStickSize { get; set; } = new Int2(65, 65); + + [DataMember(20)] + public Vector2 HeightRange { get; set; } = new Vector2(0, 10); + + [DataMemberIgnore] + public float HeightScale => HeightScaleCalculator.Calculate(this); + + /// + /// Select how to calculate HeightScale. + /// + [DataMember(30)] + [NotNull] + [Display("HeightScale", Expand = ExpandRule.Always)] + public IHeightScaleCalculator HeightScaleCalculator { get; set; } = new HeightScaleCalculator(); + + /// + /// The value to fill the height stick array. + /// + [DataMember(40)] + [DataMemberRange(0, 255, 1, 10, 0)] + public byte InitialByte { get; set; } = 0; + + public bool IsValid() => HeightmapUtils.CheckHeightParameters(HeightStickSize, HeightType, HeightRange, HeightScale, false) && + MathUtil.IsInRange(InitialByte, byte.MinValue, byte.MaxValue); + + public void CopyTo(UnmanagedArray heightStickArray, int index) where T : struct + { + if (heightStickArray == null) throw new ArgumentNullException(nameof(heightStickArray)); + if (heightStickArray is UnmanagedArray unmanagedArray) + { + unmanagedArray.Fill(InitialByte, index, HeightStickSize.X * HeightStickSize.Y); + } + else + { + throw new NotSupportedException($"{ typeof(UnmanagedArray) } type is not supported."); + } + } + + public bool Match(object obj) + { + var other = obj as ByteHeightStickArraySource; + + if (other == null) + { + return false; + } + + return other.HeightStickSize == HeightStickSize && + other.HeightRange == HeightRange && + Math.Abs(other.HeightScale - HeightScale) < float.Epsilon && + other.InitialByte == InitialByte; + } + } +} diff --git a/sources/engine/Xenko.Physics/CustomHeightScaleCalculator.cs b/sources/engine/Xenko.Physics/CustomHeightScaleCalculator.cs new file mode 100644 index 0000000000..3738ef0016 --- /dev/null +++ b/sources/engine/Xenko.Physics/CustomHeightScaleCalculator.cs @@ -0,0 +1,20 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Core; +using Xenko.Core.Mathematics; + +namespace Xenko.Physics +{ + [DataContract] + [Display("Custom")] + public class CustomHeightScaleCalculator : IHeightScaleCalculator + { + [DataMember(10)] + public float Numerator { get; set; } = 1; + + [DataMember(20)] + public float Denominator { get; set; } = 255; + + public float Calculate(IHeightStickParameters heightDescription) => MathUtil.IsZero(Denominator) ? 0 : (Numerator / Denominator); + } +} diff --git a/sources/engine/Xenko.Physics/Data/HeightfieldColliderShapeDesc.cs b/sources/engine/Xenko.Physics/Data/HeightfieldColliderShapeDesc.cs new file mode 100644 index 0000000000..39118827b6 --- /dev/null +++ b/sources/engine/Xenko.Physics/Data/HeightfieldColliderShapeDesc.cs @@ -0,0 +1,157 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using Xenko.Core; +using Xenko.Core.Annotations; +using Xenko.Core.Mathematics; +using Xenko.Core.Serialization.Contents; + +namespace Xenko.Physics +{ + [ContentSerializer(typeof(DataContentSerializer))] + [DataContract("HeightfieldColliderShapeDesc")] + [Display(300, "Heightfield")] + public class HeightfieldColliderShapeDesc : IInlineColliderShapeDesc + { + /// + /// The source to initialize the height stick array. + /// + [DataMember(10)] + [NotNull] + [Display("Source", Expand = ExpandRule.Always)] + public IHeightStickArraySource HeightStickArraySource { get; set; } = new HeightStickArraySourceFromHeightmap(); + + [DataMember(70)] + public bool FlipQuadEdges = false; + + /// + /// Add a value to local offset in order to center specific height. + /// + /// + /// NOTE: The center height is the middle point of the range, if this is disabled with asymmetrical range. + /// + [DataMember(80)] + public HeightfieldCenteringParameters Centering { get; set; } = new HeightfieldCenteringParameters + { + Enabled = true, + CenterHeight = 0, + }; + + [DataMember(100)] + public Vector3 LocalOffset; + + [DataMember(110)] + public Quaternion LocalRotation = Quaternion.Identity; + + public bool Match(object obj) + { + var other = obj as HeightfieldColliderShapeDesc; + + if (other == null) + { + return false; + } + + if (LocalOffset != other.LocalOffset || LocalRotation != other.LocalRotation) + { + return false; + } + + var sourceMatch = other.HeightStickArraySource?.Match(HeightStickArraySource) ?? HeightStickArraySource == null; + + var centeringMatch = other.Centering.Match(Centering); + + return sourceMatch && + centeringMatch && + other.FlipQuadEdges == FlipQuadEdges; + } + + /// + /// Get the offset required in order to center specific height. + /// + /// The range of the height. + /// The height to be centered. + /// The value in y axis required in order to center specific height. + public static float GetCenteringOffset(Vector2 heightRange, float centerHeight) + { + return (heightRange.X + heightRange.Y) * 0.5f - centerHeight; + } + + private static UnmanagedArray CreateHeights(IHeightStickArraySource heightStickArraySource) where T : struct + { + if (!heightStickArraySource?.IsValid() ?? false) + { + return null; + } + + var arrayLength = heightStickArraySource.HeightStickSize.X * heightStickArraySource.HeightStickSize.Y; + + var unmanagedArray = new UnmanagedArray(arrayLength); + + heightStickArraySource.CopyTo(unmanagedArray, 0); + + return unmanagedArray; + } + + /// + /// Get the centering offset that will be added to the local offset of the collider shape. + /// + /// The value that will be added to the local offset of the collider shape in order to center specific height. + public float GetCenteringOffset() + { + if (HeightStickArraySource == null) throw new InvalidOperationException($"{ nameof(HeightStickArraySource) } is a null."); + + return Centering.Enabled ? + GetCenteringOffset(HeightStickArraySource.HeightRange, Centering.CenterHeight) : + 0f; + } + + public ColliderShape CreateShape() + { + object unmanagedArray; + + switch (HeightStickArraySource.HeightType) + { + case HeightfieldTypes.Float: + { + unmanagedArray = CreateHeights(HeightStickArraySource); + break; + } + case HeightfieldTypes.Short: + { + unmanagedArray = CreateHeights(HeightStickArraySource); + break; + } + case HeightfieldTypes.Byte: + { + unmanagedArray = CreateHeights(HeightStickArraySource); + break; + } + + default: + return null; + } + + if (unmanagedArray == null) return null; + + var shape = new HeightfieldColliderShape + ( + HeightStickArraySource.HeightStickSize.X, + HeightStickArraySource.HeightStickSize.Y, + HeightStickArraySource.HeightType, + unmanagedArray, + HeightStickArraySource.HeightScale, + HeightStickArraySource.HeightRange.X, + HeightStickArraySource.HeightRange.Y, + FlipQuadEdges + ) + { + LocalOffset = LocalOffset + new Vector3(0, GetCenteringOffset(), 0), + LocalRotation = LocalRotation, + }; + + return shape; + } + } +} diff --git a/sources/engine/Xenko.Physics/Elements/StaticColliderComponent.cs b/sources/engine/Xenko.Physics/Elements/StaticColliderComponent.cs index e86d2a3aee..54c13aec4d 100644 --- a/sources/engine/Xenko.Physics/Elements/StaticColliderComponent.cs +++ b/sources/engine/Xenko.Physics/Elements/StaticColliderComponent.cs @@ -9,6 +9,9 @@ namespace Xenko.Physics [Display("Static collider")] public sealed class StaticColliderComponent : PhysicsTriggerComponentBase { + [DataMember(100)] + public bool AlwaysUpdateNaviMeshCache { get; set; } = false; + protected override void OnAttach() { NativeCollisionObject = new BulletSharp.CollisionObject diff --git a/sources/engine/Xenko.Physics/Engine/Heightmap.cs b/sources/engine/Xenko.Physics/Engine/Heightmap.cs new file mode 100644 index 0000000000..992980649c --- /dev/null +++ b/sources/engine/Xenko.Physics/Engine/Heightmap.cs @@ -0,0 +1,104 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using Xenko.Core; +using Xenko.Core.Mathematics; +using Xenko.Core.Serialization; +using Xenko.Core.Serialization.Contents; +using Xenko.Engine.Design; + +namespace Xenko.Physics +{ + [DataContract] + [ContentSerializer(typeof(DataContentSerializer))] + [DataSerializerGlobal(typeof(CloneSerializer), Profile = "Clone")] + [ReferenceSerializer, DataSerializerGlobal(typeof(ReferenceSerializer), Profile = "Content")] + public class Heightmap + { + /// + /// Float height array. + /// + [DataMember(10)] + [Display(Browsable = false)] + public float[] Floats; + + /// + /// Short height array. + /// + [DataMember(20)] + [Display(Browsable = false)] + public short[] Shorts; + + /// + /// Byte height array. + /// + [DataMember(30)] + [Display(Browsable = false)] + public byte[] Bytes; + + /// + /// The type of the height. + /// + [DataMember(40)] + [Display(Browsable = false)] + public HeightfieldTypes HeightType; + + /// + /// The size of the heightmap. + /// + /// + /// X is width and Y is length. + /// They should be greater than or equal to 2. + /// For example, this size should be 65 * 65 when you want 64 * 64 size in a scene. + /// + [DataMember(50)] + public Int2 Size; + + /// + /// The range of the height. + /// + /// + /// X is min height and Y is max height. + /// (height * HeightScale) should be in this range. + /// Positive and negative heights can not be handle at the same time when the height type is Byte. + /// + [DataMember(60)] + public Vector2 HeightRange; + + /// + /// Used to calculate the height when the height type is Short or Byte. HeightScale should be 1 when the height type is Float. + /// + [DataMember(70)] + public float HeightScale; + + public static Heightmap Create(Int2 size, HeightfieldTypes heightType, Vector2 heightRange, float heightScale, T[] data) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + + HeightmapUtils.CheckHeightParameters(size, heightType, heightRange, heightScale, true); + + var length = size.X * size.Y; + + switch (data) + { + case float[] floats when floats.Length == length: break; + case short[] shorts when shorts.Length == length: break; + case byte[] bytes when bytes.Length == length: break; + default: throw new ArgumentException($"{ typeof(T[]) } is not supported in { heightType } height type. Or { nameof(data) }.{ nameof(data).Length } doesn't match { nameof(size) }."); + } + + var heightmap = new Heightmap + { + HeightType = heightType, + Size = size, + HeightRange = heightRange, + HeightScale = heightScale, + Floats = data as float[], + Shorts = data as short[], + Bytes = data as byte[], + }; + + return heightmap; + } + } +} diff --git a/sources/engine/Xenko.Physics/Engine/HeightmapExtensions.cs b/sources/engine/Xenko.Physics/Engine/HeightmapExtensions.cs new file mode 100644 index 0000000000..14083805aa --- /dev/null +++ b/sources/engine/Xenko.Physics/Engine/HeightmapExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using System.Linq; +using Xenko.Core.Annotations; +using Xenko.Core.Mathematics; +using Xenko.Graphics; + +namespace Xenko.Physics +{ + public static class HeightmapExtensions + { + public static bool IsValid([NotNull] this Heightmap heightmap) + { + if (heightmap == null) throw new ArgumentNullException(nameof(heightmap)); + + bool IsValidHeights() + { + var length = heightmap.Size.X * heightmap.Size.Y; + + switch (heightmap.HeightType) + { + case HeightfieldTypes.Float when heightmap.Floats != null && heightmap.Floats.Length == length: + return true; + + case HeightfieldTypes.Short when heightmap.Shorts != null && heightmap.Shorts.Length == length: + return true; + + case HeightfieldTypes.Byte when heightmap.Bytes != null && heightmap.Bytes.Length == length: + return true; + } + + return false; + } + + return HeightmapUtils.CheckHeightParameters(heightmap.Size, heightmap.HeightType, heightmap.HeightRange, heightmap.HeightScale, false) && + IsValidHeights(); + } + + public static Texture CreateTexture([NotNull] this Heightmap heightmap, GraphicsDevice device) + { + if (heightmap == null) throw new ArgumentNullException(nameof(heightmap)); + + if (device == null || !heightmap.IsValid()) + { + return null; + } + + var min = heightmap.HeightRange.X / heightmap.HeightScale; + var max = heightmap.HeightRange.Y / heightmap.HeightScale; + + switch (heightmap.HeightType) + { + case HeightfieldTypes.Float: + return Texture.New2D(device, heightmap.Size.X, heightmap.Size.Y, PixelFormat.R8_UNorm, HeightmapUtils.ConvertToByteHeights(heightmap.Floats, min, max)); + + case HeightfieldTypes.Short: + return Texture.New2D(device, heightmap.Size.X, heightmap.Size.Y, PixelFormat.R8_UNorm, + heightmap.Shorts.Select((h) => (byte)MathUtil.Clamp(MathUtil.Lerp(byte.MinValue, byte.MaxValue, MathUtil.InverseLerp(min, max, h)), byte.MinValue, byte.MaxValue)).ToArray()); + + case HeightfieldTypes.Byte: + return Texture.New2D(device, heightmap.Size.X, heightmap.Size.Y, PixelFormat.R8_UNorm, + heightmap.Bytes.Select((h) => (byte)MathUtil.Clamp(MathUtil.Lerp(byte.MinValue, byte.MaxValue, MathUtil.InverseLerp(min, max, h)), byte.MinValue, byte.MaxValue)).ToArray()); + + default: + return null; + } + } + } +} diff --git a/sources/engine/Xenko.Physics/Engine/HeightmapUtils.cs b/sources/engine/Xenko.Physics/Engine/HeightmapUtils.cs new file mode 100644 index 0000000000..fed928db36 --- /dev/null +++ b/sources/engine/Xenko.Physics/Engine/HeightmapUtils.cs @@ -0,0 +1,105 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using System.Linq; +using Xenko.Core.Mathematics; + +namespace Xenko.Physics +{ + public class HeightmapUtils + { + public static bool CheckHeightParameters(Int2 size, HeightfieldTypes heightType, Vector2 heightRange, float heightScale, bool throwExceptionWhenInvalid) + { + try + { + // Size + + if (size.X < HeightfieldColliderShape.MinimumHeightStickWidth || size.Y < HeightfieldColliderShape.MinimumHeightStickLength) + { + throw new ArgumentException($"{ nameof(size) } parameters should be greater than or equal to 2."); + } + + // HeightScale + + if (MathUtil.IsZero(heightScale)) + { + throw new ArgumentException($"{ nameof(heightScale) } is 0."); + } + + if ((heightType == HeightfieldTypes.Float) && !MathUtil.IsOne(heightScale)) + { + throw new ArgumentException($"{ nameof(heightScale) } should be 1 in { heightType } type."); + } + + // HeightRange + + if (heightRange.Y < heightRange.X) + { + throw new ArgumentException($"{ nameof(heightRange) }.{ nameof(heightRange.Y) } should be greater than { nameof(heightRange.X) }."); + } + + if (Math.Abs((heightRange.Y / heightScale) - (heightRange.X / heightScale)) < 1) + { + throw new ArgumentException($"{ nameof(heightRange) } is too short."); + } + + if ((heightType == HeightfieldTypes.Byte) && + (Math.Sign(heightRange.X) + Math.Sign(heightRange.Y)) == 0) + { + throw new ArgumentException($"Can't handle { nameof(heightRange) } included both of positive and negative in { heightType } type."); + } + } + catch (ArgumentException) + { + if (throwExceptionWhenInvalid) throw; + + return false; + } + + return true; + } + + public static float ConvertToFloatHeight(float minValue, float maxValue, float value) => MathUtil.InverseLerp(minValue, maxValue, MathUtil.Clamp(value, minValue, maxValue)) * 2 - 1; + public static short ConvertToShortHeight(float minValue, float maxValue, float value) => (short)Math.Round(ConvertToFloatHeight(minValue, maxValue, value) * short.MaxValue, MidpointRounding.AwayFromZero); + public static byte ConvertToByteHeight(float minValue, float maxValue, float value) => (byte)Math.Round(MathUtil.InverseLerp(minValue, maxValue, MathUtil.Clamp(value, minValue, maxValue)) * byte.MaxValue, MidpointRounding.AwayFromZero); + + public static float[] ConvertToFloatHeights(float[] values, float minValue, float maxValue) => values.Select((v) => ConvertToFloatHeight(minValue, maxValue, v)).ToArray(); + public static float[] ConvertToFloatHeights(short[] values, short minValue = short.MinValue, short maxValue = short.MaxValue) => values.Select((v) => ConvertToFloatHeight(minValue, maxValue, v)).ToArray(); + public static float[] ConvertToFloatHeights(byte[] values, byte minValue = byte.MinValue, byte maxValue = byte.MaxValue) => values.Select((v) => ConvertToFloatHeight(minValue, maxValue, v)).ToArray(); + + public static short[] ConvertToShortHeights(float[] values, float minValue, float maxValue) => values.Select((v) => ConvertToShortHeight(minValue, maxValue, v)).ToArray(); + public static short[] ConvertToShortHeights(short[] values, short minValue = short.MinValue, short maxValue = short.MaxValue) => values.Select((v) => ConvertToShortHeight(minValue, maxValue, v)).ToArray(); + public static short[] ConvertToShortHeights(byte[] values, byte minValue = byte.MinValue, byte maxValue = byte.MaxValue) => values.Select((v) => ConvertToShortHeight(minValue, maxValue, v)).ToArray(); + + public static byte[] ConvertToByteHeights(float[] values, float minValue, float maxValue) => values.Select((v) => ConvertToByteHeight(minValue, maxValue, v)).ToArray(); + public static byte[] ConvertToByteHeights(short[] values, short minValue = short.MinValue, short maxValue = short.MaxValue) => values.Select((v) => ConvertToByteHeight(minValue, maxValue, v)).ToArray(); + + public static T[] Resize(T[] pixels, Int2 originalSize, Int2 newSize) + { + if (originalSize.Equals(newSize)) + { + return pixels.ToArray(); + } + + var originalWidth = originalSize.X; + var originalLength = originalSize.Y; + var newWidth = newSize.X; + var newLength = newSize.Y; + + int GetOriginalIndex(int x, int y) => + (int)Math.Round(MathUtil.Lerp(0, originalLength, MathUtil.InverseLerp(0, newLength, y)), MidpointRounding.AwayFromZero) * originalWidth + + (int)Math.Round(MathUtil.Lerp(0, originalWidth, MathUtil.InverseLerp(0, newWidth, x)), MidpointRounding.AwayFromZero); + + T[] newPixels = new T[newWidth * newLength]; + for (int y = 0; y < newLength; ++y) + { + for (int x = 0; x < newWidth; ++x) + { + newPixels[y * newWidth + x] = pixels[GetOriginalIndex(x, y)]; + } + } + + return newPixels; + } + } +} diff --git a/sources/engine/Xenko.Physics/Engine/PhysicsShapesRenderingService.cs b/sources/engine/Xenko.Physics/Engine/PhysicsShapesRenderingService.cs index 8592cbfa61..7084f8d3d7 100644 --- a/sources/engine/Xenko.Physics/Engine/PhysicsShapesRenderingService.cs +++ b/sources/engine/Xenko.Physics/Engine/PhysicsShapesRenderingService.cs @@ -8,7 +8,6 @@ using Xenko.Engine; using Xenko.Games; using Xenko.Graphics; -using Xenko.Physics.Shapes; using Xenko.Rendering; namespace Xenko.Physics.Engine @@ -65,7 +64,7 @@ public override void Update(GameTime gameTime) var unusedShapes = new List(); foreach (var keyValuePair in updatableDebugMeshes) { - if (keyValuePair.Value != null && keyValuePair.Key.DebugEntity?.Scene != null) + if (keyValuePair.Value != null && keyValuePair.Key.DebugEntity?.Scene != null && keyValuePair.Key.InternalShape != null) { keyValuePair.Key.UpdateDebugPrimitive(Game.GraphicsContext.CommandList, keyValuePair.Value); } @@ -161,7 +160,7 @@ private Entity CreateChildEntity(PhysicsComponent component, ColliderShape shape { IDebugPrimitive debugPrimitive; var type = shape.GetType(); - if (type == typeof(HeightfieldColliderShape)) + if (type == typeof(HeightfieldColliderShape) || type.BaseType == typeof(HeightfieldColliderShape)) { if (!updatableDebugMeshCache.TryGetValue(shape, out debugPrimitive)) { diff --git a/sources/engine/Xenko.Physics/FloatHeightStickArraySource.cs b/sources/engine/Xenko.Physics/FloatHeightStickArraySource.cs new file mode 100644 index 0000000000..f58c4387d6 --- /dev/null +++ b/sources/engine/Xenko.Physics/FloatHeightStickArraySource.cs @@ -0,0 +1,62 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using Xenko.Core; +using Xenko.Core.Mathematics; + +namespace Xenko.Physics +{ + [DataContract] + [Display("Float")] + public class FloatHeightStickArraySource : IHeightStickArraySource + { + [DataMemberIgnore] + public HeightfieldTypes HeightType => HeightfieldTypes.Float; + + [DataMember(10)] + [Display("Size")] + public Int2 HeightStickSize { get; set; } = new Int2(65, 65); + + [DataMember(20)] + public Vector2 HeightRange { get; set; } = new Vector2(-10, 10); + + [DataMemberIgnore] + public float HeightScale => 1f; + + /// + /// The value to fill the height stick array. + /// + [DataMember(30)] + public float InitialHeight { get; set; } = 0; + + public bool IsValid() => HeightmapUtils.CheckHeightParameters(HeightStickSize, HeightType, HeightRange, HeightScale, false) && + MathUtil.IsInRange(InitialHeight, HeightRange.X, HeightRange.Y); + + public void CopyTo(UnmanagedArray heightStickArray, int index) where T : struct + { + if (heightStickArray == null) throw new ArgumentNullException(nameof(heightStickArray)); + if (heightStickArray is UnmanagedArray unmanagedArray) + { + unmanagedArray.Fill(InitialHeight, index, HeightStickSize.X * HeightStickSize.Y); + } + else + { + throw new NotSupportedException($"{ typeof(UnmanagedArray) } type is not supported."); + } + } + + public bool Match(object obj) + { + var other = obj as FloatHeightStickArraySource; + + if (other == null) + { + return false; + } + + return other.HeightStickSize == HeightStickSize && + other.HeightRange == HeightRange && + Math.Abs(other.InitialHeight - InitialHeight) < float.Epsilon; + } + } +} diff --git a/sources/engine/Xenko.Physics/HeightScaleCalculator.cs b/sources/engine/Xenko.Physics/HeightScaleCalculator.cs new file mode 100644 index 0000000000..fef33dd3c0 --- /dev/null +++ b/sources/engine/Xenko.Physics/HeightScaleCalculator.cs @@ -0,0 +1,39 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using Xenko.Core; + +namespace Xenko.Physics +{ + [DataContract] + [Display("Auto")] + public class HeightScaleCalculator : IHeightScaleCalculator + { + public float Calculate(IHeightStickParameters heightDescription) + { + var heightRange = heightDescription.HeightRange; + + switch (heightDescription.HeightType) + { + case HeightfieldTypes.Float: + return 1f; + + case HeightfieldTypes.Short: + return Math.Max(Math.Abs(heightRange.X), Math.Abs(heightRange.Y)) / short.MaxValue; + + case HeightfieldTypes.Byte: + if (Math.Abs(heightRange.X) <= Math.Abs(heightRange.Y)) + { + return heightRange.Y / byte.MaxValue; + } + else + { + return heightRange.X / byte.MaxValue; + } + + default: + throw new NotSupportedException($"Unknown height type."); + } + } + } +} diff --git a/sources/engine/Xenko.Physics/HeightStickArraySourceFromHeightmap.cs b/sources/engine/Xenko.Physics/HeightStickArraySourceFromHeightmap.cs new file mode 100644 index 0000000000..f8d659612b --- /dev/null +++ b/sources/engine/Xenko.Physics/HeightStickArraySourceFromHeightmap.cs @@ -0,0 +1,82 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using Xenko.Core; +using Xenko.Core.Mathematics; + +namespace Xenko.Physics +{ + [DataContract] + [Display("Heightmap")] + public class HeightStickArraySourceFromHeightmap : IHeightStickArraySource + { + /// + /// The heightmap to initialize the height stick array. + /// + [DataMember(10)] + public Heightmap Heightmap { get; set; } + + [DataMemberIgnore] + public HeightfieldTypes HeightType => Heightmap?.HeightType ?? default; + + [DataMemberIgnore] + public Int2 HeightStickSize => Heightmap?.Size ?? default; + + [DataMemberIgnore] + public Vector2 HeightRange => Heightmap?.HeightRange ?? default; + + [DataMemberIgnore] + public float HeightScale => Heightmap?.HeightScale ?? default; + + public bool IsValid() => Heightmap?.IsValid() ?? false; + + public void CopyTo(UnmanagedArray heightStickArray, int index) where T : struct + { + if (Heightmap == null) throw new InvalidOperationException($"{ nameof(Heightmap) } is a null"); + if (heightStickArray == null) throw new ArgumentNullException(nameof(heightStickArray)); + + var heightStickArrayLength = heightStickArray.Length - index; + if (heightStickArrayLength <= 0) throw new IndexOutOfRangeException(nameof(index)); + + var typeOfT = typeof(T); + T[] heights; + + if (typeOfT == typeof(float)) + { + if (Heightmap.Floats == null) throw new InvalidOperationException($"{ nameof(Heightmap.Floats) } is a null."); + heights = (T[])(object)Heightmap.Floats; + } + else if (typeOfT == typeof(short)) + { + if (Heightmap.Shorts == null) throw new InvalidOperationException($"{ nameof(Heightmap.Shorts) } is a null."); + heights = (T[])(object)Heightmap.Shorts; + } + else if (typeOfT == typeof(byte)) + { + if (Heightmap.Bytes == null) throw new InvalidOperationException($"{ nameof(Heightmap.Bytes) } is a null."); + heights = (T[])(object)Heightmap.Bytes; + } + else + { + throw new NotSupportedException($"{ typeof(UnmanagedArray) } type is not supported."); + } + + var heightsLength = heights.Length; + if (heightStickArrayLength < heightsLength) throw new ArgumentException($"{ nameof(heightStickArray) }.{ nameof(heightStickArray.Length) } is not enough to copy."); + + heightStickArray.Write(heights, index * Utilities.SizeOf(), 0, heightsLength); + } + + public bool Match(object obj) + { + var other = obj as HeightStickArraySourceFromHeightmap; + + if (other == null) + { + return false; + } + + return other.Heightmap == Heightmap; + } + } +} diff --git a/sources/engine/Xenko.Physics/HeightfieldCenteringParameters.cs b/sources/engine/Xenko.Physics/HeightfieldCenteringParameters.cs new file mode 100644 index 0000000000..7722bc140f --- /dev/null +++ b/sources/engine/Xenko.Physics/HeightfieldCenteringParameters.cs @@ -0,0 +1,28 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using Xenko.Core; +using Xenko.Core.Annotations; + +namespace Xenko.Physics +{ + [DataContract] + public struct HeightfieldCenteringParameters + { + [DataMember(10)] + public bool Enabled { get; set; } + + /// + /// The height to be centered. + /// + [DataMember(20)] + [InlineProperty] + public float CenterHeight { get; set; } + + public bool Match(HeightfieldCenteringParameters other) + { + return other.Enabled == Enabled && + Math.Abs(other.CenterHeight - CenterHeight) < float.Epsilon; + } + } +} diff --git a/sources/engine/Xenko.Physics/IHeightScaleCalculator.cs b/sources/engine/Xenko.Physics/IHeightScaleCalculator.cs new file mode 100644 index 0000000000..3f80d95cb7 --- /dev/null +++ b/sources/engine/Xenko.Physics/IHeightScaleCalculator.cs @@ -0,0 +1,10 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Xenko.Physics +{ + public interface IHeightScaleCalculator + { + float Calculate(IHeightStickParameters heightDescription); + } +} diff --git a/sources/engine/Xenko.Physics/IHeightStickArraySource.cs b/sources/engine/Xenko.Physics/IHeightStickArraySource.cs new file mode 100644 index 0000000000..9a7a049032 --- /dev/null +++ b/sources/engine/Xenko.Physics/IHeightStickArraySource.cs @@ -0,0 +1,32 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Core; +using Xenko.Core.Mathematics; + +namespace Xenko.Physics +{ + public interface IHeightStickArraySource : IHeightStickParameters + { + /// + /// The size of the source. + /// + /// + /// X is width and Y is length. + /// They should be greater than or equal to 2. + /// For example, this size should be 65 * 65 when you want 64 * 64 size in a scene. + /// + Int2 HeightStickSize { get; } + + /// + /// Copy the source data to the height stick array. + /// + /// The data type of the height + /// The destination to copy the data. + /// The start index of the destination to copy the data. + void CopyTo(UnmanagedArray heightStickArray, int index) where T : struct; + + bool IsValid(); + + bool Match(object obj); + } +} diff --git a/sources/engine/Xenko.Physics/IHeightStickParameters.cs b/sources/engine/Xenko.Physics/IHeightStickParameters.cs new file mode 100644 index 0000000000..32a8d77500 --- /dev/null +++ b/sources/engine/Xenko.Physics/IHeightStickParameters.cs @@ -0,0 +1,29 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Xenko.Core.Mathematics; + +namespace Xenko.Physics +{ + public interface IHeightStickParameters + { + /// + /// The type of the height. + /// + HeightfieldTypes HeightType { get; } + + /// + /// The range of the height. + /// + /// + /// X is min height and Y is max height. + /// (height * HeightScale) should be in this range. + /// Positive and negative heights can not be handle at the same time when the height type is Byte. + /// + Vector2 HeightRange { get; } + + /// + /// Used to calculate the height when the height type is Short or Byte. HeightScale should be 1 when the height type is Float. + /// + float HeightScale { get; } + } +} diff --git a/sources/engine/Xenko.Physics/Shapes/HeightfieldColliderShape.cs b/sources/engine/Xenko.Physics/Shapes/HeightfieldColliderShape.cs index f01178dfda..0e599eb864 100644 --- a/sources/engine/Xenko.Physics/Shapes/HeightfieldColliderShape.cs +++ b/sources/engine/Xenko.Physics/Shapes/HeightfieldColliderShape.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Xenko.Core; using Xenko.Core.Mathematics; using Xenko.Core.Threading; @@ -11,10 +12,33 @@ using Xenko.Rendering; using Buffer = Xenko.Graphics.Buffer; -namespace Xenko.Physics.Shapes +namespace Xenko.Physics { + namespace Shapes + { + [Obsolete("This class will be deprecated. Use 'Xenko.Physics.HeightfieldColliderShape'.", false)] + public class HeightfieldColliderShape : Xenko.Physics.HeightfieldColliderShape + { + public HeightfieldColliderShape(int heightStickWidth, int heightStickLength, UnmanagedArray dynamicFieldData, float heightScale, float minHeight, float maxHeight, bool flipQuadEdges) + : base(heightStickWidth, heightStickLength, dynamicFieldData, heightScale, minHeight, maxHeight, flipQuadEdges) + { + } + public HeightfieldColliderShape(int heightStickWidth, int heightStickLength, UnmanagedArray dynamicFieldData, float heightScale, float minHeight, float maxHeight, bool flipQuadEdges) + : base(heightStickWidth, heightStickLength, dynamicFieldData, heightScale, minHeight, maxHeight, flipQuadEdges) + { + } + public HeightfieldColliderShape(int heightStickWidth, int heightStickLength, UnmanagedArray dynamicFieldData, float heightScale, float minHeight, float maxHeight, bool flipQuadEdges) + : base(heightStickWidth, heightStickLength, dynamicFieldData, heightScale, minHeight, maxHeight, flipQuadEdges) + { + } + } + } + public class HeightfieldColliderShape : ColliderShape { + public static readonly int MinimumHeightStickWidth = 2; + public static readonly int MinimumHeightStickLength = 2; + public HeightfieldColliderShape(int heightStickWidth, int heightStickLength, UnmanagedArray dynamicFieldData, float heightScale, float minHeight, float maxHeight, bool flipQuadEdges) : this(heightStickWidth, heightStickLength, HeightfieldTypes.Short, dynamicFieldData, heightScale, minHeight, maxHeight, flipQuadEdges) { @@ -132,16 +156,86 @@ public override void UpdateDebugPrimitive(CommandList commandList, IDebugPrimiti heightfieldDebugPrimitive.Update(commandList); } + private readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim(); + + ~HeightfieldColliderShape() + { + lockSlim.Dispose(); + } + public override void Dispose() { base.Dispose(); - ShortArray?.Dispose(); - ShortArray = null; - ByteArray?.Dispose(); - ByteArray = null; - FloatArray?.Dispose(); - FloatArray = null; + using (LockToReadAndWriteHeights()) + { + ShortArray?.Dispose(); + ShortArray = null; + ByteArray?.Dispose(); + ByteArray = null; + FloatArray?.Dispose(); + FloatArray = null; + } + } + + public HeightArrayLock LockToReadHeights() + { + return new HeightArrayLock(HeightArrayLock.LockTypes.Read, lockSlim); + } + + public HeightArrayLock LockToReadAndWriteHeights() + { + return new HeightArrayLock(HeightArrayLock.LockTypes.ReadWrite, lockSlim); + } + + public class HeightArrayLock : IDisposable + { + public enum LockTypes + { + Read = 0, + ReadWrite = 1, + } + + private readonly ReaderWriterLockSlim readerWriterLockSlim; + + internal HeightArrayLock(LockTypes lockType, ReaderWriterLockSlim lockSlim) + { + if (lockSlim == null) + { + throw new ArgumentNullException(nameof(lockSlim)); + } + + readerWriterLockSlim = lockSlim; + + switch (lockType) + { + case LockTypes.Read: + readerWriterLockSlim.EnterReadLock(); + break; + case LockTypes.ReadWrite: + readerWriterLockSlim.EnterWriteLock(); + break; + default: + throw new ArgumentException($"{ nameof(lockType) } is invalid type"); + } + } + + public void Unlock() + { + if (readerWriterLockSlim.IsReadLockHeld) + { + readerWriterLockSlim.ExitReadLock(); + } + else if (readerWriterLockSlim.IsWriteLockHeld) + { + readerWriterLockSlim.ExitWriteLock(); + } + } + + public void Dispose() + { + Unlock(); + } } public class HeightfieldDebugPrimitive : IDebugPrimitive diff --git a/sources/engine/Xenko.Physics/ShortHeightStickArraySource.cs b/sources/engine/Xenko.Physics/ShortHeightStickArraySource.cs new file mode 100644 index 0000000000..d9b92f2662 --- /dev/null +++ b/sources/engine/Xenko.Physics/ShortHeightStickArraySource.cs @@ -0,0 +1,73 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using Xenko.Core; +using Xenko.Core.Annotations; +using Xenko.Core.Mathematics; + +namespace Xenko.Physics +{ + [DataContract] + [Display("Short")] + public class ShortHeightStickArraySource : IHeightStickArraySource + { + [DataMemberIgnore] + public HeightfieldTypes HeightType => HeightfieldTypes.Short; + + [DataMember(10)] + [Display("Size")] + public Int2 HeightStickSize { get; set; } = new Int2(65, 65); + + [DataMember(20)] + public Vector2 HeightRange { get; set; } = new Vector2(-10, 10); + + [DataMemberIgnore] + public float HeightScale => HeightScaleCalculator.Calculate(this); + + /// + /// Select how to calculate HeightScale. + /// + [DataMember(30)] + [NotNull] + [Display("HeightScale", Expand = ExpandRule.Always)] + public IHeightScaleCalculator HeightScaleCalculator { get; set; } = new HeightScaleCalculator(); + + /// + /// The value to fill the height stick array. + /// + [DataMember(40)] + [DataMemberRange(-32767, 32767, 1, 10, 0)] + public short InitialShort { get; set; } = 0; + + public bool IsValid() => HeightmapUtils.CheckHeightParameters(HeightStickSize, HeightType, HeightRange, HeightScale, false) && + MathUtil.IsInRange(InitialShort, -short.MaxValue, short.MaxValue); + + public void CopyTo(UnmanagedArray heightStickArray, int index) where T : struct + { + if (heightStickArray == null) throw new ArgumentNullException(nameof(heightStickArray)); + if (heightStickArray is UnmanagedArray unmanagedArray) + { + unmanagedArray.Fill(InitialShort, index, HeightStickSize.X * HeightStickSize.Y); + } + else + { + throw new NotSupportedException($"{ typeof(UnmanagedArray) } type is not supported."); + } + } + + public bool Match(object obj) + { + var other = obj as ShortHeightStickArraySource; + + if (other == null) + { + return false; + } + + return other.HeightStickSize == HeightStickSize && + other.HeightRange == HeightRange && + Math.Abs(other.HeightScale - HeightScale) < float.Epsilon && + other.InitialShort == InitialShort; + } + } +} diff --git a/sources/engine/Xenko.Physics/UnmanagedArrayExtensions.cs b/sources/engine/Xenko.Physics/UnmanagedArrayExtensions.cs new file mode 100644 index 0000000000..5df6570a1d --- /dev/null +++ b/sources/engine/Xenko.Physics/UnmanagedArrayExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Xenko contributors (https://xenko.com) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; +using Xenko.Core; + +namespace Xenko.Physics +{ + public static class UnmanagedArrayExtensions + { + /// + /// Fill the array with specific value. + /// + /// The type param of UnmanagedArray + /// The destination to fill. + /// The value used to fill. + /// The start index of the destination to fill. + /// The filling length. + public static void Fill(this UnmanagedArray unmanagedArray, T value, int index, int fillLength) where T : struct + { + if (unmanagedArray == null) throw new ArgumentNullException(nameof(unmanagedArray)); + + var length = unmanagedArray.Length; + var endIndex = index + fillLength; + + if (length <= index) throw new IndexOutOfRangeException(nameof(index)); + if (length < endIndex) throw new ArgumentException($"{ nameof(unmanagedArray) }.{ nameof(unmanagedArray.Length) } is not enough to fill."); + + for (int i = index; i < endIndex; ++i) + { + unmanagedArray[i] = value; + } + } + } +}