@nice2dev/game-engine 1.3.15 → 1.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/cjs/accessibility/Accessibility.js +152 -165
- package/dist/cjs/ai/AI.js +292 -294
- package/dist/cjs/ai/BehaviorTree.js +1029 -1113
- package/dist/cjs/ai/StateMachine.js +694 -756
- package/dist/cjs/animation/Animation.js +232 -241
- package/dist/cjs/audio/AudioAdvanced.js +221 -236
- package/dist/cjs/audio/AudioBridge.js +441 -435
- package/dist/cjs/audio/AudioManager.js +242 -259
- package/dist/cjs/build-cloud/BuildOrchestration.js +1059 -1098
- package/dist/cjs/build-cloud/GodotBuildCloud.js +641 -670
- package/dist/cjs/build-cloud/UnityBuildCloud.js +563 -610
- package/dist/cjs/build-cloud/UnrealBuildCloud.js +610 -635
- package/dist/cjs/core/EventBus.js +51 -56
- package/dist/cjs/core/GameClock.js +62 -66
- package/dist/cjs/core/GameConfig.js +72 -79
- package/dist/cjs/core/GameLoop.js +80 -89
- package/dist/cjs/core/ServiceLocator.js +61 -65
- package/dist/cjs/core/Validation.js +72 -82
- package/dist/cjs/core/math.js +108 -112
- package/dist/cjs/devtools/DevTools.js +542 -563
- package/dist/cjs/devtools/DeveloperExperience.js +521 -559
- package/dist/cjs/devtools/GameplayAnalytics.js +636 -646
- package/dist/cjs/dialogue/DialogueSystem.js +984 -986
- package/dist/cjs/docs/DocGenerator.js +339 -364
- package/dist/cjs/ecs/World.js +355 -371
- package/dist/cjs/editor/AchievementEditor.js +580 -636
- package/dist/cjs/editor/AdvancedEditor.js +184 -191
- package/dist/cjs/editor/AnimationRetargeting.js +696 -741
- package/dist/cjs/editor/AssetBrowser.js +1072 -1160
- package/dist/cjs/editor/AssetManager.js +187 -192
- package/dist/cjs/editor/ConsolePro.js +971 -1061
- package/dist/cjs/editor/CraftingEditor.js +680 -730
- package/dist/cjs/editor/DebugTools.js +187 -185
- package/dist/cjs/editor/DialogueSystem.js +844 -901
- package/dist/cjs/editor/LeaderboardSystem.js +429 -483
- package/dist/cjs/editor/NiceGameEditor.js +1903 -1201
- package/dist/cjs/editor/PrefabSystem.js +772 -838
- package/dist/cjs/editor/ProfilerPanel.js +649 -700
- package/dist/cjs/editor/QuestEditor.js +792 -839
- package/dist/cjs/editor/ReplaySystem.js +585 -634
- package/dist/cjs/editor/SceneEditor.js +176 -200
- package/dist/cjs/editor/ScreenshotMode.js +621 -688
- package/dist/cjs/editor/ShaderGraph.js +1556 -1582
- package/dist/cjs/editor/TimelineEditor.js +715 -778
- package/dist/cjs/editor/UndoRedoPro.js +830 -904
- package/dist/cjs/editor/VRARMode.js +582 -645
- package/dist/cjs/engine/NiceGameEngine.js +141 -164
- package/dist/cjs/engine/SaveSystem.js +441 -495
- package/dist/cjs/enterprise/Enterprise.js +254 -261
- package/dist/cjs/export/BevyExporter.js +927 -967
- package/dist/cjs/export/CocosExporter.js +644 -673
- package/dist/cjs/export/Construct3Exporter.js +783 -794
- package/dist/cjs/export/DefoldExporter.js +1028 -1069
- package/dist/cjs/export/GDevelopExporter.js +674 -708
- package/dist/cjs/export/GameMakerExporter.js +791 -805
- package/dist/cjs/export/GodotExporter.js +1003 -1045
- package/dist/cjs/export/MinecraftBedrockExporter.js +573 -578
- package/dist/cjs/export/MonoGameExporter.js +1265 -1293
- package/dist/cjs/export/Pico8Exporter.js +766 -812
- package/dist/cjs/export/PlatformExporter.js +188 -206
- package/dist/cjs/export/RPGMakerExporter.js +860 -879
- package/dist/cjs/export/RobloxExporter.js +883 -919
- package/dist/cjs/export/Solar2DExporter.js +1218 -1256
- package/dist/cjs/export/ThreeJSExporter.js +1006 -1073
- package/dist/cjs/export/Tic80Exporter.js +1210 -1267
- package/dist/cjs/export/UnityExporter.js +1116 -1131
- package/dist/cjs/export/UnrealExporter.js +889 -919
- package/dist/cjs/export/WebExporter.js +931 -970
- package/dist/cjs/export/index.js +42 -65
- package/dist/cjs/godot-integration/GodotAssetLibrary.js +659 -692
- package/dist/cjs/godot-integration/GodotImport.js +929 -1027
- package/dist/cjs/godot-integration/GodotLiveLink.js +915 -945
- package/dist/cjs/godot-integration/components/GodotProjectBrowser.js +911 -611
- package/dist/cjs/i18n/I18n.js +860 -900
- package/dist/cjs/i18n/useTranslation.js +84 -89
- package/dist/cjs/import/AsepriteImporter.js +658 -716
- package/dist/cjs/import/DragonBonesImporter.js +437 -461
- package/dist/cjs/import/GameMakerImporter.js +511 -521
- package/dist/cjs/import/GodotSceneImporter.js +788 -822
- package/dist/cjs/import/LDtkImporter.js +436 -454
- package/dist/cjs/import/Live2DImporter.js +497 -518
- package/dist/cjs/import/NdgFormat.js +424 -457
- package/dist/cjs/import/OgmoImporter.js +466 -493
- package/dist/cjs/import/RPGMakerImporter.js +451 -477
- package/dist/cjs/import/SceneImporter.js +378 -423
- package/dist/cjs/import/SpineImporter.js +511 -546
- package/dist/cjs/import/SpriterImporter.js +590 -613
- package/dist/cjs/import/TiledMapImporter.js +811 -846
- package/dist/cjs/import/UnitySceneImporter.js +687 -703
- package/dist/cjs/import/index.js +206 -251
- package/dist/cjs/index.js +275 -278
- package/dist/cjs/input/GamepadNavigation.js +185 -205
- package/dist/cjs/input/InputManager.js +397 -421
- package/dist/cjs/input/useGamepads.js +70 -81
- package/dist/cjs/integration/IconSprite.js +129 -256
- package/dist/cjs/inventory/InventorySystem.js +920 -995
- package/dist/cjs/kids/KidMode.js +997 -1022
- package/dist/cjs/kids/KidTools.js +570 -594
- package/dist/cjs/monetization/Monetization.js +170 -177
- package/dist/cjs/multiplayer/LocalMultiplayer.js +287 -287
- package/dist/cjs/multiplayer/MiniGameTypes.js +49 -59
- package/dist/cjs/native-engine/AnimationSystem.js +731 -806
- package/dist/cjs/native-engine/AssetPipeline.js +454 -515
- package/dist/cjs/native-engine/AudioEngine.js +763 -804
- package/dist/cjs/native-engine/InputSystem.js +1075 -1145
- package/dist/cjs/native-engine/NetworkingSystem.js +733 -793
- package/dist/cjs/native-engine/PhysicsEngine.js +583 -607
- package/dist/cjs/native-engine/RenderingEngine.js +302 -350
- package/dist/cjs/native-engine/SceneGraph.js +769 -808
- package/dist/cjs/network/MultiplayerTransport.js +97 -132
- package/dist/cjs/network/Networking.js +549 -564
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/AbortController.js +21 -29
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/AccessTokenHttpClient.js +40 -48
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/DefaultHttpClient.js +35 -43
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/DefaultReconnectPolicy.js +10 -15
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Errors.js +103 -135
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/FetchHttpClient.js +131 -158
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HandshakeProtocol.js +39 -53
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HeaderNames.js +5 -8
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HttpClient.js +40 -48
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HttpConnection.js +494 -572
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HubConnection.js +826 -953
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/HubConnectionBuilder.js +125 -146
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/IHubProtocol.js +13 -23
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ILogger.js +11 -25
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ITransport.js +13 -24
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/JsonHubProtocol.js +108 -121
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Loggers.js +10 -14
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/LongPollingTransport.js +150 -185
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/MessageBuffer.js +158 -196
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/ServerSentEventsTransport.js +90 -105
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Subject.js +30 -35
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/TextMessageFormat.js +16 -21
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/Utils.js +215 -248
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/WebSocketTransport.js +135 -157
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/XhrHttpClient.js +76 -85
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/index.js +24 -27
- package/dist/cjs/node_modules/@microsoft/signalr/dist/esm/pkg-version.js +3 -4
- package/dist/cjs/pathfinding/Pathfinding.js +245 -263
- package/dist/cjs/performance/AssetStreaming.js +561 -635
- package/dist/cjs/performance/BenchmarkSuite.js +283 -311
- package/dist/cjs/performance/DeltaCompression.js +467 -526
- package/dist/cjs/performance/DeterministicPhysics.js +450 -498
- package/dist/cjs/performance/ECSOptimization.js +644 -695
- package/dist/cjs/performance/GameSaveSystem.js +509 -556
- package/dist/cjs/performance/HotReload.js +447 -500
- package/dist/cjs/performance/InputRecording.js +511 -561
- package/dist/cjs/performance/InterestManagement.js +453 -500
- package/dist/cjs/performance/LocalizationRuntime.js +376 -423
- package/dist/cjs/performance/MemoryManagement.js +410 -470
- package/dist/cjs/performance/ModSupport.js +445 -496
- package/dist/cjs/performance/MultiThreadedPhysics.js +400 -443
- package/dist/cjs/performance/NetworkPrediction.js +424 -482
- package/dist/cjs/performance/ObjectPooling.js +569 -617
- package/dist/cjs/performance/Performance.js +174 -190
- package/dist/cjs/performance/PerformanceAdvanced.js +614 -637
- package/dist/cjs/performance/WebGPURenderer.js +656 -683
- package/dist/cjs/physics/PhysicsAdvanced.js +345 -379
- package/dist/cjs/physics/PhysicsEngine2D.js +391 -427
- package/dist/cjs/plugins/PluginSDK.js +432 -466
- package/dist/cjs/plugins/PluginTestKit.js +307 -330
- package/dist/cjs/procedural/Procedural.js +243 -254
- package/dist/cjs/procedural/WorldBuilding.js +297 -314
- package/dist/cjs/quest/QuestSystem.js +975 -932
- package/dist/cjs/release/MobileExport.js +157 -163
- package/dist/cjs/release/Release.js +225 -238
- package/dist/cjs/rendering/Camera2D.js +121 -124
- package/dist/cjs/rendering/Renderer2D.js +321 -335
- package/dist/cjs/rendering/WebGPURenderPipeline.js +632 -651
- package/dist/cjs/runtime3d/Runtime3D.js +359 -378
- package/dist/cjs/runtime3d/SceneEditor3D.js +252 -263
- package/dist/cjs/scene/SceneManager.js +177 -193
- package/dist/cjs/scripting/GraphToAST.js +574 -547
- package/dist/cjs/scripting/LanguageExporter.js +254 -279
- package/dist/cjs/scripting/NodeGraph.js +546 -570
- package/dist/cjs/scripting/ScriptAST.js +31 -39
- package/dist/cjs/scripting/VisualScripting2.js +1016 -1065
- package/dist/cjs/scripting/exporters/CSharpExporter.js +464 -496
- package/dist/cjs/scripting/exporters/GDScriptExporter.js +390 -442
- package/dist/cjs/scripting/exporters/LuaExporter.js +406 -447
- package/dist/cjs/scripting/exporters/PythonExporter.js +484 -549
- package/dist/cjs/scripting/exporters/RustExporter.js +459 -512
- package/dist/cjs/scripting/exporters/TypeScriptExporter.js +505 -556
- package/dist/cjs/social/Social.js +72 -80
- package/dist/cjs/systems/LightingSystem.js +844 -931
- package/dist/cjs/systems/ParticleSystem2.js +1447 -1451
- package/dist/cjs/systems/PhysicsDebugSystem.js +1070 -1141
- package/dist/cjs/systems/TerrainSystem.js +985 -1098
- package/dist/cjs/templates/ActionTemplates.js +296 -309
- package/dist/cjs/templates/FightingTemplate.js +417 -463
- package/dist/cjs/templates/MetroidvaniaTemplate.js +353 -405
- package/dist/cjs/templates/PartyTemplates.js +190 -202
- package/dist/cjs/templates/PuzzleTemplates.js +245 -261
- package/dist/cjs/templates/RPGTemplates.js +160 -178
- package/dist/cjs/templates/SportsTemplates.js +237 -250
- package/dist/cjs/templates/StrategyTemplates.js +143 -153
- package/dist/cjs/templates/SurvivalTemplate.js +336 -380
- package/dist/cjs/templates/WaveDefense.js +499 -526
- package/dist/cjs/tilemap/Tilemap.js +207 -214
- package/dist/cjs/tutorials/TutorialBranching.js +396 -417
- package/dist/cjs/tutorials/TutorialEngine.js +888 -931
- package/dist/cjs/tutorials/TutorialOverlay.js +602 -640
- package/dist/cjs/tutorials/content/Tutorials_T05_T08.js +787 -809
- package/dist/cjs/tutorials/content/Tutorials_T09_T11.js +736 -756
- package/dist/cjs/tutorials/content/Tutorials_T12_T15.js +754 -778
- package/dist/cjs/ui/GameUI.js +295 -312
- package/dist/cjs/unity-integration/UnityAssetStore.js +700 -741
- package/dist/cjs/unity-integration/UnityImport.js +1066 -1176
- package/dist/cjs/unity-integration/UnityLiveLink.js +934 -981
- package/dist/cjs/unity-integration/components/UnityAssetStoreBrowser.js +1233 -757
- package/dist/cjs/unity-integration/components/UnityProjectBrowser.js +1240 -768
- package/dist/cjs/unreal-integration/UnrealImport.js +391 -412
- package/dist/cjs/unreal-integration/UnrealLiveLink.js +1102 -1137
- package/dist/cjs/unreal-integration/UnrealMarketplace.js +436 -473
- package/dist/cjs/unreal-integration/components/UnrealProjectBrowser.js +1150 -767
- package/dist/cjs/xr/ARVR.js +167 -175
- package/dist/esm/accessibility/Accessibility.js +160 -164
- package/dist/esm/ai/AI.js +310 -293
- package/dist/esm/ai/BehaviorTree.js +1057 -1112
- package/dist/esm/ai/StateMachine.js +709 -755
- package/dist/esm/animation/Animation.js +237 -240
- package/dist/esm/audio/AudioAdvanced.js +239 -235
- package/dist/esm/audio/AudioBridge.js +448 -434
- package/dist/esm/audio/AudioManager.js +243 -258
- package/dist/esm/build-cloud/BuildOrchestration.js +1071 -1097
- package/dist/esm/build-cloud/GodotBuildCloud.js +647 -669
- package/dist/esm/build-cloud/UnityBuildCloud.js +571 -609
- package/dist/esm/build-cloud/UnrealBuildCloud.js +615 -634
- package/dist/esm/core/EventBus.js +52 -55
- package/dist/esm/core/GameClock.js +63 -65
- package/dist/esm/core/GameConfig.js +77 -78
- package/dist/esm/core/GameLoop.js +81 -88
- package/dist/esm/core/ServiceLocator.js +63 -64
- package/dist/esm/core/Validation.js +82 -81
- package/dist/esm/core/math.js +109 -111
- package/dist/esm/devtools/DevTools.js +546 -562
- package/dist/esm/devtools/DeveloperExperience.js +535 -558
- package/dist/esm/devtools/GameplayAnalytics.js +647 -645
- package/dist/esm/dialogue/DialogueSystem.js +998 -985
- package/dist/esm/docs/DocGenerator.js +342 -363
- package/dist/esm/ecs/World.js +358 -370
- package/dist/esm/editor/AchievementEditor.js +584 -635
- package/dist/esm/editor/AdvancedEditor.js +191 -190
- package/dist/esm/editor/AnimationRetargeting.js +700 -740
- package/dist/esm/editor/AssetBrowser.js +1078 -1159
- package/dist/esm/editor/AssetManager.js +188 -191
- package/dist/esm/editor/ConsolePro.js +977 -1060
- package/dist/esm/editor/CraftingEditor.js +684 -729
- package/dist/esm/editor/DebugTools.js +194 -184
- package/dist/esm/editor/DialogueSystem.js +849 -900
- package/dist/esm/editor/LeaderboardSystem.js +433 -482
- package/dist/esm/editor/NiceGameEditor.js +1908 -1200
- package/dist/esm/editor/PrefabSystem.js +777 -837
- package/dist/esm/editor/ProfilerPanel.js +654 -699
- package/dist/esm/editor/QuestEditor.js +795 -838
- package/dist/esm/editor/ReplaySystem.js +591 -633
- package/dist/esm/editor/SceneEditor.js +189 -199
- package/dist/esm/editor/ScreenshotMode.js +628 -687
- package/dist/esm/editor/ShaderGraph.js +1565 -1581
- package/dist/esm/editor/TimelineEditor.js +733 -777
- package/dist/esm/editor/UndoRedoPro.js +835 -903
- package/dist/esm/editor/VRARMode.js +586 -644
- package/dist/esm/engine/NiceGameEngine.js +142 -163
- package/dist/esm/engine/SaveSystem.js +447 -494
- package/dist/esm/enterprise/Enterprise.js +265 -260
- package/dist/esm/export/BevyExporter.js +929 -966
- package/dist/esm/export/CocosExporter.js +646 -672
- package/dist/esm/export/Construct3Exporter.js +785 -793
- package/dist/esm/export/DefoldExporter.js +1030 -1068
- package/dist/esm/export/GDevelopExporter.js +676 -707
- package/dist/esm/export/GameMakerExporter.js +793 -804
- package/dist/esm/export/GodotExporter.js +1004 -1044
- package/dist/esm/export/MinecraftBedrockExporter.js +575 -577
- package/dist/esm/export/MonoGameExporter.js +1267 -1292
- package/dist/esm/export/Pico8Exporter.js +768 -811
- package/dist/esm/export/PlatformExporter.js +193 -205
- package/dist/esm/export/RPGMakerExporter.js +862 -878
- package/dist/esm/export/RobloxExporter.js +885 -918
- package/dist/esm/export/Solar2DExporter.js +1220 -1255
- package/dist/esm/export/ThreeJSExporter.js +1007 -1072
- package/dist/esm/export/Tic80Exporter.js +1212 -1266
- package/dist/esm/export/UnityExporter.js +1117 -1130
- package/dist/esm/export/UnrealExporter.js +891 -918
- package/dist/esm/export/WebExporter.js +933 -969
- package/dist/esm/export/index.js +86 -71
- package/dist/esm/godot-integration/GodotAssetLibrary.js +663 -691
- package/dist/esm/godot-integration/GodotImport.js +935 -1026
- package/dist/esm/godot-integration/GodotLiveLink.js +918 -944
- package/dist/esm/godot-integration/components/GodotProjectBrowser.js +912 -610
- package/dist/esm/i18n/I18n.js +871 -899
- package/dist/esm/i18n/useTranslation.js +88 -88
- package/dist/esm/import/AsepriteImporter.js +637 -715
- package/dist/esm/import/DragonBonesImporter.js +417 -460
- package/dist/esm/import/GameMakerImporter.js +491 -520
- package/dist/esm/import/GodotSceneImporter.js +767 -821
- package/dist/esm/import/LDtkImporter.js +415 -453
- package/dist/esm/import/Live2DImporter.js +477 -517
- package/dist/esm/import/NdgFormat.js +410 -456
- package/dist/esm/import/OgmoImporter.js +446 -492
- package/dist/esm/import/RPGMakerImporter.js +431 -476
- package/dist/esm/import/SceneImporter.js +385 -422
- package/dist/esm/import/SpineImporter.js +491 -545
- package/dist/esm/import/SpriterImporter.js +570 -612
- package/dist/esm/import/TiledMapImporter.js +790 -845
- package/dist/esm/import/UnitySceneImporter.js +666 -702
- package/dist/esm/import/index.js +247 -258
- package/dist/esm/index.js +1332 -188
- package/dist/esm/input/GamepadNavigation.js +187 -204
- package/dist/esm/input/InputManager.js +401 -420
- package/dist/esm/input/useGamepads.js +72 -80
- package/dist/esm/integration/IconSprite.js +143 -255
- package/dist/esm/inventory/InventorySystem.js +925 -994
- package/dist/esm/kids/KidMode.js +1003 -1021
- package/dist/esm/kids/KidTools.js +573 -593
- package/dist/esm/monetization/Monetization.js +180 -176
- package/dist/esm/multiplayer/LocalMultiplayer.js +290 -286
- package/dist/esm/multiplayer/MiniGameTypes.js +58 -58
- package/dist/esm/native-engine/AnimationSystem.js +735 -805
- package/dist/esm/native-engine/AssetPipeline.js +456 -514
- package/dist/esm/native-engine/AudioEngine.js +765 -803
- package/dist/esm/native-engine/InputSystem.js +1079 -1144
- package/dist/esm/native-engine/NetworkingSystem.js +740 -792
- package/dist/esm/native-engine/PhysicsEngine.js +585 -606
- package/dist/esm/native-engine/RenderingEngine.js +304 -349
- package/dist/esm/native-engine/SceneGraph.js +773 -807
- package/dist/esm/network/MultiplayerTransport.js +99 -131
- package/dist/esm/network/Networking.js +556 -563
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/AbortController.js +22 -28
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/AccessTokenHttpClient.js +41 -47
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/DefaultHttpClient.js +36 -42
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/DefaultReconnectPolicy.js +11 -14
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Errors.js +111 -134
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/FetchHttpClient.js +132 -157
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HandshakeProtocol.js +40 -52
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HeaderNames.js +6 -7
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HttpClient.js +42 -47
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HttpConnection.js +496 -571
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HubConnection.js +828 -952
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/HubConnectionBuilder.js +126 -145
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/IHubProtocol.js +14 -23
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ILogger.js +12 -25
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ITransport.js +15 -24
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/JsonHubProtocol.js +109 -120
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Loggers.js +11 -13
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/LongPollingTransport.js +151 -184
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/MessageBuffer.js +159 -195
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/ServerSentEventsTransport.js +91 -104
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Subject.js +31 -34
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/TextMessageFormat.js +17 -20
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/Utils.js +230 -248
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/WebSocketTransport.js +136 -156
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/XhrHttpClient.js +77 -84
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/index.js +31 -12
- package/dist/esm/node_modules/@microsoft/signalr/dist/esm/pkg-version.js +4 -3
- package/dist/esm/pathfinding/Pathfinding.js +249 -262
- package/dist/esm/performance/AssetStreaming.js +567 -634
- package/dist/esm/performance/BenchmarkSuite.js +285 -310
- package/dist/esm/performance/DeltaCompression.js +478 -525
- package/dist/esm/performance/DeterministicPhysics.js +479 -497
- package/dist/esm/performance/ECSOptimization.js +653 -694
- package/dist/esm/performance/GameSaveSystem.js +515 -555
- package/dist/esm/performance/HotReload.js +454 -499
- package/dist/esm/performance/InputRecording.js +520 -560
- package/dist/esm/performance/InterestManagement.js +460 -499
- package/dist/esm/performance/LocalizationRuntime.js +382 -422
- package/dist/esm/performance/MemoryManagement.js +415 -469
- package/dist/esm/performance/ModSupport.js +455 -495
- package/dist/esm/performance/MultiThreadedPhysics.js +408 -442
- package/dist/esm/performance/NetworkPrediction.js +436 -481
- package/dist/esm/performance/ObjectPooling.js +582 -616
- package/dist/esm/performance/Performance.js +182 -189
- package/dist/esm/performance/PerformanceAdvanced.js +621 -636
- package/dist/esm/performance/WebGPURenderer.js +660 -682
- package/dist/esm/physics/PhysicsAdvanced.js +359 -378
- package/dist/esm/physics/PhysicsEngine2D.js +393 -426
- package/dist/esm/plugins/PluginSDK.js +435 -465
- package/dist/esm/plugins/PluginTestKit.js +315 -329
- package/dist/esm/procedural/Procedural.js +248 -253
- package/dist/esm/procedural/WorldBuilding.js +316 -313
- package/dist/esm/quest/QuestSystem.js +982 -931
- package/dist/esm/release/MobileExport.js +163 -162
- package/dist/esm/release/Release.js +234 -237
- package/dist/esm/rendering/Camera2D.js +122 -123
- package/dist/esm/rendering/Renderer2D.js +323 -334
- package/dist/esm/rendering/WebGPURenderPipeline.js +647 -650
- package/dist/esm/runtime3d/Runtime3D.js +368 -377
- package/dist/esm/runtime3d/SceneEditor3D.js +268 -262
- package/dist/esm/scene/SceneManager.js +178 -192
- package/dist/esm/scripting/GraphToAST.js +576 -546
- package/dist/esm/scripting/LanguageExporter.js +263 -278
- package/dist/esm/scripting/NodeGraph.js +550 -569
- package/dist/esm/scripting/ScriptAST.js +45 -38
- package/dist/esm/scripting/VisualScripting2.js +1025 -1064
- package/dist/esm/scripting/exporters/CSharpExporter.js +465 -495
- package/dist/esm/scripting/exporters/GDScriptExporter.js +391 -441
- package/dist/esm/scripting/exporters/LuaExporter.js +407 -446
- package/dist/esm/scripting/exporters/PythonExporter.js +485 -548
- package/dist/esm/scripting/exporters/RustExporter.js +460 -511
- package/dist/esm/scripting/exporters/TypeScriptExporter.js +506 -555
- package/dist/esm/social/Social.js +77 -79
- package/dist/esm/systems/LightingSystem.js +855 -930
- package/dist/esm/systems/ParticleSystem2.js +1453 -1450
- package/dist/esm/systems/PhysicsDebugSystem.js +1078 -1140
- package/dist/esm/systems/TerrainSystem.js +996 -1097
- package/dist/esm/templates/ActionTemplates.js +307 -308
- package/dist/esm/templates/FightingTemplate.js +439 -462
- package/dist/esm/templates/MetroidvaniaTemplate.js +391 -404
- package/dist/esm/templates/PartyTemplates.js +206 -201
- package/dist/esm/templates/PuzzleTemplates.js +260 -260
- package/dist/esm/templates/RPGTemplates.js +172 -177
- package/dist/esm/templates/SportsTemplates.js +251 -249
- package/dist/esm/templates/StrategyTemplates.js +158 -152
- package/dist/esm/templates/SurvivalTemplate.js +367 -379
- package/dist/esm/templates/WaveDefense.js +501 -525
- package/dist/esm/tilemap/Tilemap.js +209 -213
- package/dist/esm/tutorials/TutorialBranching.js +399 -416
- package/dist/esm/tutorials/TutorialEngine.js +893 -930
- package/dist/esm/tutorials/TutorialOverlay.js +603 -639
- package/dist/esm/tutorials/content/Tutorials_T05_T08.js +792 -808
- package/dist/esm/tutorials/content/Tutorials_T09_T11.js +740 -755
- package/dist/esm/tutorials/content/Tutorials_T12_T15.js +759 -777
- package/dist/esm/ui/GameUI.js +296 -311
- package/dist/esm/unity-integration/UnityAssetStore.js +704 -740
- package/dist/esm/unity-integration/UnityImport.js +1071 -1175
- package/dist/esm/unity-integration/UnityLiveLink.js +938 -980
- package/dist/esm/unity-integration/components/UnityAssetStoreBrowser.js +1234 -756
- package/dist/esm/unity-integration/components/UnityProjectBrowser.js +1241 -767
- package/dist/esm/unreal-integration/UnrealImport.js +395 -411
- package/dist/esm/unreal-integration/UnrealLiveLink.js +1105 -1136
- package/dist/esm/unreal-integration/UnrealMarketplace.js +442 -472
- package/dist/esm/unreal-integration/components/UnrealProjectBrowser.js +1151 -766
- package/dist/esm/xr/ARVR.js +181 -174
- package/package.json +7 -12
|
@@ -1,1128 +1,1015 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file TerrainSystem.ts
|
|
5
|
-
* @description Advanced Terrain System with Heightmap, Splatmap, and Foliage
|
|
6
|
-
* PRO-1.4: Professional Editor Features
|
|
7
|
-
*
|
|
8
|
-
* Features:
|
|
9
|
-
* - Heightmap terrain with LOD
|
|
10
|
-
* - Splatmap texture blending (up to 8 layers)
|
|
11
|
-
* - Foliage scattering (grass, trees, rocks)
|
|
12
|
-
* - Terrain brushes (raise, lower, smooth, flatten, noise, erosion)
|
|
13
|
-
* - Procedural generation (Perlin noise, diamond-square, hydraulic erosion)
|
|
14
|
-
* - Holes/caves support
|
|
15
|
-
* - Detail meshes and decals
|
|
16
|
-
* - Terrain chunks with streaming
|
|
17
|
-
* - Physics collision mesh generation
|
|
18
|
-
*/
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
19
3
|
const DEFAULT_TERRAIN_CONFIG = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
4
|
+
width: 1e3,
|
|
5
|
+
depth: 1e3,
|
|
6
|
+
maxHeight: 100,
|
|
7
|
+
heightmapResolution: 513,
|
|
8
|
+
// Power of 2 + 1 for LOD
|
|
9
|
+
splatmapResolution: 512,
|
|
10
|
+
chunksPerSide: 4,
|
|
11
|
+
detailResolution: 1024,
|
|
12
|
+
detailResolutionPerPatch: 8,
|
|
13
|
+
basemapDistance: 1e3,
|
|
14
|
+
castShadows: true,
|
|
15
|
+
drawInstanced: true
|
|
31
16
|
};
|
|
32
17
|
function createTerrainLayer(id, name, diffuseTexture) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
name,
|
|
21
|
+
diffuseTexture,
|
|
22
|
+
tileSize: { x: 15, y: 15 },
|
|
23
|
+
tileOffset: { x: 0, y: 0 },
|
|
24
|
+
metallic: 0,
|
|
25
|
+
smoothness: 0.5,
|
|
26
|
+
specularColor: { r: 0.2, g: 0.2, b: 0.2, a: 1 },
|
|
27
|
+
normalScale: 1,
|
|
28
|
+
diffuseRemapMin: { r: 0, g: 0, b: 0, a: 0 },
|
|
29
|
+
diffuseRemapMax: { r: 1, g: 1, b: 1, a: 1 },
|
|
30
|
+
maskRemapMin: { r: 0, g: 0, b: 0, a: 0 },
|
|
31
|
+
maskRemapMax: { r: 1, g: 1, b: 1, a: 1 }
|
|
32
|
+
};
|
|
48
33
|
}
|
|
49
34
|
function createGrassDetail(id, name, textureId) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
name,
|
|
38
|
+
textureId,
|
|
39
|
+
renderMode: "grass",
|
|
40
|
+
useInstancing: true,
|
|
41
|
+
healthyColor: { r: 0.26, g: 0.8, b: 0.28, a: 1 },
|
|
42
|
+
dryColor: { r: 0.8, g: 0.72, b: 0.28, a: 1 },
|
|
43
|
+
minWidth: 0.8,
|
|
44
|
+
maxWidth: 1.2,
|
|
45
|
+
minHeight: 0.4,
|
|
46
|
+
maxHeight: 0.8,
|
|
47
|
+
noiseSpread: 0.1,
|
|
48
|
+
density: 1,
|
|
49
|
+
alignToGround: 0.5,
|
|
50
|
+
positionJitter: 0.5,
|
|
51
|
+
holeEdgePadding: 0
|
|
52
|
+
};
|
|
68
53
|
}
|
|
69
54
|
function createTreePrototype(id, name, meshId) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
55
|
+
return {
|
|
56
|
+
id,
|
|
57
|
+
name,
|
|
58
|
+
meshId,
|
|
59
|
+
lodMeshes: [],
|
|
60
|
+
bendFactor: 1,
|
|
61
|
+
minScale: 0.8,
|
|
62
|
+
maxScale: 1.2,
|
|
63
|
+
randomRotation: true,
|
|
64
|
+
alignToGround: 0.5,
|
|
65
|
+
navMeshArea: 0
|
|
66
|
+
};
|
|
82
67
|
}
|
|
83
68
|
const DEFAULT_BRUSH = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
69
|
+
type: "raise",
|
|
70
|
+
size: 10,
|
|
71
|
+
strength: 0.5,
|
|
72
|
+
falloff: 0.5,
|
|
73
|
+
rotation: 0,
|
|
74
|
+
spacingJitter: 0,
|
|
75
|
+
scatter: 0
|
|
91
76
|
};
|
|
92
|
-
// ============================================================
|
|
93
|
-
// Terrain System Class
|
|
94
|
-
// ============================================================
|
|
95
77
|
class TerrainSystem {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
z: (gz + 1) * chunkDepth - config.depth / 2,
|
|
141
|
-
},
|
|
142
|
-
isLoaded: gx < 2 && gz < 2, // Load center chunks
|
|
143
|
-
lodLevel: 0,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
id: `terrain_${Date.now()}`,
|
|
149
|
-
name: 'New Terrain',
|
|
150
|
-
config,
|
|
151
|
-
heightmap,
|
|
152
|
-
splatmaps,
|
|
153
|
-
holemap,
|
|
154
|
-
layers: [],
|
|
155
|
-
detailPrototypes: [],
|
|
156
|
-
detailMaps: [],
|
|
157
|
-
treePrototypes: [],
|
|
158
|
-
trees: [],
|
|
159
|
-
chunks: chunkList,
|
|
160
|
-
metadata: {
|
|
161
|
-
createdAt: Date.now(),
|
|
162
|
-
modifiedAt: Date.now(),
|
|
163
|
-
version: 1,
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
// ─── Heightmap Operations ──────────────────────────────
|
|
168
|
-
/** Get height at grid coordinates (0 to resolution-1) */
|
|
169
|
-
getHeightAtGrid(gridX, gridZ) {
|
|
170
|
-
const res = this.data.config.heightmapResolution;
|
|
171
|
-
const x = Math.max(0, Math.min(res - 1, Math.floor(gridX)));
|
|
172
|
-
const z = Math.max(0, Math.min(res - 1, Math.floor(gridZ)));
|
|
173
|
-
return this.data.heightmap[z * res + x] * this.data.config.maxHeight;
|
|
174
|
-
}
|
|
175
|
-
/** Set height at grid coordinates */
|
|
176
|
-
setHeightAtGrid(gridX, gridZ, height, recordUndo = true) {
|
|
177
|
-
const res = this.data.config.heightmapResolution;
|
|
178
|
-
const maxH = this.data.config.maxHeight;
|
|
179
|
-
if (gridX < 0 || gridX >= res || gridZ < 0 || gridZ >= res) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const idx = gridZ * res + gridX;
|
|
183
|
-
const normalizedHeight = Math.max(0, Math.min(1, height / maxH));
|
|
184
|
-
if (recordUndo) {
|
|
185
|
-
this.recordHeightChange(gridX, gridZ, this.data.heightmap[idx], normalizedHeight);
|
|
186
|
-
}
|
|
187
|
-
this.data.heightmap[idx] = normalizedHeight;
|
|
188
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
189
|
-
}
|
|
190
|
-
/** Get interpolated height at world coordinates */
|
|
191
|
-
getHeightAtWorld(worldX, worldZ) {
|
|
192
|
-
const { width, depth, heightmapResolution: res } = this.data.config;
|
|
193
|
-
// Convert world to grid
|
|
194
|
-
const gx = ((worldX + width / 2) / width) * (res - 1);
|
|
195
|
-
const gz = ((worldZ + depth / 2) / depth) * (res - 1);
|
|
196
|
-
// Bilinear interpolation
|
|
197
|
-
const ix = Math.floor(gx);
|
|
198
|
-
const iz = Math.floor(gz);
|
|
199
|
-
const fx = gx - ix;
|
|
200
|
-
const fz = gz - iz;
|
|
201
|
-
const h00 = this.getHeightAtGrid(ix, iz);
|
|
202
|
-
const h10 = this.getHeightAtGrid(ix + 1, iz);
|
|
203
|
-
const h01 = this.getHeightAtGrid(ix, iz + 1);
|
|
204
|
-
const h11 = this.getHeightAtGrid(ix + 1, iz + 1);
|
|
205
|
-
const h0 = h00 + (h10 - h00) * fx;
|
|
206
|
-
const h1 = h01 + (h11 - h01) * fx;
|
|
207
|
-
return h0 + (h1 - h0) * fz;
|
|
208
|
-
}
|
|
209
|
-
/** Convert world position to grid coordinates */
|
|
210
|
-
worldToGrid(worldX, worldZ) {
|
|
211
|
-
const { width, depth, heightmapResolution: res } = this.data.config;
|
|
212
|
-
return {
|
|
213
|
-
x: ((worldX + width / 2) / width) * (res - 1),
|
|
214
|
-
y: ((worldZ + depth / 2) / depth) * (res - 1),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
/** Convert grid coordinates to world position */
|
|
218
|
-
gridToWorld(gridX, gridZ) {
|
|
219
|
-
const { width, depth, heightmapResolution: res } = this.data.config;
|
|
220
|
-
const worldX = (gridX / (res - 1)) * width - width / 2;
|
|
221
|
-
const worldZ = (gridZ / (res - 1)) * depth - depth / 2;
|
|
222
|
-
const worldY = this.getHeightAtGrid(gridX, gridZ);
|
|
223
|
-
return { x: worldX, y: worldY, z: worldZ };
|
|
224
|
-
}
|
|
225
|
-
/// Get terrain normal at world position
|
|
226
|
-
getNormalAtWorld(worldX, worldZ) {
|
|
227
|
-
const { width, depth, heightmapResolution: res } = this.data.config;
|
|
228
|
-
const cellWidth = width / (res - 1);
|
|
229
|
-
const cellDepth = depth / (res - 1);
|
|
230
|
-
const hL = this.getHeightAtWorld(worldX - cellWidth, worldZ);
|
|
231
|
-
const hR = this.getHeightAtWorld(worldX + cellWidth, worldZ);
|
|
232
|
-
const hD = this.getHeightAtWorld(worldX, worldZ - cellDepth);
|
|
233
|
-
const hU = this.getHeightAtWorld(worldX, worldZ + cellDepth);
|
|
234
|
-
// Cross product to get normal
|
|
235
|
-
const dx = hR - hL;
|
|
236
|
-
const dz = hU - hD;
|
|
237
|
-
const nx = -dx / (2 * cellWidth);
|
|
238
|
-
const ny = 1;
|
|
239
|
-
const nz = -dz / (2 * cellDepth);
|
|
240
|
-
// Normalize
|
|
241
|
-
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
242
|
-
return { x: nx / len, y: ny / len, z: nz / len };
|
|
243
|
-
}
|
|
244
|
-
// ─── Brush Operations ──────────────────────────────────
|
|
245
|
-
applyBrush(brush, worldX, worldZ) {
|
|
246
|
-
switch (brush.type) {
|
|
247
|
-
case 'raise':
|
|
248
|
-
this.applyHeightBrush(worldX, worldZ, brush, 1);
|
|
249
|
-
break;
|
|
250
|
-
case 'lower':
|
|
251
|
-
this.applyHeightBrush(worldX, worldZ, brush, -1);
|
|
252
|
-
break;
|
|
253
|
-
case 'flatten':
|
|
254
|
-
this.applyFlattenBrush(worldX, worldZ, brush);
|
|
255
|
-
break;
|
|
256
|
-
case 'smooth':
|
|
257
|
-
this.applySmoothBrush(worldX, worldZ, brush);
|
|
258
|
-
break;
|
|
259
|
-
case 'noise':
|
|
260
|
-
this.applyNoiseBrush(worldX, worldZ, brush);
|
|
261
|
-
break;
|
|
262
|
-
case 'erosion':
|
|
263
|
-
this.applyErosionBrush(worldX, worldZ, brush);
|
|
264
|
-
break;
|
|
265
|
-
case 'paint':
|
|
266
|
-
this.applyPaintBrush(worldX, worldZ, brush);
|
|
267
|
-
break;
|
|
268
|
-
case 'paintHoles':
|
|
269
|
-
this.applyHoleBrush(worldX, worldZ, brush);
|
|
270
|
-
break;
|
|
271
|
-
case 'stampHeight':
|
|
272
|
-
this.applyStampBrush(worldX, worldZ, brush);
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
applyHeightBrush(worldX, worldZ, brush, direction) {
|
|
277
|
-
const grid = this.worldToGrid(worldX, worldZ);
|
|
278
|
-
const { heightmapResolution: res, width, maxHeight } = this.data.config;
|
|
279
|
-
const gridRadius = (brush.size / (width / (res - 1))) * 0.5;
|
|
280
|
-
const delta = (brush.strength * direction * maxHeight) / 100;
|
|
281
|
-
const minX = Math.max(0, Math.floor(grid.x - gridRadius));
|
|
282
|
-
const maxX = Math.min(res - 1, Math.ceil(grid.x + gridRadius));
|
|
283
|
-
const minZ = Math.max(0, Math.floor(grid.y - gridRadius));
|
|
284
|
-
const maxZ = Math.min(res - 1, Math.ceil(grid.y + gridRadius));
|
|
285
|
-
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
286
|
-
for (let gx = minX; gx <= maxX; gx++) {
|
|
287
|
-
const dx = gx - grid.x;
|
|
288
|
-
const dz = gz - grid.y;
|
|
289
|
-
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
290
|
-
if (dist <= gridRadius) {
|
|
291
|
-
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
292
|
-
const currentHeight = this.getHeightAtGrid(gx, gz);
|
|
293
|
-
const newHeight = currentHeight + delta * falloff;
|
|
294
|
-
this.setHeightAtGrid(gx, gz, newHeight);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
applyFlattenBrush(worldX, worldZ, brush) {
|
|
300
|
-
const grid = this.worldToGrid(worldX, worldZ);
|
|
301
|
-
const { heightmapResolution: res, width } = this.data.config;
|
|
302
|
-
const gridRadius = (brush.size / (width / (res - 1))) * 0.5;
|
|
303
|
-
const targetHeight = brush.targetHeight ?? this.getHeightAtWorld(worldX, worldZ);
|
|
304
|
-
const minX = Math.max(0, Math.floor(grid.x - gridRadius));
|
|
305
|
-
const maxX = Math.min(res - 1, Math.ceil(grid.x + gridRadius));
|
|
306
|
-
const minZ = Math.max(0, Math.floor(grid.y - gridRadius));
|
|
307
|
-
const maxZ = Math.min(res - 1, Math.ceil(grid.y + gridRadius));
|
|
308
|
-
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
309
|
-
for (let gx = minX; gx <= maxX; gx++) {
|
|
310
|
-
const dx = gx - grid.x;
|
|
311
|
-
const dz = gz - grid.y;
|
|
312
|
-
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
313
|
-
if (dist <= gridRadius) {
|
|
314
|
-
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
315
|
-
const currentHeight = this.getHeightAtGrid(gx, gz);
|
|
316
|
-
const newHeight = currentHeight + (targetHeight - currentHeight) * falloff * brush.strength;
|
|
317
|
-
this.setHeightAtGrid(gx, gz, newHeight);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
applySmoothBrush(worldX, worldZ, brush) {
|
|
323
|
-
const grid = this.worldToGrid(worldX, worldZ);
|
|
324
|
-
const { heightmapResolution: res, width } = this.data.config;
|
|
325
|
-
const gridRadius = (brush.size / (width / (res - 1))) * 0.5;
|
|
326
|
-
const minX = Math.max(0, Math.floor(grid.x - gridRadius));
|
|
327
|
-
const maxX = Math.min(res - 1, Math.ceil(grid.x + gridRadius));
|
|
328
|
-
const minZ = Math.max(0, Math.floor(grid.y - gridRadius));
|
|
329
|
-
const maxZ = Math.min(res - 1, Math.ceil(grid.y + gridRadius));
|
|
330
|
-
// Collect heights first
|
|
331
|
-
const newHeights = [];
|
|
332
|
-
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
333
|
-
for (let gx = minX; gx <= maxX; gx++) {
|
|
334
|
-
const dx = gx - grid.x;
|
|
335
|
-
const dz = gz - grid.y;
|
|
336
|
-
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
337
|
-
if (dist <= gridRadius) {
|
|
338
|
-
// Average of neighbors
|
|
339
|
-
let sum = 0;
|
|
340
|
-
let count = 0;
|
|
341
|
-
for (let nz = -1; nz <= 1; nz++) {
|
|
342
|
-
for (let nx = -1; nx <= 1; nx++) {
|
|
343
|
-
const ngx = gx + nx;
|
|
344
|
-
const ngz = gz + nz;
|
|
345
|
-
if (ngx >= 0 && ngx < res && ngz >= 0 && ngz < res) {
|
|
346
|
-
sum += this.getHeightAtGrid(ngx, ngz);
|
|
347
|
-
count++;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
const avgHeight = sum / count;
|
|
352
|
-
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
353
|
-
const currentHeight = this.getHeightAtGrid(gx, gz);
|
|
354
|
-
const newHeight = currentHeight + (avgHeight - currentHeight) * falloff * brush.strength;
|
|
355
|
-
newHeights.push({ gx, gz, height: newHeight });
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
// Apply new heights
|
|
360
|
-
for (const { gx, gz, height } of newHeights) {
|
|
361
|
-
this.setHeightAtGrid(gx, gz, height);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
applyNoiseBrush(worldX, worldZ, brush) {
|
|
365
|
-
const grid = this.worldToGrid(worldX, worldZ);
|
|
366
|
-
const { heightmapResolution: res, width, maxHeight } = this.data.config;
|
|
367
|
-
const gridRadius = (brush.size / (width / (res - 1))) * 0.5;
|
|
368
|
-
const noiseScale = brush.noiseScale ?? 0.1;
|
|
369
|
-
const octaves = brush.noiseOctaves ?? 4;
|
|
370
|
-
const minX = Math.max(0, Math.floor(grid.x - gridRadius));
|
|
371
|
-
const maxX = Math.min(res - 1, Math.ceil(grid.x + gridRadius));
|
|
372
|
-
const minZ = Math.max(0, Math.floor(grid.y - gridRadius));
|
|
373
|
-
const maxZ = Math.min(res - 1, Math.ceil(grid.y + gridRadius));
|
|
374
|
-
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
375
|
-
for (let gx = minX; gx <= maxX; gx++) {
|
|
376
|
-
const dx = gx - grid.x;
|
|
377
|
-
const dz = gz - grid.y;
|
|
378
|
-
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
379
|
-
if (dist <= gridRadius) {
|
|
380
|
-
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
381
|
-
const noise = this.perlinNoise2D(gx * noiseScale, gz * noiseScale, octaves);
|
|
382
|
-
const delta = noise * brush.strength * maxHeight * 0.1 * falloff;
|
|
383
|
-
const currentHeight = this.getHeightAtGrid(gx, gz);
|
|
384
|
-
this.setHeightAtGrid(gx, gz, currentHeight + delta);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
applyErosionBrush(worldX, worldZ, brush) {
|
|
390
|
-
const grid = this.worldToGrid(worldX, worldZ);
|
|
391
|
-
const { heightmapResolution: res, width } = this.data.config;
|
|
392
|
-
const gridRadius = (brush.size / (width / (res - 1))) * 0.5;
|
|
393
|
-
const minX = Math.max(1, Math.floor(grid.x - gridRadius));
|
|
394
|
-
const maxX = Math.min(res - 2, Math.ceil(grid.x + gridRadius));
|
|
395
|
-
const minZ = Math.max(1, Math.floor(grid.y - gridRadius));
|
|
396
|
-
const maxZ = Math.min(res - 2, Math.ceil(grid.y + gridRadius));
|
|
397
|
-
// Simple thermal erosion
|
|
398
|
-
const newHeights = [];
|
|
399
|
-
const talusAngle = 0.5; // Adjustment factor
|
|
400
|
-
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
401
|
-
for (let gx = minX; gx <= maxX; gx++) {
|
|
402
|
-
const dx = gx - grid.x;
|
|
403
|
-
const dz = gz - grid.y;
|
|
404
|
-
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
405
|
-
if (dist <= gridRadius) {
|
|
406
|
-
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
407
|
-
const h = this.getHeightAtGrid(gx, gz);
|
|
408
|
-
let totalDiff = 0;
|
|
409
|
-
const neighbors = [
|
|
410
|
-
this.getHeightAtGrid(gx - 1, gz),
|
|
411
|
-
this.getHeightAtGrid(gx + 1, gz),
|
|
412
|
-
this.getHeightAtGrid(gx, gz - 1),
|
|
413
|
-
this.getHeightAtGrid(gx, gz + 1),
|
|
414
|
-
];
|
|
415
|
-
for (const nh of neighbors) {
|
|
416
|
-
const diff = h - nh;
|
|
417
|
-
if (diff > talusAngle) {
|
|
418
|
-
totalDiff += diff - talusAngle;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
// Erode if slope too steep
|
|
422
|
-
if (totalDiff > 0) {
|
|
423
|
-
const erosion = totalDiff * brush.strength * falloff * 0.25;
|
|
424
|
-
newHeights.push({ gx, gz, height: h - erosion });
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
// Apply erosion
|
|
430
|
-
for (const { gx, gz, height } of newHeights) {
|
|
431
|
-
this.setHeightAtGrid(gx, gz, height);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
applyPaintBrush(worldX, worldZ, brush) {
|
|
435
|
-
const { splatmapResolution: res, width } = this.data.config;
|
|
436
|
-
const layerIndex = brush.layerIndex ?? 0;
|
|
437
|
-
if (layerIndex >= 8) {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
const splatmapIndex = Math.floor(layerIndex / 4);
|
|
441
|
-
const channelIndex = layerIndex % 4;
|
|
442
|
-
// Convert world to splatmap coords
|
|
443
|
-
const smX = ((worldX + width / 2) / width) * (res - 1);
|
|
444
|
-
const smZ = ((worldZ + width / 2) / width) * (res - 1);
|
|
445
|
-
const gridRadius = (brush.size / (width / (res - 1))) * 0.5;
|
|
446
|
-
const minX = Math.max(0, Math.floor(smX - gridRadius));
|
|
447
|
-
const maxX = Math.min(res - 1, Math.ceil(smX + gridRadius));
|
|
448
|
-
const minZ = Math.max(0, Math.floor(smZ - gridRadius));
|
|
449
|
-
const maxZ = Math.min(res - 1, Math.ceil(smZ + gridRadius));
|
|
450
|
-
const splatmap = this.data.splatmaps[splatmapIndex];
|
|
451
|
-
for (let z = minZ; z <= maxZ; z++) {
|
|
452
|
-
for (let x = minX; x <= maxX; x++) {
|
|
453
|
-
const dx = x - smX;
|
|
454
|
-
const dz = z - smZ;
|
|
455
|
-
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
456
|
-
if (dist <= gridRadius) {
|
|
457
|
-
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
458
|
-
const idx = (z * res + x) * 4;
|
|
459
|
-
const strength = brush.strength * falloff * 255;
|
|
460
|
-
// Add to target channel
|
|
461
|
-
const current = splatmap[idx + channelIndex];
|
|
462
|
-
const newValue = Math.min(255, current + strength);
|
|
463
|
-
splatmap[idx + channelIndex] = newValue;
|
|
464
|
-
// Normalize all channels so they sum to 255
|
|
465
|
-
this.normalizeSplatmapPixel(x, z);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
470
|
-
}
|
|
471
|
-
normalizeSplatmapPixel(x, z) {
|
|
472
|
-
const res = this.data.config.splatmapResolution;
|
|
473
|
-
let total = 0;
|
|
474
|
-
// Sum all 8 channels
|
|
475
|
-
for (let s = 0; s < 2; s++) {
|
|
476
|
-
const idx = (z * res + x) * 4;
|
|
477
|
-
for (let c = 0; c < 4; c++) {
|
|
478
|
-
total += this.data.splatmaps[s][idx + c];
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
// Normalize
|
|
482
|
-
if (total > 0) {
|
|
483
|
-
const scale = 255 / total;
|
|
484
|
-
for (let s = 0; s < 2; s++) {
|
|
485
|
-
const idx = (z * res + x) * 4;
|
|
486
|
-
for (let c = 0; c < 4; c++) {
|
|
487
|
-
this.data.splatmaps[s][idx + c] = Math.round(this.data.splatmaps[s][idx + c] * scale);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
applyHoleBrush(worldX, worldZ, brush) {
|
|
493
|
-
const { heightmapResolution: res, width } = this.data.config;
|
|
494
|
-
const gridCoords = this.worldToGrid(worldX, worldZ);
|
|
495
|
-
const gridRadius = (brush.size / (width / (res - 1))) * 0.5;
|
|
496
|
-
const isAddHole = brush.strength > 0;
|
|
497
|
-
const minX = Math.max(0, Math.floor(gridCoords.x - gridRadius));
|
|
498
|
-
const maxX = Math.min(res - 1, Math.ceil(gridCoords.x + gridRadius));
|
|
499
|
-
const minZ = Math.max(0, Math.floor(gridCoords.y - gridRadius));
|
|
500
|
-
const maxZ = Math.min(res - 1, Math.ceil(gridCoords.y + gridRadius));
|
|
501
|
-
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
502
|
-
for (let gx = minX; gx <= maxX; gx++) {
|
|
503
|
-
const dx = gx - gridCoords.x;
|
|
504
|
-
const dz = gz - gridCoords.y;
|
|
505
|
-
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
506
|
-
if (dist <= gridRadius) {
|
|
507
|
-
this.data.holemap[gz * res + gx] = isAddHole ? 0 : 1;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
512
|
-
}
|
|
513
|
-
applyStampBrush(worldX, worldZ, brush) {
|
|
514
|
-
// TODO: Apply stamp texture as height
|
|
515
|
-
// For now, just raise
|
|
516
|
-
this.applyHeightBrush(worldX, worldZ, brush, 1);
|
|
517
|
-
}
|
|
518
|
-
calculateFalloff(t, falloffStrength) {
|
|
519
|
-
// t is 0 at center, 1 at edge
|
|
520
|
-
// falloffStrength: 0 = hard edge, 1 = very smooth
|
|
521
|
-
if (t >= 1) {
|
|
522
|
-
return 0;
|
|
523
|
-
}
|
|
524
|
-
if (falloffStrength <= 0) {
|
|
525
|
-
return 1;
|
|
526
|
-
}
|
|
527
|
-
const smoothT = t * t * (3 - 2 * t); // Smoothstep
|
|
528
|
-
const power = 1 + falloffStrength * 3;
|
|
529
|
-
return Math.pow(1 - smoothT, power);
|
|
530
|
-
}
|
|
531
|
-
// ─── Procedural Generation ─────────────────────────────
|
|
532
|
-
generatePerlinTerrain(scale = 0.01, octaves = 6, persistence = 0.5) {
|
|
533
|
-
const res = this.data.config.heightmapResolution;
|
|
534
|
-
for (let z = 0; z < res; z++) {
|
|
535
|
-
for (let x = 0; x < res; x++) {
|
|
536
|
-
const height = this.perlinNoise2D(x * scale, z * scale, octaves, persistence);
|
|
537
|
-
this.data.heightmap[z * res + x] = (height + 1) * 0.5; // Normalize to 0-1
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
541
|
-
}
|
|
542
|
-
generateDiamondSquare(roughness = 0.5) {
|
|
543
|
-
const size = this.data.config.heightmapResolution;
|
|
544
|
-
// Initialize corners
|
|
545
|
-
this.data.heightmap[0] = Math.random();
|
|
546
|
-
this.data.heightmap[size - 1] = Math.random();
|
|
547
|
-
this.data.heightmap[(size - 1) * size] = Math.random();
|
|
548
|
-
this.data.heightmap[(size - 1) * size + size - 1] = Math.random();
|
|
549
|
-
let stepSize = size - 1;
|
|
550
|
-
let scale = roughness;
|
|
551
|
-
while (stepSize > 1) {
|
|
552
|
-
const halfStep = stepSize / 2;
|
|
553
|
-
// Diamond step
|
|
554
|
-
for (let z = halfStep; z < size - 1; z += stepSize) {
|
|
555
|
-
for (let x = halfStep; x < size - 1; x += stepSize) {
|
|
556
|
-
const avg = (this.data.heightmap[(z - halfStep) * size + (x - halfStep)] +
|
|
557
|
-
this.data.heightmap[(z - halfStep) * size + (x + halfStep)] +
|
|
558
|
-
this.data.heightmap[(z + halfStep) * size + (x - halfStep)] +
|
|
559
|
-
this.data.heightmap[(z + halfStep) * size + (x + halfStep)]) /
|
|
560
|
-
4;
|
|
561
|
-
this.data.heightmap[z * size + x] = avg + (Math.random() - 0.5) * scale;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
// Square step
|
|
565
|
-
for (let z = 0; z < size; z += halfStep) {
|
|
566
|
-
for (let x = (z + halfStep) % stepSize; x < size; x += stepSize) {
|
|
567
|
-
let sum = 0;
|
|
568
|
-
let count = 0;
|
|
569
|
-
if (z - halfStep >= 0) {
|
|
570
|
-
sum += this.data.heightmap[(z - halfStep) * size + x];
|
|
571
|
-
count++;
|
|
572
|
-
}
|
|
573
|
-
if (z + halfStep < size) {
|
|
574
|
-
sum += this.data.heightmap[(z + halfStep) * size + x];
|
|
575
|
-
count++;
|
|
576
|
-
}
|
|
577
|
-
if (x - halfStep >= 0) {
|
|
578
|
-
sum += this.data.heightmap[z * size + (x - halfStep)];
|
|
579
|
-
count++;
|
|
580
|
-
}
|
|
581
|
-
if (x + halfStep < size) {
|
|
582
|
-
sum += this.data.heightmap[z * size + (x + halfStep)];
|
|
583
|
-
count++;
|
|
584
|
-
}
|
|
585
|
-
this.data.heightmap[z * size + x] = sum / count + (Math.random() - 0.5) * scale;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
stepSize = halfStep;
|
|
589
|
-
scale *= 0.5;
|
|
590
|
-
}
|
|
591
|
-
// Normalize
|
|
592
|
-
let min = Infinity;
|
|
593
|
-
let max = -Infinity;
|
|
594
|
-
for (let i = 0; i < this.data.heightmap.length; i++) {
|
|
595
|
-
min = Math.min(min, this.data.heightmap[i]);
|
|
596
|
-
max = Math.max(max, this.data.heightmap[i]);
|
|
597
|
-
}
|
|
598
|
-
const range = max - min;
|
|
599
|
-
if (range > 0) {
|
|
600
|
-
for (let i = 0; i < this.data.heightmap.length; i++) {
|
|
601
|
-
this.data.heightmap[i] = (this.data.heightmap[i] - min) / range;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
605
|
-
}
|
|
606
|
-
applyHydraulicErosion(iterations = 50000) {
|
|
607
|
-
const res = this.data.config.heightmapResolution;
|
|
608
|
-
const { maxHeight: _maxHeight } = this.data.config;
|
|
609
|
-
// Erosion parameters
|
|
610
|
-
const inertia = 0.05;
|
|
611
|
-
const sedimentCapacityFactor = 4;
|
|
612
|
-
const minSedimentCapacity = 0.01;
|
|
613
|
-
const erodeSpeed = 0.3;
|
|
614
|
-
const depositSpeed = 0.3;
|
|
615
|
-
const evaporateSpeed = 0.01;
|
|
616
|
-
const gravity = 4;
|
|
617
|
-
const maxDropletLifetime = 30;
|
|
618
|
-
const initialWaterVolume = 1;
|
|
619
|
-
const initialSpeed = 1;
|
|
620
|
-
for (let i = 0; i < iterations; i++) {
|
|
621
|
-
// Start at random position
|
|
622
|
-
let posX = Math.random() * (res - 1);
|
|
623
|
-
let posZ = Math.random() * (res - 1);
|
|
624
|
-
let dirX = 0;
|
|
625
|
-
let dirZ = 0;
|
|
626
|
-
let speed = initialSpeed;
|
|
627
|
-
let water = initialWaterVolume;
|
|
628
|
-
let sediment = 0;
|
|
629
|
-
for (let lifetime = 0; lifetime < maxDropletLifetime; lifetime++) {
|
|
630
|
-
const cellX = Math.floor(posX);
|
|
631
|
-
const cellZ = Math.floor(posZ);
|
|
632
|
-
const cellOffsetX = posX - cellX;
|
|
633
|
-
const cellOffsetZ = posZ - cellZ;
|
|
634
|
-
// Calculate gradient
|
|
635
|
-
const heightNW = this.data.heightmap[cellZ * res + cellX];
|
|
636
|
-
const heightNE = cellX < res - 1 ? this.data.heightmap[cellZ * res + cellX + 1] : heightNW;
|
|
637
|
-
const heightSW = cellZ < res - 1 ? this.data.heightmap[(cellZ + 1) * res + cellX] : heightNW;
|
|
638
|
-
const heightSE = cellX < res - 1 && cellZ < res - 1
|
|
639
|
-
? this.data.heightmap[(cellZ + 1) * res + cellX + 1]
|
|
640
|
-
: heightNW;
|
|
641
|
-
const gradientX = (heightNE - heightNW) * (1 - cellOffsetZ) + (heightSE - heightSW) * cellOffsetZ;
|
|
642
|
-
const gradientZ = (heightSW - heightNW) * (1 - cellOffsetX) + (heightSE - heightNE) * cellOffsetX;
|
|
643
|
-
// Update direction with inertia
|
|
644
|
-
dirX = dirX * inertia - gradientX * (1 - inertia);
|
|
645
|
-
dirZ = dirZ * inertia - gradientZ * (1 - inertia);
|
|
646
|
-
// Normalize direction
|
|
647
|
-
const len = Math.sqrt(dirX * dirX + dirZ * dirZ);
|
|
648
|
-
if (len > 0) {
|
|
649
|
-
dirX /= len;
|
|
650
|
-
dirZ /= len;
|
|
651
|
-
}
|
|
652
|
-
// Move
|
|
653
|
-
posX += dirX;
|
|
654
|
-
posZ += dirZ;
|
|
655
|
-
// Check bounds
|
|
656
|
-
if (posX < 0 || posX >= res - 1 || posZ < 0 || posZ >= res - 1 || len === 0) {
|
|
657
|
-
break;
|
|
658
|
-
}
|
|
659
|
-
// Calculate height difference
|
|
660
|
-
const newHeight = heightNW * (1 - cellOffsetX) * (1 - cellOffsetZ) +
|
|
661
|
-
heightNE * cellOffsetX * (1 - cellOffsetZ) +
|
|
662
|
-
heightSW * (1 - cellOffsetX) * cellOffsetZ +
|
|
663
|
-
heightSE * cellOffsetX * cellOffsetZ;
|
|
664
|
-
const oldHeight = this.data.heightmap[cellZ * res + cellX] * (1 - cellOffsetX) * (1 - cellOffsetZ) +
|
|
665
|
-
(cellX < res - 1
|
|
666
|
-
? this.data.heightmap[cellZ * res + cellX + 1]
|
|
667
|
-
: this.data.heightmap[cellZ * res + cellX]) *
|
|
668
|
-
cellOffsetX *
|
|
669
|
-
(1 - cellOffsetZ) +
|
|
670
|
-
(cellZ < res - 1
|
|
671
|
-
? this.data.heightmap[(cellZ + 1) * res + cellX]
|
|
672
|
-
: this.data.heightmap[cellZ * res + cellX]) *
|
|
673
|
-
(1 - cellOffsetX) *
|
|
674
|
-
cellOffsetZ +
|
|
675
|
-
(cellX < res - 1 && cellZ < res - 1
|
|
676
|
-
? this.data.heightmap[(cellZ + 1) * res + cellX + 1]
|
|
677
|
-
: this.data.heightmap[cellZ * res + cellX]) *
|
|
678
|
-
cellOffsetX *
|
|
679
|
-
cellOffsetZ;
|
|
680
|
-
const heightDiff = newHeight - oldHeight;
|
|
681
|
-
// Calculate sediment capacity
|
|
682
|
-
const sedimentCapacity = Math.max(-heightDiff * speed * water * sedimentCapacityFactor, minSedimentCapacity);
|
|
683
|
-
// Deposit or erode
|
|
684
|
-
if (sediment > sedimentCapacity || heightDiff > 0) {
|
|
685
|
-
// Deposit
|
|
686
|
-
const depositAmount = heightDiff > 0
|
|
687
|
-
? Math.min(heightDiff, sediment)
|
|
688
|
-
: (sediment - sedimentCapacity) * depositSpeed;
|
|
689
|
-
sediment -= depositAmount;
|
|
690
|
-
this.data.heightmap[cellZ * res + cellX] +=
|
|
691
|
-
depositAmount * (1 - cellOffsetX) * (1 - cellOffsetZ);
|
|
692
|
-
if (cellX < res - 1) {
|
|
693
|
-
this.data.heightmap[cellZ * res + cellX + 1] +=
|
|
694
|
-
depositAmount * cellOffsetX * (1 - cellOffsetZ);
|
|
695
|
-
}
|
|
696
|
-
if (cellZ < res - 1) {
|
|
697
|
-
this.data.heightmap[(cellZ + 1) * res + cellX] +=
|
|
698
|
-
depositAmount * (1 - cellOffsetX) * cellOffsetZ;
|
|
699
|
-
}
|
|
700
|
-
if (cellX < res - 1 && cellZ < res - 1) {
|
|
701
|
-
this.data.heightmap[(cellZ + 1) * res + cellX + 1] +=
|
|
702
|
-
depositAmount * cellOffsetX * cellOffsetZ;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
else {
|
|
706
|
-
// Erode
|
|
707
|
-
const erodeAmount = Math.min((sedimentCapacity - sediment) * erodeSpeed, -heightDiff);
|
|
708
|
-
this.data.heightmap[cellZ * res + cellX] -=
|
|
709
|
-
erodeAmount * (1 - cellOffsetX) * (1 - cellOffsetZ);
|
|
710
|
-
if (cellX < res - 1) {
|
|
711
|
-
this.data.heightmap[cellZ * res + cellX + 1] -=
|
|
712
|
-
erodeAmount * cellOffsetX * (1 - cellOffsetZ);
|
|
713
|
-
}
|
|
714
|
-
if (cellZ < res - 1) {
|
|
715
|
-
this.data.heightmap[(cellZ + 1) * res + cellX] -=
|
|
716
|
-
erodeAmount * (1 - cellOffsetX) * cellOffsetZ;
|
|
717
|
-
}
|
|
718
|
-
if (cellX < res - 1 && cellZ < res - 1) {
|
|
719
|
-
this.data.heightmap[(cellZ + 1) * res + cellX + 1] -=
|
|
720
|
-
erodeAmount * cellOffsetX * cellOffsetZ;
|
|
721
|
-
}
|
|
722
|
-
sediment += erodeAmount;
|
|
723
|
-
}
|
|
724
|
-
// Update speed and water
|
|
725
|
-
speed = Math.sqrt(Math.max(0, speed * speed + heightDiff * gravity));
|
|
726
|
-
water *= 1 - evaporateSpeed;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
// Clamp heights
|
|
730
|
-
for (let i = 0; i < this.data.heightmap.length; i++) {
|
|
731
|
-
this.data.heightmap[i] = Math.max(0, Math.min(1, this.data.heightmap[i]));
|
|
732
|
-
}
|
|
733
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
734
|
-
}
|
|
735
|
-
// ─── Perlin Noise ──────────────────────────────────────
|
|
736
|
-
perlinNoise2D(x, y, octaves = 6, persistence = 0.5) {
|
|
737
|
-
let total = 0;
|
|
738
|
-
let frequency = 1;
|
|
739
|
-
let amplitude = 1;
|
|
740
|
-
let maxValue = 0;
|
|
741
|
-
for (let i = 0; i < octaves; i++) {
|
|
742
|
-
total += this.noise2D(x * frequency, y * frequency) * amplitude;
|
|
743
|
-
maxValue += amplitude;
|
|
744
|
-
amplitude *= persistence;
|
|
745
|
-
frequency *= 2;
|
|
746
|
-
}
|
|
747
|
-
return total / maxValue;
|
|
748
|
-
}
|
|
749
|
-
noise2D(x, y) {
|
|
750
|
-
// Simple hash-based noise
|
|
751
|
-
const X = Math.floor(x) & 255;
|
|
752
|
-
const Y = Math.floor(y) & 255;
|
|
753
|
-
const xf = x - Math.floor(x);
|
|
754
|
-
const yf = y - Math.floor(y);
|
|
755
|
-
const topRight = this.dot2D(X + 1, Y, xf - 1, yf);
|
|
756
|
-
const topLeft = this.dot2D(X, Y, xf, yf);
|
|
757
|
-
const bottomRight = this.dot2D(X + 1, Y + 1, xf - 1, yf - 1);
|
|
758
|
-
const bottomLeft = this.dot2D(X, Y + 1, xf, yf - 1);
|
|
759
|
-
const u = this.fade(xf);
|
|
760
|
-
const v = this.fade(yf);
|
|
761
|
-
return this.lerp(this.lerp(topLeft, topRight, u), this.lerp(bottomLeft, bottomRight, u), v);
|
|
762
|
-
}
|
|
763
|
-
dot2D(ix, iy, x, y) {
|
|
764
|
-
// Pseudo-random gradient
|
|
765
|
-
const random = this.hash(ix, iy);
|
|
766
|
-
const angle = random * Math.PI * 2;
|
|
767
|
-
return x * Math.cos(angle) + y * Math.sin(angle);
|
|
768
|
-
}
|
|
769
|
-
hash(x, y) {
|
|
770
|
-
const n = x + y * 57;
|
|
771
|
-
const nn = (n << 13) ^ n;
|
|
772
|
-
return ((nn * (nn * nn * 15731 + 789221) + 1376312589) & 0x7fffffff) / 2147483648.0;
|
|
773
|
-
}
|
|
774
|
-
fade(t) {
|
|
775
|
-
return t * t * t * (t * (t * 6 - 15) + 10);
|
|
776
|
-
}
|
|
777
|
-
lerp(a, b, t) {
|
|
778
|
-
return a + t * (b - a);
|
|
779
|
-
}
|
|
780
|
-
// ─── Foliage ───────────────────────────────────────────
|
|
781
|
-
addDetailPrototype(prototype) {
|
|
782
|
-
this.data.detailPrototypes.push(prototype);
|
|
783
|
-
// Create corresponding density map
|
|
784
|
-
const res = this.data.config.detailResolution;
|
|
785
|
-
this.data.detailMaps.push(new Uint8Array(res * res));
|
|
786
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
787
|
-
}
|
|
788
|
-
paintDetail(worldX, worldZ, prototypeIndex, radius, density) {
|
|
789
|
-
if (prototypeIndex >= this.data.detailMaps.length) {
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
const { width, detailResolution: res } = this.data.config;
|
|
793
|
-
const densityMap = this.data.detailMaps[prototypeIndex];
|
|
794
|
-
const gridX = ((worldX + width / 2) / width) * (res - 1);
|
|
795
|
-
const gridZ = ((worldZ + width / 2) / width) * (res - 1);
|
|
796
|
-
const gridRadius = (radius / width) * res;
|
|
797
|
-
const minX = Math.max(0, Math.floor(gridX - gridRadius));
|
|
798
|
-
const maxX = Math.min(res - 1, Math.ceil(gridX + gridRadius));
|
|
799
|
-
const minZ = Math.max(0, Math.floor(gridZ - gridRadius));
|
|
800
|
-
const maxZ = Math.min(res - 1, Math.ceil(gridZ + gridRadius));
|
|
801
|
-
for (let z = minZ; z <= maxZ; z++) {
|
|
802
|
-
for (let x = minX; x <= maxX; x++) {
|
|
803
|
-
const dx = x - gridX;
|
|
804
|
-
const dz = z - gridZ;
|
|
805
|
-
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
806
|
-
if (dist <= gridRadius) {
|
|
807
|
-
const falloff = 1 - dist / gridRadius;
|
|
808
|
-
const idx = z * res + x;
|
|
809
|
-
const current = densityMap[idx];
|
|
810
|
-
densityMap[idx] = Math.min(255, current + density * falloff * 255);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
815
|
-
}
|
|
816
|
-
scatterFoliage(prototypeIndex, minSlope = 0, maxSlope = 45, minHeight = 0, maxHeight = 1) {
|
|
817
|
-
if (prototypeIndex >= this.data.detailPrototypes.length) {
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const { width, depth, heightmapResolution: _hmRes, detailResolution: res } = this.data.config;
|
|
821
|
-
const densityMap = this.data.detailMaps[prototypeIndex];
|
|
822
|
-
const prototype = this.data.detailPrototypes[prototypeIndex];
|
|
823
|
-
for (let z = 0; z < res; z++) {
|
|
824
|
-
for (let x = 0; x < res; x++) {
|
|
825
|
-
const worldX = (x / (res - 1)) * width - width / 2;
|
|
826
|
-
const worldZ = (z / (res - 1)) * depth - depth / 2;
|
|
827
|
-
// Get terrain properties at this point
|
|
828
|
-
const height = this.getHeightAtWorld(worldX, worldZ) / this.data.config.maxHeight;
|
|
829
|
-
const normal = this.getNormalAtWorld(worldX, worldZ);
|
|
830
|
-
const slope = Math.acos(normal.y) * (180 / Math.PI);
|
|
831
|
-
// Check constraints
|
|
832
|
-
const isValidHeight = height >= minHeight && height <= maxHeight;
|
|
833
|
-
const isValidSlope = slope >= minSlope && slope <= maxSlope;
|
|
834
|
-
if (isValidHeight && isValidSlope) {
|
|
835
|
-
// Apply noise-based density
|
|
836
|
-
const noise = (this.perlinNoise2D(x * 0.1, z * 0.1, 2) + 1) * 0.5;
|
|
837
|
-
const baseDensity = prototype.density * noise;
|
|
838
|
-
densityMap[z * res + x] = Math.min(255, densityMap[z * res + x] + baseDensity * 255);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
843
|
-
}
|
|
844
|
-
// ─── Trees ─────────────────────────────────────────────
|
|
845
|
-
addTreePrototype(prototype) {
|
|
846
|
-
this.data.treePrototypes.push(prototype);
|
|
847
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
848
|
-
}
|
|
849
|
-
addTree(worldX, worldZ, prototypeId, scale, rotation) {
|
|
850
|
-
const prototype = this.data.treePrototypes.find((p) => p.id === prototypeId);
|
|
851
|
-
if (!prototype) {
|
|
852
|
-
return null;
|
|
853
|
-
}
|
|
854
|
-
const height = this.getHeightAtWorld(worldX, worldZ);
|
|
855
|
-
const actualScale = scale ?? prototype.minScale + Math.random() * (prototype.maxScale - prototype.minScale);
|
|
856
|
-
const actualRotation = rotation ?? (prototype.randomRotation ? Math.random() * Math.PI * 2 : 0);
|
|
857
|
-
const instance = {
|
|
858
|
-
position: { x: worldX, y: height, z: worldZ },
|
|
859
|
-
rotation: actualRotation,
|
|
860
|
-
scale: { x: actualScale, y: actualScale, z: actualScale },
|
|
861
|
-
prototypeId,
|
|
862
|
-
color: { r: 1, g: 1, b: 1, a: 1 },
|
|
863
|
-
lightmapColor: { r: 1, g: 1, b: 1, a: 1 },
|
|
864
|
-
};
|
|
865
|
-
this.data.trees.push(instance);
|
|
866
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
867
|
-
return instance;
|
|
868
|
-
}
|
|
869
|
-
scatterTrees(prototypeId, density, minSlope = 0, maxSlope = 30, minHeight = 0, maxHeight = 1, minSpacing = 5) {
|
|
870
|
-
const prototype = this.data.treePrototypes.find((p) => p.id === prototypeId);
|
|
871
|
-
if (!prototype) {
|
|
872
|
-
return 0;
|
|
873
|
-
}
|
|
874
|
-
const { width, depth } = this.data.config;
|
|
875
|
-
let treesAdded = 0;
|
|
876
|
-
// Use Poisson disk sampling for natural distribution
|
|
877
|
-
const cellSize = minSpacing / Math.SQRT2;
|
|
878
|
-
const gridWidth = Math.ceil(width / cellSize);
|
|
879
|
-
const gridDepth = Math.ceil(depth / cellSize);
|
|
880
|
-
const grid = [];
|
|
881
|
-
for (let i = 0; i < gridWidth; i++) {
|
|
882
|
-
grid[i] = [];
|
|
883
|
-
for (let j = 0; j < gridDepth; j++) {
|
|
884
|
-
grid[i][j] = null;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
const numAttempts = Math.floor(width * depth * density);
|
|
888
|
-
const activeList = [];
|
|
889
|
-
// Start with a random point
|
|
890
|
-
const startX = Math.random() * width - width / 2;
|
|
891
|
-
const startZ = Math.random() * depth - depth / 2;
|
|
892
|
-
const startHeight = this.getHeightAtWorld(startX, startZ);
|
|
893
|
-
activeList.push({ x: startX, y: startHeight, z: startZ });
|
|
894
|
-
for (let attempt = 0; attempt < numAttempts && activeList.length > 0; attempt++) {
|
|
895
|
-
const idx = Math.floor(Math.random() * activeList.length);
|
|
896
|
-
const point = activeList[idx];
|
|
897
|
-
let found = false;
|
|
898
|
-
for (let k = 0; k < 30; k++) {
|
|
899
|
-
const angle = Math.random() * Math.PI * 2;
|
|
900
|
-
const r = minSpacing + Math.random() * minSpacing;
|
|
901
|
-
const newX = point.x + Math.cos(angle) * r;
|
|
902
|
-
const newZ = point.z + Math.sin(angle) * r;
|
|
903
|
-
// Check bounds
|
|
904
|
-
if (newX < -width / 2 || newX > width / 2 || newZ < -depth / 2 || newZ > depth / 2) {
|
|
905
|
-
continue;
|
|
906
|
-
}
|
|
907
|
-
// Check terrain constraints
|
|
908
|
-
const newHeight = this.getHeightAtWorld(newX, newZ) / this.data.config.maxHeight;
|
|
909
|
-
const normal = this.getNormalAtWorld(newX, newZ);
|
|
910
|
-
const slope = Math.acos(normal.y) * (180 / Math.PI);
|
|
911
|
-
if (newHeight < minHeight ||
|
|
912
|
-
newHeight > maxHeight ||
|
|
913
|
-
slope < minSlope ||
|
|
914
|
-
slope > maxSlope) {
|
|
915
|
-
continue;
|
|
916
|
-
}
|
|
917
|
-
// Check grid for nearby points
|
|
918
|
-
const gridX = Math.floor((newX + width / 2) / cellSize);
|
|
919
|
-
const gridZ = Math.floor((newZ + depth / 2) / cellSize);
|
|
920
|
-
let tooClose = false;
|
|
921
|
-
for (let di = -2; di <= 2 && !tooClose; di++) {
|
|
922
|
-
for (let dj = -2; dj <= 2 && !tooClose; dj++) {
|
|
923
|
-
const ni = gridX + di;
|
|
924
|
-
const nj = gridZ + dj;
|
|
925
|
-
if (ni >= 0 && ni < gridWidth && nj >= 0 && nj < gridDepth && grid[ni][nj]) {
|
|
926
|
-
const neighbor = grid[ni][nj];
|
|
927
|
-
const dx = neighbor.x - newX;
|
|
928
|
-
const dz = neighbor.z - newZ;
|
|
929
|
-
if (dx * dx + dz * dz < minSpacing * minSpacing) {
|
|
930
|
-
tooClose = true;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
if (!tooClose) {
|
|
936
|
-
const newPoint = { x: newX, y: this.getHeightAtWorld(newX, newZ), z: newZ };
|
|
937
|
-
grid[gridX][gridZ] = newPoint;
|
|
938
|
-
activeList.push(newPoint);
|
|
939
|
-
this.addTree(newX, newZ, prototypeId);
|
|
940
|
-
treesAdded++;
|
|
941
|
-
found = true;
|
|
942
|
-
break;
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
if (!found) {
|
|
946
|
-
activeList.splice(idx, 1);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
return treesAdded;
|
|
950
|
-
}
|
|
951
|
-
removeTreesInRadius(worldX, worldZ, radius) {
|
|
952
|
-
const initialCount = this.data.trees.length;
|
|
953
|
-
this.data.trees = this.data.trees.filter((tree) => {
|
|
954
|
-
const dx = tree.position.x - worldX;
|
|
955
|
-
const dz = tree.position.z - worldZ;
|
|
956
|
-
return dx * dx + dz * dz > radius * radius;
|
|
78
|
+
constructor(config) {
|
|
79
|
+
this.undoStack = [];
|
|
80
|
+
this.redoStack = [];
|
|
81
|
+
this.maxUndoSteps = 50;
|
|
82
|
+
this.data = this.createEmptyTerrain(config);
|
|
83
|
+
}
|
|
84
|
+
createEmptyTerrain(configOverride) {
|
|
85
|
+
const config = { ...DEFAULT_TERRAIN_CONFIG, ...configOverride };
|
|
86
|
+
const hmRes = config.heightmapResolution;
|
|
87
|
+
const smRes = config.splatmapResolution;
|
|
88
|
+
config.detailResolution;
|
|
89
|
+
const chunks = config.chunksPerSide;
|
|
90
|
+
const heightmap = new Float32Array(hmRes * hmRes);
|
|
91
|
+
const splatmaps = [
|
|
92
|
+
new Uint8Array(smRes * smRes * 4),
|
|
93
|
+
new Uint8Array(smRes * smRes * 4)
|
|
94
|
+
];
|
|
95
|
+
for (let i = 0; i < smRes * smRes; i++) {
|
|
96
|
+
splatmaps[0][i * 4] = 255;
|
|
97
|
+
}
|
|
98
|
+
const holemap = new Uint8Array(hmRes * hmRes);
|
|
99
|
+
holemap.fill(1);
|
|
100
|
+
const chunkList = [];
|
|
101
|
+
const chunkWidth = config.width / chunks;
|
|
102
|
+
const chunkDepth = config.depth / chunks;
|
|
103
|
+
for (let gz = 0; gz < chunks; gz++) {
|
|
104
|
+
for (let gx = 0; gx < chunks; gx++) {
|
|
105
|
+
chunkList.push({
|
|
106
|
+
id: `chunk_${gx}_${gz}`,
|
|
107
|
+
gridX: gx,
|
|
108
|
+
gridZ: gz,
|
|
109
|
+
boundsMin: {
|
|
110
|
+
x: gx * chunkWidth - config.width / 2,
|
|
111
|
+
y: 0,
|
|
112
|
+
z: gz * chunkDepth - config.depth / 2
|
|
113
|
+
},
|
|
114
|
+
boundsMax: {
|
|
115
|
+
x: (gx + 1) * chunkWidth - config.width / 2,
|
|
116
|
+
y: config.maxHeight,
|
|
117
|
+
z: (gz + 1) * chunkDepth - config.depth / 2
|
|
118
|
+
},
|
|
119
|
+
isLoaded: gx < 2 && gz < 2,
|
|
120
|
+
// Load center chunks
|
|
121
|
+
lodLevel: 0
|
|
957
122
|
});
|
|
958
|
-
|
|
959
|
-
return initialCount - this.data.trees.length;
|
|
960
|
-
}
|
|
961
|
-
// ─── Undo/Redo ─────────────────────────────────────────
|
|
962
|
-
recordHeightChange(gridX, gridZ, oldValue, newValue) {
|
|
963
|
-
const operation = {
|
|
964
|
-
type: 'height',
|
|
965
|
-
gridX,
|
|
966
|
-
gridZ,
|
|
967
|
-
oldValue,
|
|
968
|
-
newValue,
|
|
969
|
-
timestamp: Date.now(),
|
|
970
|
-
};
|
|
971
|
-
this.undoStack.push(operation);
|
|
972
|
-
if (this.undoStack.length > this.maxUndoSteps) {
|
|
973
|
-
this.undoStack.shift();
|
|
974
|
-
}
|
|
975
|
-
this.redoStack = [];
|
|
976
|
-
}
|
|
977
|
-
undo() {
|
|
978
|
-
const operation = this.undoStack.pop();
|
|
979
|
-
if (!operation) {
|
|
980
|
-
return false;
|
|
981
|
-
}
|
|
982
|
-
if (operation.type === 'height') {
|
|
983
|
-
const res = this.data.config.heightmapResolution;
|
|
984
|
-
this.data.heightmap[operation.gridZ * res + operation.gridX] = operation.oldValue;
|
|
985
|
-
}
|
|
986
|
-
this.redoStack.push(operation);
|
|
987
|
-
this.data.metadata.modifiedAt = Date.now();
|
|
988
|
-
return true;
|
|
123
|
+
}
|
|
989
124
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
125
|
+
return {
|
|
126
|
+
id: `terrain_${Date.now()}`,
|
|
127
|
+
name: "New Terrain",
|
|
128
|
+
config,
|
|
129
|
+
heightmap,
|
|
130
|
+
splatmaps,
|
|
131
|
+
holemap,
|
|
132
|
+
layers: [],
|
|
133
|
+
detailPrototypes: [],
|
|
134
|
+
detailMaps: [],
|
|
135
|
+
treePrototypes: [],
|
|
136
|
+
trees: [],
|
|
137
|
+
chunks: chunkList,
|
|
138
|
+
metadata: {
|
|
139
|
+
createdAt: Date.now(),
|
|
140
|
+
modifiedAt: Date.now(),
|
|
141
|
+
version: 1
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// ─── Heightmap Operations ──────────────────────────────
|
|
146
|
+
/** Get height at grid coordinates (0 to resolution-1) */
|
|
147
|
+
getHeightAtGrid(gridX, gridZ) {
|
|
148
|
+
const res = this.data.config.heightmapResolution;
|
|
149
|
+
const x = Math.max(0, Math.min(res - 1, Math.floor(gridX)));
|
|
150
|
+
const z = Math.max(0, Math.min(res - 1, Math.floor(gridZ)));
|
|
151
|
+
return this.data.heightmap[z * res + x] * this.data.config.maxHeight;
|
|
152
|
+
}
|
|
153
|
+
/** Set height at grid coordinates */
|
|
154
|
+
setHeightAtGrid(gridX, gridZ, height, recordUndo = true) {
|
|
155
|
+
const res = this.data.config.heightmapResolution;
|
|
156
|
+
const maxH = this.data.config.maxHeight;
|
|
157
|
+
if (gridX < 0 || gridX >= res || gridZ < 0 || gridZ >= res) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const idx = gridZ * res + gridX;
|
|
161
|
+
const normalizedHeight = Math.max(0, Math.min(1, height / maxH));
|
|
162
|
+
if (recordUndo) {
|
|
163
|
+
this.recordHeightChange(gridX, gridZ, this.data.heightmap[idx], normalizedHeight);
|
|
164
|
+
}
|
|
165
|
+
this.data.heightmap[idx] = normalizedHeight;
|
|
166
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
167
|
+
}
|
|
168
|
+
/** Get interpolated height at world coordinates */
|
|
169
|
+
getHeightAtWorld(worldX, worldZ) {
|
|
170
|
+
const { width, depth, heightmapResolution: res } = this.data.config;
|
|
171
|
+
const gx = (worldX + width / 2) / width * (res - 1);
|
|
172
|
+
const gz = (worldZ + depth / 2) / depth * (res - 1);
|
|
173
|
+
const ix = Math.floor(gx);
|
|
174
|
+
const iz = Math.floor(gz);
|
|
175
|
+
const fx = gx - ix;
|
|
176
|
+
const fz = gz - iz;
|
|
177
|
+
const h00 = this.getHeightAtGrid(ix, iz);
|
|
178
|
+
const h10 = this.getHeightAtGrid(ix + 1, iz);
|
|
179
|
+
const h01 = this.getHeightAtGrid(ix, iz + 1);
|
|
180
|
+
const h11 = this.getHeightAtGrid(ix + 1, iz + 1);
|
|
181
|
+
const h0 = h00 + (h10 - h00) * fx;
|
|
182
|
+
const h1 = h01 + (h11 - h01) * fx;
|
|
183
|
+
return h0 + (h1 - h0) * fz;
|
|
184
|
+
}
|
|
185
|
+
/** Convert world position to grid coordinates */
|
|
186
|
+
worldToGrid(worldX, worldZ) {
|
|
187
|
+
const { width, depth, heightmapResolution: res } = this.data.config;
|
|
188
|
+
return {
|
|
189
|
+
x: (worldX + width / 2) / width * (res - 1),
|
|
190
|
+
y: (worldZ + depth / 2) / depth * (res - 1)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** Convert grid coordinates to world position */
|
|
194
|
+
gridToWorld(gridX, gridZ) {
|
|
195
|
+
const { width, depth, heightmapResolution: res } = this.data.config;
|
|
196
|
+
const worldX = gridX / (res - 1) * width - width / 2;
|
|
197
|
+
const worldZ = gridZ / (res - 1) * depth - depth / 2;
|
|
198
|
+
const worldY = this.getHeightAtGrid(gridX, gridZ);
|
|
199
|
+
return { x: worldX, y: worldY, z: worldZ };
|
|
200
|
+
}
|
|
201
|
+
/// Get terrain normal at world position
|
|
202
|
+
getNormalAtWorld(worldX, worldZ) {
|
|
203
|
+
const { width, depth, heightmapResolution: res } = this.data.config;
|
|
204
|
+
const cellWidth = width / (res - 1);
|
|
205
|
+
const cellDepth = depth / (res - 1);
|
|
206
|
+
const hL = this.getHeightAtWorld(worldX - cellWidth, worldZ);
|
|
207
|
+
const hR = this.getHeightAtWorld(worldX + cellWidth, worldZ);
|
|
208
|
+
const hD = this.getHeightAtWorld(worldX, worldZ - cellDepth);
|
|
209
|
+
const hU = this.getHeightAtWorld(worldX, worldZ + cellDepth);
|
|
210
|
+
const dx = hR - hL;
|
|
211
|
+
const dz = hU - hD;
|
|
212
|
+
const nx = -dx / (2 * cellWidth);
|
|
213
|
+
const ny = 1;
|
|
214
|
+
const nz = -dz / (2 * cellDepth);
|
|
215
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
216
|
+
return { x: nx / len, y: ny / len, z: nz / len };
|
|
217
|
+
}
|
|
218
|
+
// ─── Brush Operations ──────────────────────────────────
|
|
219
|
+
applyBrush(brush, worldX, worldZ) {
|
|
220
|
+
switch (brush.type) {
|
|
221
|
+
case "raise":
|
|
222
|
+
this.applyHeightBrush(worldX, worldZ, brush, 1);
|
|
223
|
+
break;
|
|
224
|
+
case "lower":
|
|
225
|
+
this.applyHeightBrush(worldX, worldZ, brush, -1);
|
|
226
|
+
break;
|
|
227
|
+
case "flatten":
|
|
228
|
+
this.applyFlattenBrush(worldX, worldZ, brush);
|
|
229
|
+
break;
|
|
230
|
+
case "smooth":
|
|
231
|
+
this.applySmoothBrush(worldX, worldZ, brush);
|
|
232
|
+
break;
|
|
233
|
+
case "noise":
|
|
234
|
+
this.applyNoiseBrush(worldX, worldZ, brush);
|
|
235
|
+
break;
|
|
236
|
+
case "erosion":
|
|
237
|
+
this.applyErosionBrush(worldX, worldZ, brush);
|
|
238
|
+
break;
|
|
239
|
+
case "paint":
|
|
240
|
+
this.applyPaintBrush(worldX, worldZ, brush);
|
|
241
|
+
break;
|
|
242
|
+
case "paintHoles":
|
|
243
|
+
this.applyHoleBrush(worldX, worldZ, brush);
|
|
244
|
+
break;
|
|
245
|
+
case "stampHeight":
|
|
246
|
+
this.applyStampBrush(worldX, worldZ, brush);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
applyHeightBrush(worldX, worldZ, brush, direction) {
|
|
251
|
+
const grid = this.worldToGrid(worldX, worldZ);
|
|
252
|
+
const { heightmapResolution: res, width, maxHeight } = this.data.config;
|
|
253
|
+
const gridRadius = brush.size / (width / (res - 1)) * 0.5;
|
|
254
|
+
const delta = brush.strength * direction * maxHeight / 100;
|
|
255
|
+
const minX = Math.max(0, Math.floor(grid.x - gridRadius));
|
|
256
|
+
const maxX = Math.min(res - 1, Math.ceil(grid.x + gridRadius));
|
|
257
|
+
const minZ = Math.max(0, Math.floor(grid.y - gridRadius));
|
|
258
|
+
const maxZ = Math.min(res - 1, Math.ceil(grid.y + gridRadius));
|
|
259
|
+
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
260
|
+
for (let gx = minX; gx <= maxX; gx++) {
|
|
261
|
+
const dx = gx - grid.x;
|
|
262
|
+
const dz = gz - grid.y;
|
|
263
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
264
|
+
if (dist <= gridRadius) {
|
|
265
|
+
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
266
|
+
const currentHeight = this.getHeightAtGrid(gx, gz);
|
|
267
|
+
const newHeight = currentHeight + delta * falloff;
|
|
268
|
+
this.setHeightAtGrid(gx, gz, newHeight);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
applyFlattenBrush(worldX, worldZ, brush) {
|
|
274
|
+
const grid = this.worldToGrid(worldX, worldZ);
|
|
275
|
+
const { heightmapResolution: res, width } = this.data.config;
|
|
276
|
+
const gridRadius = brush.size / (width / (res - 1)) * 0.5;
|
|
277
|
+
const targetHeight = brush.targetHeight ?? this.getHeightAtWorld(worldX, worldZ);
|
|
278
|
+
const minX = Math.max(0, Math.floor(grid.x - gridRadius));
|
|
279
|
+
const maxX = Math.min(res - 1, Math.ceil(grid.x + gridRadius));
|
|
280
|
+
const minZ = Math.max(0, Math.floor(grid.y - gridRadius));
|
|
281
|
+
const maxZ = Math.min(res - 1, Math.ceil(grid.y + gridRadius));
|
|
282
|
+
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
283
|
+
for (let gx = minX; gx <= maxX; gx++) {
|
|
284
|
+
const dx = gx - grid.x;
|
|
285
|
+
const dz = gz - grid.y;
|
|
286
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
287
|
+
if (dist <= gridRadius) {
|
|
288
|
+
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
289
|
+
const currentHeight = this.getHeightAtGrid(gx, gz);
|
|
290
|
+
const newHeight = currentHeight + (targetHeight - currentHeight) * falloff * brush.strength;
|
|
291
|
+
this.setHeightAtGrid(gx, gz, newHeight);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
applySmoothBrush(worldX, worldZ, brush) {
|
|
297
|
+
const grid = this.worldToGrid(worldX, worldZ);
|
|
298
|
+
const { heightmapResolution: res, width } = this.data.config;
|
|
299
|
+
const gridRadius = brush.size / (width / (res - 1)) * 0.5;
|
|
300
|
+
const minX = Math.max(0, Math.floor(grid.x - gridRadius));
|
|
301
|
+
const maxX = Math.min(res - 1, Math.ceil(grid.x + gridRadius));
|
|
302
|
+
const minZ = Math.max(0, Math.floor(grid.y - gridRadius));
|
|
303
|
+
const maxZ = Math.min(res - 1, Math.ceil(grid.y + gridRadius));
|
|
304
|
+
const newHeights = [];
|
|
305
|
+
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
306
|
+
for (let gx = minX; gx <= maxX; gx++) {
|
|
307
|
+
const dx = gx - grid.x;
|
|
308
|
+
const dz = gz - grid.y;
|
|
309
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
310
|
+
if (dist <= gridRadius) {
|
|
311
|
+
let sum = 0;
|
|
312
|
+
let count = 0;
|
|
313
|
+
for (let nz = -1; nz <= 1; nz++) {
|
|
314
|
+
for (let nx = -1; nx <= 1; nx++) {
|
|
315
|
+
const ngx = gx + nx;
|
|
316
|
+
const ngz = gz + nz;
|
|
317
|
+
if (ngx >= 0 && ngx < res && ngz >= 0 && ngz < res) {
|
|
318
|
+
sum += this.getHeightAtGrid(ngx, ngz);
|
|
319
|
+
count++;
|
|
320
|
+
}
|
|
1053
321
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
322
|
+
}
|
|
323
|
+
const avgHeight = sum / count;
|
|
324
|
+
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
325
|
+
const currentHeight = this.getHeightAtGrid(gx, gz);
|
|
326
|
+
const newHeight = currentHeight + (avgHeight - currentHeight) * falloff * brush.strength;
|
|
327
|
+
newHeights.push({ gx, gz, height: newHeight });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const { gx, gz, height } of newHeights) {
|
|
332
|
+
this.setHeightAtGrid(gx, gz, height);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
applyNoiseBrush(worldX, worldZ, brush) {
|
|
336
|
+
const grid = this.worldToGrid(worldX, worldZ);
|
|
337
|
+
const { heightmapResolution: res, width, maxHeight } = this.data.config;
|
|
338
|
+
const gridRadius = brush.size / (width / (res - 1)) * 0.5;
|
|
339
|
+
const noiseScale = brush.noiseScale ?? 0.1;
|
|
340
|
+
const octaves = brush.noiseOctaves ?? 4;
|
|
341
|
+
const minX = Math.max(0, Math.floor(grid.x - gridRadius));
|
|
342
|
+
const maxX = Math.min(res - 1, Math.ceil(grid.x + gridRadius));
|
|
343
|
+
const minZ = Math.max(0, Math.floor(grid.y - gridRadius));
|
|
344
|
+
const maxZ = Math.min(res - 1, Math.ceil(grid.y + gridRadius));
|
|
345
|
+
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
346
|
+
for (let gx = minX; gx <= maxX; gx++) {
|
|
347
|
+
const dx = gx - grid.x;
|
|
348
|
+
const dz = gz - grid.y;
|
|
349
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
350
|
+
if (dist <= gridRadius) {
|
|
351
|
+
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
352
|
+
const noise = this.perlinNoise2D(gx * noiseScale, gz * noiseScale, octaves);
|
|
353
|
+
const delta = noise * brush.strength * maxHeight * 0.1 * falloff;
|
|
354
|
+
const currentHeight = this.getHeightAtGrid(gx, gz);
|
|
355
|
+
this.setHeightAtGrid(gx, gz, currentHeight + delta);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
applyErosionBrush(worldX, worldZ, brush) {
|
|
361
|
+
const grid = this.worldToGrid(worldX, worldZ);
|
|
362
|
+
const { heightmapResolution: res, width } = this.data.config;
|
|
363
|
+
const gridRadius = brush.size / (width / (res - 1)) * 0.5;
|
|
364
|
+
const minX = Math.max(1, Math.floor(grid.x - gridRadius));
|
|
365
|
+
const maxX = Math.min(res - 2, Math.ceil(grid.x + gridRadius));
|
|
366
|
+
const minZ = Math.max(1, Math.floor(grid.y - gridRadius));
|
|
367
|
+
const maxZ = Math.min(res - 2, Math.ceil(grid.y + gridRadius));
|
|
368
|
+
const newHeights = [];
|
|
369
|
+
const talusAngle = 0.5;
|
|
370
|
+
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
371
|
+
for (let gx = minX; gx <= maxX; gx++) {
|
|
372
|
+
const dx = gx - grid.x;
|
|
373
|
+
const dz = gz - grid.y;
|
|
374
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
375
|
+
if (dist <= gridRadius) {
|
|
376
|
+
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
377
|
+
const h = this.getHeightAtGrid(gx, gz);
|
|
378
|
+
let totalDiff = 0;
|
|
379
|
+
const neighbors = [
|
|
380
|
+
this.getHeightAtGrid(gx - 1, gz),
|
|
381
|
+
this.getHeightAtGrid(gx + 1, gz),
|
|
382
|
+
this.getHeightAtGrid(gx, gz - 1),
|
|
383
|
+
this.getHeightAtGrid(gx, gz + 1)
|
|
384
|
+
];
|
|
385
|
+
for (const nh of neighbors) {
|
|
386
|
+
const diff = h - nh;
|
|
387
|
+
if (diff > talusAngle) {
|
|
388
|
+
totalDiff += diff - talusAngle;
|
|
1060
389
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
390
|
+
}
|
|
391
|
+
if (totalDiff > 0) {
|
|
392
|
+
const erosion = totalDiff * brush.strength * falloff * 0.25;
|
|
393
|
+
newHeights.push({ gx, gz, height: h - erosion });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const { gx, gz, height } of newHeights) {
|
|
399
|
+
this.setHeightAtGrid(gx, gz, height);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
applyPaintBrush(worldX, worldZ, brush) {
|
|
403
|
+
const { splatmapResolution: res, width } = this.data.config;
|
|
404
|
+
const layerIndex = brush.layerIndex ?? 0;
|
|
405
|
+
if (layerIndex >= 8) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const splatmapIndex = Math.floor(layerIndex / 4);
|
|
409
|
+
const channelIndex = layerIndex % 4;
|
|
410
|
+
const smX = (worldX + width / 2) / width * (res - 1);
|
|
411
|
+
const smZ = (worldZ + width / 2) / width * (res - 1);
|
|
412
|
+
const gridRadius = brush.size / (width / (res - 1)) * 0.5;
|
|
413
|
+
const minX = Math.max(0, Math.floor(smX - gridRadius));
|
|
414
|
+
const maxX = Math.min(res - 1, Math.ceil(smX + gridRadius));
|
|
415
|
+
const minZ = Math.max(0, Math.floor(smZ - gridRadius));
|
|
416
|
+
const maxZ = Math.min(res - 1, Math.ceil(smZ + gridRadius));
|
|
417
|
+
const splatmap = this.data.splatmaps[splatmapIndex];
|
|
418
|
+
for (let z = minZ; z <= maxZ; z++) {
|
|
419
|
+
for (let x = minX; x <= maxX; x++) {
|
|
420
|
+
const dx = x - smX;
|
|
421
|
+
const dz = z - smZ;
|
|
422
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
423
|
+
if (dist <= gridRadius) {
|
|
424
|
+
const falloff = this.calculateFalloff(dist / gridRadius, brush.falloff);
|
|
425
|
+
const idx = (z * res + x) * 4;
|
|
426
|
+
const strength = brush.strength * falloff * 255;
|
|
427
|
+
const current = splatmap[idx + channelIndex];
|
|
428
|
+
const newValue = Math.min(255, current + strength);
|
|
429
|
+
splatmap[idx + channelIndex] = newValue;
|
|
430
|
+
this.normalizeSplatmapPixel(x, z);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
435
|
+
}
|
|
436
|
+
normalizeSplatmapPixel(x, z) {
|
|
437
|
+
const res = this.data.config.splatmapResolution;
|
|
438
|
+
let total = 0;
|
|
439
|
+
for (let s = 0; s < 2; s++) {
|
|
440
|
+
const idx = (z * res + x) * 4;
|
|
441
|
+
for (let c = 0; c < 4; c++) {
|
|
442
|
+
total += this.data.splatmaps[s][idx + c];
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (total > 0) {
|
|
446
|
+
const scale = 255 / total;
|
|
447
|
+
for (let s = 0; s < 2; s++) {
|
|
448
|
+
const idx = (z * res + x) * 4;
|
|
449
|
+
for (let c = 0; c < 4; c++) {
|
|
450
|
+
this.data.splatmaps[s][idx + c] = Math.round(this.data.splatmaps[s][idx + c] * scale);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
applyHoleBrush(worldX, worldZ, brush) {
|
|
456
|
+
const { heightmapResolution: res, width } = this.data.config;
|
|
457
|
+
const gridCoords = this.worldToGrid(worldX, worldZ);
|
|
458
|
+
const gridRadius = brush.size / (width / (res - 1)) * 0.5;
|
|
459
|
+
const isAddHole = brush.strength > 0;
|
|
460
|
+
const minX = Math.max(0, Math.floor(gridCoords.x - gridRadius));
|
|
461
|
+
const maxX = Math.min(res - 1, Math.ceil(gridCoords.x + gridRadius));
|
|
462
|
+
const minZ = Math.max(0, Math.floor(gridCoords.y - gridRadius));
|
|
463
|
+
const maxZ = Math.min(res - 1, Math.ceil(gridCoords.y + gridRadius));
|
|
464
|
+
for (let gz = minZ; gz <= maxZ; gz++) {
|
|
465
|
+
for (let gx = minX; gx <= maxX; gx++) {
|
|
466
|
+
const dx = gx - gridCoords.x;
|
|
467
|
+
const dz = gz - gridCoords.y;
|
|
468
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
469
|
+
if (dist <= gridRadius) {
|
|
470
|
+
this.data.holemap[gz * res + gx] = isAddHole ? 0 : 1;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
475
|
+
}
|
|
476
|
+
applyStampBrush(worldX, worldZ, brush) {
|
|
477
|
+
this.applyHeightBrush(worldX, worldZ, brush, 1);
|
|
478
|
+
}
|
|
479
|
+
calculateFalloff(t, falloffStrength) {
|
|
480
|
+
if (t >= 1) {
|
|
481
|
+
return 0;
|
|
482
|
+
}
|
|
483
|
+
if (falloffStrength <= 0) {
|
|
484
|
+
return 1;
|
|
485
|
+
}
|
|
486
|
+
const smoothT = t * t * (3 - 2 * t);
|
|
487
|
+
const power = 1 + falloffStrength * 3;
|
|
488
|
+
return Math.pow(1 - smoothT, power);
|
|
489
|
+
}
|
|
490
|
+
// ─── Procedural Generation ─────────────────────────────
|
|
491
|
+
generatePerlinTerrain(scale = 0.01, octaves = 6, persistence = 0.5) {
|
|
492
|
+
const res = this.data.config.heightmapResolution;
|
|
493
|
+
for (let z = 0; z < res; z++) {
|
|
494
|
+
for (let x = 0; x < res; x++) {
|
|
495
|
+
const height = this.perlinNoise2D(x * scale, z * scale, octaves, persistence);
|
|
496
|
+
this.data.heightmap[z * res + x] = (height + 1) * 0.5;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
500
|
+
}
|
|
501
|
+
generateDiamondSquare(roughness = 0.5) {
|
|
502
|
+
const size = this.data.config.heightmapResolution;
|
|
503
|
+
this.data.heightmap[0] = Math.random();
|
|
504
|
+
this.data.heightmap[size - 1] = Math.random();
|
|
505
|
+
this.data.heightmap[(size - 1) * size] = Math.random();
|
|
506
|
+
this.data.heightmap[(size - 1) * size + size - 1] = Math.random();
|
|
507
|
+
let stepSize = size - 1;
|
|
508
|
+
let scale = roughness;
|
|
509
|
+
while (stepSize > 1) {
|
|
510
|
+
const halfStep = stepSize / 2;
|
|
511
|
+
for (let z = halfStep; z < size - 1; z += stepSize) {
|
|
512
|
+
for (let x = halfStep; x < size - 1; x += stepSize) {
|
|
513
|
+
const avg = (this.data.heightmap[(z - halfStep) * size + (x - halfStep)] + this.data.heightmap[(z - halfStep) * size + (x + halfStep)] + this.data.heightmap[(z + halfStep) * size + (x - halfStep)] + this.data.heightmap[(z + halfStep) * size + (x + halfStep)]) / 4;
|
|
514
|
+
this.data.heightmap[z * size + x] = avg + (Math.random() - 0.5) * scale;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
for (let z = 0; z < size; z += halfStep) {
|
|
518
|
+
for (let x = (z + halfStep) % stepSize; x < size; x += stepSize) {
|
|
519
|
+
let sum = 0;
|
|
520
|
+
let count = 0;
|
|
521
|
+
if (z - halfStep >= 0) {
|
|
522
|
+
sum += this.data.heightmap[(z - halfStep) * size + x];
|
|
523
|
+
count++;
|
|
524
|
+
}
|
|
525
|
+
if (z + halfStep < size) {
|
|
526
|
+
sum += this.data.heightmap[(z + halfStep) * size + x];
|
|
527
|
+
count++;
|
|
528
|
+
}
|
|
529
|
+
if (x - halfStep >= 0) {
|
|
530
|
+
sum += this.data.heightmap[z * size + (x - halfStep)];
|
|
531
|
+
count++;
|
|
532
|
+
}
|
|
533
|
+
if (x + halfStep < size) {
|
|
534
|
+
sum += this.data.heightmap[z * size + (x + halfStep)];
|
|
535
|
+
count++;
|
|
536
|
+
}
|
|
537
|
+
this.data.heightmap[z * size + x] = sum / count + (Math.random() - 0.5) * scale;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
stepSize = halfStep;
|
|
541
|
+
scale *= 0.5;
|
|
542
|
+
}
|
|
543
|
+
let min = Infinity;
|
|
544
|
+
let max = -Infinity;
|
|
545
|
+
for (let i = 0; i < this.data.heightmap.length; i++) {
|
|
546
|
+
min = Math.min(min, this.data.heightmap[i]);
|
|
547
|
+
max = Math.max(max, this.data.heightmap[i]);
|
|
548
|
+
}
|
|
549
|
+
const range = max - min;
|
|
550
|
+
if (range > 0) {
|
|
551
|
+
for (let i = 0; i < this.data.heightmap.length; i++) {
|
|
552
|
+
this.data.heightmap[i] = (this.data.heightmap[i] - min) / range;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
556
|
+
}
|
|
557
|
+
applyHydraulicErosion(iterations = 5e4) {
|
|
558
|
+
const res = this.data.config.heightmapResolution;
|
|
559
|
+
const { maxHeight: _maxHeight } = this.data.config;
|
|
560
|
+
const inertia = 0.05;
|
|
561
|
+
const sedimentCapacityFactor = 4;
|
|
562
|
+
const minSedimentCapacity = 0.01;
|
|
563
|
+
const erodeSpeed = 0.3;
|
|
564
|
+
const depositSpeed = 0.3;
|
|
565
|
+
const evaporateSpeed = 0.01;
|
|
566
|
+
const gravity = 4;
|
|
567
|
+
const maxDropletLifetime = 30;
|
|
568
|
+
const initialWaterVolume = 1;
|
|
569
|
+
const initialSpeed = 1;
|
|
570
|
+
for (let i = 0; i < iterations; i++) {
|
|
571
|
+
let posX = Math.random() * (res - 1);
|
|
572
|
+
let posZ = Math.random() * (res - 1);
|
|
573
|
+
let dirX = 0;
|
|
574
|
+
let dirZ = 0;
|
|
575
|
+
let speed = initialSpeed;
|
|
576
|
+
let water = initialWaterVolume;
|
|
577
|
+
let sediment = 0;
|
|
578
|
+
for (let lifetime = 0; lifetime < maxDropletLifetime; lifetime++) {
|
|
579
|
+
const cellX = Math.floor(posX);
|
|
580
|
+
const cellZ = Math.floor(posZ);
|
|
581
|
+
const cellOffsetX = posX - cellX;
|
|
582
|
+
const cellOffsetZ = posZ - cellZ;
|
|
583
|
+
const heightNW = this.data.heightmap[cellZ * res + cellX];
|
|
584
|
+
const heightNE = cellX < res - 1 ? this.data.heightmap[cellZ * res + cellX + 1] : heightNW;
|
|
585
|
+
const heightSW = cellZ < res - 1 ? this.data.heightmap[(cellZ + 1) * res + cellX] : heightNW;
|
|
586
|
+
const heightSE = cellX < res - 1 && cellZ < res - 1 ? this.data.heightmap[(cellZ + 1) * res + cellX + 1] : heightNW;
|
|
587
|
+
const gradientX = (heightNE - heightNW) * (1 - cellOffsetZ) + (heightSE - heightSW) * cellOffsetZ;
|
|
588
|
+
const gradientZ = (heightSW - heightNW) * (1 - cellOffsetX) + (heightSE - heightNE) * cellOffsetX;
|
|
589
|
+
dirX = dirX * inertia - gradientX * (1 - inertia);
|
|
590
|
+
dirZ = dirZ * inertia - gradientZ * (1 - inertia);
|
|
591
|
+
const len = Math.sqrt(dirX * dirX + dirZ * dirZ);
|
|
592
|
+
if (len > 0) {
|
|
593
|
+
dirX /= len;
|
|
594
|
+
dirZ /= len;
|
|
595
|
+
}
|
|
596
|
+
posX += dirX;
|
|
597
|
+
posZ += dirZ;
|
|
598
|
+
if (posX < 0 || posX >= res - 1 || posZ < 0 || posZ >= res - 1 || len === 0) {
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
const newHeight = heightNW * (1 - cellOffsetX) * (1 - cellOffsetZ) + heightNE * cellOffsetX * (1 - cellOffsetZ) + heightSW * (1 - cellOffsetX) * cellOffsetZ + heightSE * cellOffsetX * cellOffsetZ;
|
|
602
|
+
const oldHeight = this.data.heightmap[cellZ * res + cellX] * (1 - cellOffsetX) * (1 - cellOffsetZ) + (cellX < res - 1 ? this.data.heightmap[cellZ * res + cellX + 1] : this.data.heightmap[cellZ * res + cellX]) * cellOffsetX * (1 - cellOffsetZ) + (cellZ < res - 1 ? this.data.heightmap[(cellZ + 1) * res + cellX] : this.data.heightmap[cellZ * res + cellX]) * (1 - cellOffsetX) * cellOffsetZ + (cellX < res - 1 && cellZ < res - 1 ? this.data.heightmap[(cellZ + 1) * res + cellX + 1] : this.data.heightmap[cellZ * res + cellX]) * cellOffsetX * cellOffsetZ;
|
|
603
|
+
const heightDiff = newHeight - oldHeight;
|
|
604
|
+
const sedimentCapacity = Math.max(
|
|
605
|
+
-heightDiff * speed * water * sedimentCapacityFactor,
|
|
606
|
+
minSedimentCapacity
|
|
607
|
+
);
|
|
608
|
+
if (sediment > sedimentCapacity || heightDiff > 0) {
|
|
609
|
+
const depositAmount = heightDiff > 0 ? Math.min(heightDiff, sediment) : (sediment - sedimentCapacity) * depositSpeed;
|
|
610
|
+
sediment -= depositAmount;
|
|
611
|
+
this.data.heightmap[cellZ * res + cellX] += depositAmount * (1 - cellOffsetX) * (1 - cellOffsetZ);
|
|
612
|
+
if (cellX < res - 1) {
|
|
613
|
+
this.data.heightmap[cellZ * res + cellX + 1] += depositAmount * cellOffsetX * (1 - cellOffsetZ);
|
|
614
|
+
}
|
|
615
|
+
if (cellZ < res - 1) {
|
|
616
|
+
this.data.heightmap[(cellZ + 1) * res + cellX] += depositAmount * (1 - cellOffsetX) * cellOffsetZ;
|
|
617
|
+
}
|
|
618
|
+
if (cellX < res - 1 && cellZ < res - 1) {
|
|
619
|
+
this.data.heightmap[(cellZ + 1) * res + cellX + 1] += depositAmount * cellOffsetX * cellOffsetZ;
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
const erodeAmount = Math.min((sedimentCapacity - sediment) * erodeSpeed, -heightDiff);
|
|
623
|
+
this.data.heightmap[cellZ * res + cellX] -= erodeAmount * (1 - cellOffsetX) * (1 - cellOffsetZ);
|
|
624
|
+
if (cellX < res - 1) {
|
|
625
|
+
this.data.heightmap[cellZ * res + cellX + 1] -= erodeAmount * cellOffsetX * (1 - cellOffsetZ);
|
|
626
|
+
}
|
|
627
|
+
if (cellZ < res - 1) {
|
|
628
|
+
this.data.heightmap[(cellZ + 1) * res + cellX] -= erodeAmount * (1 - cellOffsetX) * cellOffsetZ;
|
|
629
|
+
}
|
|
630
|
+
if (cellX < res - 1 && cellZ < res - 1) {
|
|
631
|
+
this.data.heightmap[(cellZ + 1) * res + cellX + 1] -= erodeAmount * cellOffsetX * cellOffsetZ;
|
|
632
|
+
}
|
|
633
|
+
sediment += erodeAmount;
|
|
634
|
+
}
|
|
635
|
+
speed = Math.sqrt(Math.max(0, speed * speed + heightDiff * gravity));
|
|
636
|
+
water *= 1 - evaporateSpeed;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
for (let i = 0; i < this.data.heightmap.length; i++) {
|
|
640
|
+
this.data.heightmap[i] = Math.max(0, Math.min(1, this.data.heightmap[i]));
|
|
641
|
+
}
|
|
642
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
643
|
+
}
|
|
644
|
+
// ─── Perlin Noise ──────────────────────────────────────
|
|
645
|
+
perlinNoise2D(x, y, octaves = 6, persistence = 0.5) {
|
|
646
|
+
let total = 0;
|
|
647
|
+
let frequency = 1;
|
|
648
|
+
let amplitude = 1;
|
|
649
|
+
let maxValue = 0;
|
|
650
|
+
for (let i = 0; i < octaves; i++) {
|
|
651
|
+
total += this.noise2D(x * frequency, y * frequency) * amplitude;
|
|
652
|
+
maxValue += amplitude;
|
|
653
|
+
amplitude *= persistence;
|
|
654
|
+
frequency *= 2;
|
|
655
|
+
}
|
|
656
|
+
return total / maxValue;
|
|
657
|
+
}
|
|
658
|
+
noise2D(x, y) {
|
|
659
|
+
const X = Math.floor(x) & 255;
|
|
660
|
+
const Y = Math.floor(y) & 255;
|
|
661
|
+
const xf = x - Math.floor(x);
|
|
662
|
+
const yf = y - Math.floor(y);
|
|
663
|
+
const topRight = this.dot2D(X + 1, Y, xf - 1, yf);
|
|
664
|
+
const topLeft = this.dot2D(X, Y, xf, yf);
|
|
665
|
+
const bottomRight = this.dot2D(X + 1, Y + 1, xf - 1, yf - 1);
|
|
666
|
+
const bottomLeft = this.dot2D(X, Y + 1, xf, yf - 1);
|
|
667
|
+
const u = this.fade(xf);
|
|
668
|
+
const v = this.fade(yf);
|
|
669
|
+
return this.lerp(this.lerp(topLeft, topRight, u), this.lerp(bottomLeft, bottomRight, u), v);
|
|
670
|
+
}
|
|
671
|
+
dot2D(ix, iy, x, y) {
|
|
672
|
+
const random = this.hash(ix, iy);
|
|
673
|
+
const angle = random * Math.PI * 2;
|
|
674
|
+
return x * Math.cos(angle) + y * Math.sin(angle);
|
|
675
|
+
}
|
|
676
|
+
hash(x, y) {
|
|
677
|
+
const n = x + y * 57;
|
|
678
|
+
const nn = n << 13 ^ n;
|
|
679
|
+
return (nn * (nn * nn * 15731 + 789221) + 1376312589 & 2147483647) / 2147483648;
|
|
680
|
+
}
|
|
681
|
+
fade(t) {
|
|
682
|
+
return t * t * t * (t * (t * 6 - 15) + 10);
|
|
683
|
+
}
|
|
684
|
+
lerp(a, b, t) {
|
|
685
|
+
return a + t * (b - a);
|
|
686
|
+
}
|
|
687
|
+
// ─── Foliage ───────────────────────────────────────────
|
|
688
|
+
addDetailPrototype(prototype) {
|
|
689
|
+
this.data.detailPrototypes.push(prototype);
|
|
690
|
+
const res = this.data.config.detailResolution;
|
|
691
|
+
this.data.detailMaps.push(new Uint8Array(res * res));
|
|
692
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
693
|
+
}
|
|
694
|
+
paintDetail(worldX, worldZ, prototypeIndex, radius, density) {
|
|
695
|
+
if (prototypeIndex >= this.data.detailMaps.length) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const { width, detailResolution: res } = this.data.config;
|
|
699
|
+
const densityMap = this.data.detailMaps[prototypeIndex];
|
|
700
|
+
const gridX = (worldX + width / 2) / width * (res - 1);
|
|
701
|
+
const gridZ = (worldZ + width / 2) / width * (res - 1);
|
|
702
|
+
const gridRadius = radius / width * res;
|
|
703
|
+
const minX = Math.max(0, Math.floor(gridX - gridRadius));
|
|
704
|
+
const maxX = Math.min(res - 1, Math.ceil(gridX + gridRadius));
|
|
705
|
+
const minZ = Math.max(0, Math.floor(gridZ - gridRadius));
|
|
706
|
+
const maxZ = Math.min(res - 1, Math.ceil(gridZ + gridRadius));
|
|
707
|
+
for (let z = minZ; z <= maxZ; z++) {
|
|
708
|
+
for (let x = minX; x <= maxX; x++) {
|
|
709
|
+
const dx = x - gridX;
|
|
710
|
+
const dz = z - gridZ;
|
|
711
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
712
|
+
if (dist <= gridRadius) {
|
|
713
|
+
const falloff = 1 - dist / gridRadius;
|
|
714
|
+
const idx = z * res + x;
|
|
715
|
+
const current = densityMap[idx];
|
|
716
|
+
densityMap[idx] = Math.min(255, current + density * falloff * 255);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
721
|
+
}
|
|
722
|
+
scatterFoliage(prototypeIndex, minSlope = 0, maxSlope = 45, minHeight = 0, maxHeight = 1) {
|
|
723
|
+
if (prototypeIndex >= this.data.detailPrototypes.length) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const { width, depth, heightmapResolution: _hmRes, detailResolution: res } = this.data.config;
|
|
727
|
+
const densityMap = this.data.detailMaps[prototypeIndex];
|
|
728
|
+
const prototype = this.data.detailPrototypes[prototypeIndex];
|
|
729
|
+
for (let z = 0; z < res; z++) {
|
|
730
|
+
for (let x = 0; x < res; x++) {
|
|
731
|
+
const worldX = x / (res - 1) * width - width / 2;
|
|
732
|
+
const worldZ = z / (res - 1) * depth - depth / 2;
|
|
733
|
+
const height = this.getHeightAtWorld(worldX, worldZ) / this.data.config.maxHeight;
|
|
734
|
+
const normal = this.getNormalAtWorld(worldX, worldZ);
|
|
735
|
+
const slope = Math.acos(normal.y) * (180 / Math.PI);
|
|
736
|
+
const isValidHeight = height >= minHeight && height <= maxHeight;
|
|
737
|
+
const isValidSlope = slope >= minSlope && slope <= maxSlope;
|
|
738
|
+
if (isValidHeight && isValidSlope) {
|
|
739
|
+
const noise = (this.perlinNoise2D(x * 0.1, z * 0.1, 2) + 1) * 0.5;
|
|
740
|
+
const baseDensity = prototype.density * noise;
|
|
741
|
+
densityMap[z * res + x] = Math.min(255, densityMap[z * res + x] + baseDensity * 255);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
746
|
+
}
|
|
747
|
+
// ─── Trees ─────────────────────────────────────────────
|
|
748
|
+
addTreePrototype(prototype) {
|
|
749
|
+
this.data.treePrototypes.push(prototype);
|
|
750
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
751
|
+
}
|
|
752
|
+
addTree(worldX, worldZ, prototypeId, scale, rotation) {
|
|
753
|
+
const prototype = this.data.treePrototypes.find((p) => p.id === prototypeId);
|
|
754
|
+
if (!prototype) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
const height = this.getHeightAtWorld(worldX, worldZ);
|
|
758
|
+
const actualScale = scale ?? prototype.minScale + Math.random() * (prototype.maxScale - prototype.minScale);
|
|
759
|
+
const actualRotation = rotation ?? (prototype.randomRotation ? Math.random() * Math.PI * 2 : 0);
|
|
760
|
+
const instance = {
|
|
761
|
+
position: { x: worldX, y: height, z: worldZ },
|
|
762
|
+
rotation: actualRotation,
|
|
763
|
+
scale: { x: actualScale, y: actualScale, z: actualScale },
|
|
764
|
+
prototypeId,
|
|
765
|
+
color: { r: 1, g: 1, b: 1, a: 1 },
|
|
766
|
+
lightmapColor: { r: 1, g: 1, b: 1, a: 1 }
|
|
767
|
+
};
|
|
768
|
+
this.data.trees.push(instance);
|
|
769
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
770
|
+
return instance;
|
|
771
|
+
}
|
|
772
|
+
scatterTrees(prototypeId, density, minSlope = 0, maxSlope = 30, minHeight = 0, maxHeight = 1, minSpacing = 5) {
|
|
773
|
+
const prototype = this.data.treePrototypes.find((p) => p.id === prototypeId);
|
|
774
|
+
if (!prototype) {
|
|
775
|
+
return 0;
|
|
776
|
+
}
|
|
777
|
+
const { width, depth } = this.data.config;
|
|
778
|
+
let treesAdded = 0;
|
|
779
|
+
const cellSize = minSpacing / Math.SQRT2;
|
|
780
|
+
const gridWidth = Math.ceil(width / cellSize);
|
|
781
|
+
const gridDepth = Math.ceil(depth / cellSize);
|
|
782
|
+
const grid = [];
|
|
783
|
+
for (let i = 0; i < gridWidth; i++) {
|
|
784
|
+
grid[i] = [];
|
|
785
|
+
for (let j = 0; j < gridDepth; j++) {
|
|
786
|
+
grid[i][j] = null;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const numAttempts = Math.floor(width * depth * density);
|
|
790
|
+
const activeList = [];
|
|
791
|
+
const startX = Math.random() * width - width / 2;
|
|
792
|
+
const startZ = Math.random() * depth - depth / 2;
|
|
793
|
+
const startHeight = this.getHeightAtWorld(startX, startZ);
|
|
794
|
+
activeList.push({ x: startX, y: startHeight, z: startZ });
|
|
795
|
+
for (let attempt = 0; attempt < numAttempts && activeList.length > 0; attempt++) {
|
|
796
|
+
const idx = Math.floor(Math.random() * activeList.length);
|
|
797
|
+
const point = activeList[idx];
|
|
798
|
+
let found = false;
|
|
799
|
+
for (let k = 0; k < 30; k++) {
|
|
800
|
+
const angle = Math.random() * Math.PI * 2;
|
|
801
|
+
const r = minSpacing + Math.random() * minSpacing;
|
|
802
|
+
const newX = point.x + Math.cos(angle) * r;
|
|
803
|
+
const newZ = point.z + Math.sin(angle) * r;
|
|
804
|
+
if (newX < -width / 2 || newX > width / 2 || newZ < -depth / 2 || newZ > depth / 2) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const newHeight = this.getHeightAtWorld(newX, newZ) / this.data.config.maxHeight;
|
|
808
|
+
const normal = this.getNormalAtWorld(newX, newZ);
|
|
809
|
+
const slope = Math.acos(normal.y) * (180 / Math.PI);
|
|
810
|
+
if (newHeight < minHeight || newHeight > maxHeight || slope < minSlope || slope > maxSlope) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const gridX = Math.floor((newX + width / 2) / cellSize);
|
|
814
|
+
const gridZ = Math.floor((newZ + depth / 2) / cellSize);
|
|
815
|
+
let tooClose = false;
|
|
816
|
+
for (let di = -2; di <= 2 && !tooClose; di++) {
|
|
817
|
+
for (let dj = -2; dj <= 2 && !tooClose; dj++) {
|
|
818
|
+
const ni = gridX + di;
|
|
819
|
+
const nj = gridZ + dj;
|
|
820
|
+
if (ni >= 0 && ni < gridWidth && nj >= 0 && nj < gridDepth && grid[ni][nj]) {
|
|
821
|
+
const neighbor = grid[ni][nj];
|
|
822
|
+
const dx = neighbor.x - newX;
|
|
823
|
+
const dz = neighbor.z - newZ;
|
|
824
|
+
if (dx * dx + dz * dz < minSpacing * minSpacing) {
|
|
825
|
+
tooClose = true;
|
|
826
|
+
}
|
|
1063
827
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (!tooClose) {
|
|
831
|
+
const newPoint = { x: newX, y: this.getHeightAtWorld(newX, newZ), z: newZ };
|
|
832
|
+
grid[gridX][gridZ] = newPoint;
|
|
833
|
+
activeList.push(newPoint);
|
|
834
|
+
this.addTree(newX, newZ, prototypeId);
|
|
835
|
+
treesAdded++;
|
|
836
|
+
found = true;
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (!found) {
|
|
841
|
+
activeList.splice(idx, 1);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return treesAdded;
|
|
845
|
+
}
|
|
846
|
+
removeTreesInRadius(worldX, worldZ, radius) {
|
|
847
|
+
const initialCount = this.data.trees.length;
|
|
848
|
+
this.data.trees = this.data.trees.filter((tree) => {
|
|
849
|
+
const dx = tree.position.x - worldX;
|
|
850
|
+
const dz = tree.position.z - worldZ;
|
|
851
|
+
return dx * dx + dz * dz > radius * radius;
|
|
852
|
+
});
|
|
853
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
854
|
+
return initialCount - this.data.trees.length;
|
|
855
|
+
}
|
|
856
|
+
// ─── Undo/Redo ─────────────────────────────────────────
|
|
857
|
+
recordHeightChange(gridX, gridZ, oldValue, newValue) {
|
|
858
|
+
const operation = {
|
|
859
|
+
type: "height",
|
|
860
|
+
gridX,
|
|
861
|
+
gridZ,
|
|
862
|
+
oldValue,
|
|
863
|
+
newValue,
|
|
864
|
+
timestamp: Date.now()
|
|
865
|
+
};
|
|
866
|
+
this.undoStack.push(operation);
|
|
867
|
+
if (this.undoStack.length > this.maxUndoSteps) {
|
|
868
|
+
this.undoStack.shift();
|
|
869
|
+
}
|
|
870
|
+
this.redoStack = [];
|
|
871
|
+
}
|
|
872
|
+
undo() {
|
|
873
|
+
const operation = this.undoStack.pop();
|
|
874
|
+
if (!operation) {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
if (operation.type === "height") {
|
|
878
|
+
const res = this.data.config.heightmapResolution;
|
|
879
|
+
this.data.heightmap[operation.gridZ * res + operation.gridX] = operation.oldValue;
|
|
880
|
+
}
|
|
881
|
+
this.redoStack.push(operation);
|
|
882
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
redo() {
|
|
886
|
+
const operation = this.redoStack.pop();
|
|
887
|
+
if (!operation) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
if (operation.type === "height") {
|
|
891
|
+
const res = this.data.config.heightmapResolution;
|
|
892
|
+
this.data.heightmap[operation.gridZ * res + operation.gridX] = operation.newValue;
|
|
893
|
+
}
|
|
894
|
+
this.undoStack.push(operation);
|
|
895
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
// ─── Serialization ─────────────────────────────────────
|
|
899
|
+
getData() {
|
|
900
|
+
return this.data;
|
|
901
|
+
}
|
|
902
|
+
setData(data) {
|
|
903
|
+
this.data = data;
|
|
904
|
+
this.undoStack = [];
|
|
905
|
+
this.redoStack = [];
|
|
906
|
+
}
|
|
907
|
+
serialize() {
|
|
908
|
+
return JSON.stringify({
|
|
909
|
+
...this.data,
|
|
910
|
+
heightmap: Array.from(this.data.heightmap),
|
|
911
|
+
splatmaps: this.data.splatmaps.map((s) => Array.from(s)),
|
|
912
|
+
holemap: Array.from(this.data.holemap),
|
|
913
|
+
detailMaps: this.data.detailMaps.map((d) => Array.from(d))
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
static deserialize(json) {
|
|
917
|
+
const data = JSON.parse(json);
|
|
918
|
+
const terrain = new TerrainSystem(data.config);
|
|
919
|
+
terrain.data = {
|
|
920
|
+
...data,
|
|
921
|
+
heightmap: new Float32Array(data.heightmap),
|
|
922
|
+
splatmaps: data.splatmaps.map((s) => new Uint8Array(s)),
|
|
923
|
+
holemap: new Uint8Array(data.holemap),
|
|
924
|
+
detailMaps: data.detailMaps.map((d) => new Uint8Array(d))
|
|
925
|
+
};
|
|
926
|
+
return terrain;
|
|
927
|
+
}
|
|
928
|
+
// ─── Export Heightmap ──────────────────────────────────
|
|
929
|
+
exportHeightmapAsRAW() {
|
|
930
|
+
const res = this.data.config.heightmapResolution;
|
|
931
|
+
const buffer = new ArrayBuffer(res * res * 2);
|
|
932
|
+
const view = new Uint16Array(buffer);
|
|
933
|
+
for (let i = 0; i < this.data.heightmap.length; i++) {
|
|
934
|
+
view[i] = Math.round(this.data.heightmap[i] * 65535);
|
|
935
|
+
}
|
|
936
|
+
return buffer;
|
|
937
|
+
}
|
|
938
|
+
importHeightmapFromRAW(buffer, is16bit = true) {
|
|
939
|
+
const res = this.data.config.heightmapResolution;
|
|
940
|
+
if (is16bit) {
|
|
941
|
+
const view = new Uint16Array(buffer);
|
|
942
|
+
const expectedSize = res * res;
|
|
943
|
+
if (view.length !== expectedSize) {
|
|
944
|
+
throw new Error(`RAW size mismatch: expected ${expectedSize}, got ${view.length}`);
|
|
945
|
+
}
|
|
946
|
+
for (let i = 0; i < view.length; i++) {
|
|
947
|
+
this.data.heightmap[i] = view[i] / 65535;
|
|
948
|
+
}
|
|
949
|
+
} else {
|
|
950
|
+
const view = new Uint8Array(buffer);
|
|
951
|
+
const expectedSize = res * res;
|
|
952
|
+
if (view.length !== expectedSize) {
|
|
953
|
+
throw new Error(`RAW size mismatch: expected ${expectedSize}, got ${view.length}`);
|
|
954
|
+
}
|
|
955
|
+
for (let i = 0; i < view.length; i++) {
|
|
956
|
+
this.data.heightmap[i] = view[i] / 255;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
this.data.metadata.modifiedAt = Date.now();
|
|
960
|
+
}
|
|
961
|
+
exportSplatmapAsPNG(splatmapIndex) {
|
|
962
|
+
if (splatmapIndex >= this.data.splatmaps.length) {
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
const res = this.data.config.splatmapResolution;
|
|
966
|
+
const splatmap = this.data.splatmaps[splatmapIndex];
|
|
967
|
+
const imageData = new ImageData(res, res);
|
|
968
|
+
for (let i = 0; i < splatmap.length; i++) {
|
|
969
|
+
imageData.data[i] = splatmap[i];
|
|
970
|
+
}
|
|
971
|
+
return imageData;
|
|
972
|
+
}
|
|
1079
973
|
}
|
|
1080
|
-
// ============================================================
|
|
1081
|
-
// Factory Functions
|
|
1082
|
-
// ============================================================
|
|
1083
974
|
function createTerrain(config) {
|
|
1084
|
-
|
|
975
|
+
return new TerrainSystem(config);
|
|
1085
976
|
}
|
|
1086
977
|
function createFlatTerrain(config) {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
return terrain;
|
|
978
|
+
const terrain = new TerrainSystem(config);
|
|
979
|
+
return terrain;
|
|
1090
980
|
}
|
|
1091
981
|
function createHillyTerrain(config) {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
982
|
+
const terrain = new TerrainSystem(config);
|
|
983
|
+
terrain.generatePerlinTerrain(5e-3, 6, 0.5);
|
|
984
|
+
return terrain;
|
|
1095
985
|
}
|
|
1096
986
|
function createMountainousTerrain(config) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
987
|
+
const terrain = new TerrainSystem(config);
|
|
988
|
+
terrain.generateDiamondSquare(0.6);
|
|
989
|
+
terrain.applyHydraulicErosion(3e4);
|
|
990
|
+
return terrain;
|
|
1101
991
|
}
|
|
1102
992
|
function createIslandTerrain(config) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
terrain.setData(data);
|
|
1123
|
-
return terrain;
|
|
993
|
+
const terrain = new TerrainSystem(config);
|
|
994
|
+
const res = terrain.getData().config.heightmapResolution;
|
|
995
|
+
const data = terrain.getData();
|
|
996
|
+
terrain.generatePerlinTerrain(8e-3, 5, 0.5);
|
|
997
|
+
const centerX = res / 2;
|
|
998
|
+
const centerZ = res / 2;
|
|
999
|
+
const maxDist = Math.min(centerX, centerZ) * 0.9;
|
|
1000
|
+
for (let z = 0; z < res; z++) {
|
|
1001
|
+
for (let x = 0; x < res; x++) {
|
|
1002
|
+
const dx = x - centerX;
|
|
1003
|
+
const dz = z - centerZ;
|
|
1004
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
1005
|
+
const falloff = Math.max(0, 1 - dist / maxDist);
|
|
1006
|
+
const smoothFalloff = falloff * falloff * (3 - 2 * falloff);
|
|
1007
|
+
data.heightmap[z * res + x] *= smoothFalloff;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
terrain.setData(data);
|
|
1011
|
+
return terrain;
|
|
1124
1012
|
}
|
|
1125
|
-
|
|
1126
1013
|
exports.DEFAULT_BRUSH = DEFAULT_BRUSH;
|
|
1127
1014
|
exports.DEFAULT_TERRAIN_CONFIG = DEFAULT_TERRAIN_CONFIG;
|
|
1128
1015
|
exports.TerrainSystem = TerrainSystem;
|