// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.ComponentModel; using FlaxEditor.CustomEditors; using FlaxEditor.GUI.Dialogs; using FlaxEngine; using FlaxEngine.GUI; // ReSharper disable InconsistentNaming // ReSharper disable UnusedMember.Local // ReSharper disable UnusedMember.Global #pragma warning disable 649 #pragma warning disable 414 namespace FlaxEditor.Tools.Terrain { /// /// Terrain creator dialog. Allows user to specify initial terrain properties perform proper setup. /// /// sealed class CreateTerrainDialog : Dialog { private enum ChunkSizes { _31 = 31, _63 = 63, _127 = 127, _254 = 254, _255 = 255, _511 = 511, } private class Options { [EditorOrder(100), EditorDisplay("Layout", "Number Of Patches"), DefaultValue(typeof(Int2), "1,1"), Limit(0, 512), Tooltip("Amount of terrain patches in each direction (X and Z). Each terrain patch contains a grid of 16 chunks. Patches can be later added or removed from terrain using a terrain editor tool.")] public Int2 NumberOfPatches = new Int2(1, 1); [EditorOrder(110), EditorDisplay("Layout"), DefaultValue(ChunkSizes._127), Tooltip("The size of the chunk (amount of quads per edge for the highest LOD).")] public ChunkSizes ChunkSize = ChunkSizes._127; [EditorOrder(120), EditorDisplay("Layout", "LOD Count"), DefaultValue(6), Limit(1, FlaxEngine.Terrain.MaxLODs), Tooltip("The maximum Level Of Details count. The actual amount of LODs may be lower due to provided chunk size (each LOD has 4 times less quads).")] public int LODCount = 6; [EditorOrder(130), EditorDisplay("Layout"), DefaultValue(null), Tooltip("The default material used for terrain rendering (chunks can override this). It must have Domain set to terrain.")] public MaterialBase Material; [EditorOrder(200), EditorDisplay("Collision"), DefaultValue(null), AssetReference(typeof(PhysicalMaterial), true), Tooltip("Terrain default physical material used to define the collider physical properties.")] public JsonAsset PhysicalMaterial; [EditorOrder(210), EditorDisplay("Collision", "Collision LOD"), DefaultValue(-1), Limit(-1, 100, 0.1f), Tooltip("Terrain geometry LOD index used for collision.")] public int CollisionLOD = -1; [EditorOrder(300), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom heightmap texture to import. Used as a source for height field values (from channel Red).")] public Texture Heightmap; [EditorOrder(310), EditorDisplay("Import Data"), DefaultValue(5000.0f), Tooltip("Custom heightmap texture values scale. Applied to adjust the normalized heightmap values into the world units.")] public float HeightmapScale = 5000.0f; [EditorOrder(320), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers compositing.")] public Texture Splatmap1; [EditorOrder(330), EditorDisplay("Import Data"), DefaultValue(null), Tooltip("Custom terrain splat map used as a source of the terrain layers weights. Each channel from RGBA is used as an independent layer weight for terrain layers compositing.")] public Texture Splatmap2; [EditorOrder(400), EditorDisplay("Transform", "Position"), DefaultValue(typeof(Double3), "0,0,0"), Tooltip("Position of the terrain (importer offset it on the Y axis.)")] public Double3 Position = new Double3(0.0f, 0.0f, 0.0f); [EditorOrder(410), EditorDisplay("Transform", "Rotation"), DefaultValue(typeof(Quaternion), "0,0,0,1"), Tooltip("Orientation of the terrain")] public Quaternion Orientation = Quaternion.Identity; [EditorOrder(420), EditorDisplay("Transform", "Scale"), DefaultValue(typeof(Float3), "1,1,1"), Limit(float.MinValue, float.MaxValue, 0.01f), Tooltip("Scale of the terrain")] public Float3 Scale = Float3.One; } private readonly Options _options = new Options(); private bool _isDone; private bool _isWorking; private FlaxEngine.Terrain _terrain; private CustomEditorPresenter _editor; /// /// Initializes a new instance of the class. /// public CreateTerrainDialog() : base("Create terrain") { const float TotalWidth = 450; const float EditorHeight = 600; Width = TotalWidth; // Header and help description var headerLabel = new Label { Text = "New Terrain", AnchorPreset = AnchorPresets.HorizontalStretchTop, Offsets = new Margin(0, 0, 0, 40), Parent = this, Font = new FontReference(Style.Current.FontTitle) }; var infoLabel = new Label { Text = "Specify options for new terrain.\nIt will be added to the first opened scene.\nMany of the following settings can be adjusted later.\nYou can also create terrain at runtime from code.", HorizontalAlignment = TextAlignment.Near, Margin = new Margin(7), AnchorPreset = AnchorPresets.HorizontalStretchTop, Offsets = new Margin(10, -20, 45, 70), Parent = this }; // Buttons const float ButtonsWidth = 60; const float ButtonsHeight = 24; const float ButtonsMargin = 8; var importButton = new Button { Text = "Create", AnchorPreset = AnchorPresets.BottomRight, Offsets = new Margin(-ButtonsWidth - ButtonsMargin, ButtonsWidth, -ButtonsHeight - ButtonsMargin, ButtonsHeight), Parent = this }; importButton.Clicked += OnSubmit; var cancelButton = new Button { Text = "Cancel", AnchorPreset = AnchorPresets.BottomRight, Offsets = new Margin(-ButtonsWidth - ButtonsMargin - ButtonsWidth - ButtonsMargin, ButtonsWidth, -ButtonsHeight - ButtonsMargin, ButtonsHeight), Parent = this }; cancelButton.Clicked += OnCancel; // Settings editor var settingsEditor = new CustomEditorPresenter(null); settingsEditor.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop; settingsEditor.Panel.Offsets = new Margin(2, 2, infoLabel.Bottom + 2, EditorHeight); settingsEditor.Panel.Parent = this; _editor = settingsEditor; _dialogSize = new Float2(TotalWidth, settingsEditor.Panel.Bottom); settingsEditor.Select(_options); } private void OnCreate() { var scene = Level.GetScene(0); if (scene == null) throw new InvalidOperationException("No scene found to add terrain to it!"); // Create terrain object and setup some options var terrain = new FlaxEngine.Terrain(); terrain.Setup(_options.LODCount, (int)_options.ChunkSize); terrain.Transform = new Transform(_options.Position, _options.Orientation, _options.Scale); terrain.Material = _options.Material; terrain.PhysicalMaterial = _options.PhysicalMaterial; terrain.CollisionLOD = _options.CollisionLOD; if (_options.Heightmap) terrain.Position -= new Vector3(0, _options.HeightmapScale * 0.5f, 0); // Add to scene (even if generation fails user gets a terrain in the scene) terrain.Parent = scene; Editor.Instance.Scene.MarkSceneEdited(scene); // Show loading label var label = new Label { AnchorPreset = AnchorPresets.StretchAll, Offsets = Margin.Zero, Text = "Generating terrain...", BackgroundColor = Style.Current.ForegroundDisabled, Parent = this, }; // Lock UI _editor.Panel.Enabled = false; _isWorking = true; _isDone = false; // Start async work _terrain = terrain; var thread = new System.Threading.Thread(Generate) { Name = "Terrain Generator" }; thread.Start(); } private void Generate() { _isWorking = true; _isDone = false; // Call tool to generate the terrain patches from the input data if (TerrainTools.GenerateTerrain(_terrain, ref _options.NumberOfPatches, _options.Heightmap, _options.HeightmapScale, _options.Splatmap1, _options.Splatmap2)) { Editor.LogError("Failed to generate terrain. See log for more info."); } _isWorking = false; _isDone = true; } /// public override void OnSubmit() { if (_isWorking) return; OnCreate(); } /// public override void OnCancel() { if (_isWorking) return; base.OnCancel(); } /// public override void Update(float deltaTime) { if (_isDone) { Editor.Instance.SceneEditing.Select(_terrain); _terrain = null; _isDone = false; Close(DialogResult.OK); return; } base.Update(deltaTime); } /// protected override bool CanCloseWindow(ClosingReason reason) { if (_isWorking && reason == ClosingReason.User) return false; return base.CanCloseWindow(reason); } /// public override bool OnKeyDown(KeyboardKeys key) { if (_isWorking) return true; switch (key) { case KeyboardKeys.Escape: OnCancel(); return true; case KeyboardKeys.Return: OnSubmit(); return true; } return base.OnKeyDown(key); } } }